├── .gitignore ├── jsconfig.json ├── test ├── snapshots │ └── recorded.test.js.snap ├── recorded │ ├── api.items.list │ │ ├── test.js │ │ └── prepare.js │ ├── api.getProperties │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-with-pagination │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.remove-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.remove-by-content-id-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-multiple-calls │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-archived │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.remove │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-by-content-id-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-draft-item │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-pull-request │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-using-content-id │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.remove-by-content-id │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft-with-loaded-items │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-unsetting-text-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft-with-empty-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-by-content-id │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-unsetting-single-select-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.remove-by-repository-and-number-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── _lib │ │ ├── clear-test-project.js │ │ ├── create-test-repository.js │ │ ├── delete-all-test-repositories.js │ │ └── octokit.js │ ├── api.items.add-multiple-for-same-issue │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-with-quotes-in-value │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-undefined-value │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-existing-item-after-api.items.list │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-then-api.items.update │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-by-content-id │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-by-content-repository-and-number-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-empty-string │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-existing-item-with-fields-after-api.items.list │ │ ├── test.js │ │ └── prepare.js │ ├── getInstance │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-with-custom-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft-with-invalid-date │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.remove-by-repository-and-number │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-by-content-content-repository-and-number │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-by-content-repository-and-number │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft-with-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-without-custom-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.archive │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-then-api.items.remove-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-without-configuring-custom-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-status-without-user-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-with-fields-using-wrong-capitalization │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.archive-by-content-id │ │ ├── test.js │ │ └── prepare.js │ ├── getInstance-field-not-found │ │ ├── test.js │ │ └── prepare.js │ ├── getInstance-project-not-found │ │ ├── test.js │ │ ├── prepare.js │ │ └── fixtures.json │ ├── api.items.list-then-api.items.remove-clears-cache │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-iteration │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-user-defined-status-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-status-with-spaces-in-field-keys │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-emoji-alias │ │ ├── test.js │ │ └── prepare.js │ ├── api.getProperties-project-not-found │ │ ├── test.js │ │ ├── prepare.js │ │ └── fixtures.json │ ├── api.getProperties-field-not-found │ │ ├── prepare.js │ │ └── test.js │ ├── api.items.update-optional-non-existing-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft-with-custom-truncate-function │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-with-optional-non-existing-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-invalid-field-option │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.add-draft-with-too-long-text │ │ ├── prepare.js │ │ └── test.js │ ├── api.items.get-by-content-id-with-optional-user-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-then-api.items.add-then-api.items.get-by-content-id │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-by-content-id-optional-non-existing-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.get-by-content-id-with-non-optional-missing-user-fields │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-built-in-read-only-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-unknown-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-by-content-repository-and-number-optional-non-existing-field │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.archive-by-content-repository-and-number │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.list-with-match-field-name-option │ │ ├── test.js │ │ └── prepare.js │ ├── api.items.update-with-match-field-option-value-constructor-option │ │ ├── test.js │ │ └── prepare.js │ └── record-fixtures.js ├── smoke.test.js ├── constructor.test.js └── recorded.test.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 02_feature_request.yml │ └── 01_bug_report.yml └── workflows │ ├── release.yml │ ├── update-prettier.yml │ └── test.yml ├── .env.example ├── api ├── lib │ ├── default-match-function.js │ ├── default-truncate-function.js │ ├── remove-object-keys.js │ ├── handle-not-found-graphql-error.js │ ├── project-node-to-properties.js │ ├── archive-project-item.js │ ├── remove-project-item.js │ ├── item-fields-nodes-to-fields-map.js │ ├── get-state-with-project-fields.js │ ├── update-project-item-fields.js │ ├── project-item-node-to-github-project-item.js │ └── project-fields-nodes-to-fields-map.js ├── project.getProperties.js ├── items.remove.js ├── items.remove-by-content-id.js ├── items.archive.js ├── items.archive-by-content-id.js ├── items.update.js ├── items.update-by-content-id.js ├── items.remove-by-content-repository-and-name.js ├── items.get.js ├── items.archive-by-content-repository-and-number.js ├── items.update-by-content-repository-and-number.js ├── items.get-by-content-id.js ├── items.get-by-content-repository-and-number.js ├── items.add.js ├── items.add-draft.js ├── errors.js └── items.list.js ├── LICENSE.md ├── package.json ├── .all-contributorsrc ├── CODE_OF_CONDUCT.md ├── index.js ├── test.js.md └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | coverage 3 | node_modules -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/snapshots/recorded.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gr2m/github-project/HEAD/test/snapshots/recorded.test.js.snap -------------------------------------------------------------------------------- /test/recorded/api.items.list/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.list(); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.getProperties/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export function test(project) { 7 | return project.getProperties(); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-with-pagination/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.list(); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.get(""); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.remove(""); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.addDraft({ title: "Draft Title" }); 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 🆘 I need Help 4 | url: https://github.com/gr2m/github-projects/discussions 5 | about: Got a question? An idea? Feedback? Start here. 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # used for recording fixtures 2 | TEST_REPOSITORY_NAME_PREFIX= 3 | PROJECT_NUMBER= 4 | GH_PROJECT_FIXTURES_APP_ID= 5 | GH_PROJECT_FIXTURES_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- 6 | ... 7 | -----END RSA PRIVATE KEY-----" 8 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.update("", { text: "new text" }); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-content-id-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.removeByContentId(""); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.get/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.get(itemId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-multiple-calls/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export async function test(project) { 7 | await project.items.list(); 8 | return project.items.list(); 9 | } 10 | -------------------------------------------------------------------------------- /api/lib/default-match-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} projectValue 3 | * @param {string} userValue 4 | * 5 | * @returns boolean 6 | */ 7 | export function defaultMatchFunction(projectValue, userValue) { 8 | return projectValue === userValue; 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.add/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | return project.items.add(contentId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-archived/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.get(itemId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.remove(itemId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-id-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.updateByContentId("", { text: "new text" }); 8 | } 9 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-draft-item/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.get(itemId); 9 | } 10 | -------------------------------------------------------------------------------- /api/lib/default-truncate-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Maximum text field value length is 1024 bytes. By default, we don't do anything. 3 | * 4 | * @param {string} text 5 | * @returns {string} 6 | */ 7 | export function defaultTruncateFunction(text) { 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-pull-request/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "PR_1") { 8 | return project.items.add(contentId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-using-content-id/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | return project.items.get(contentId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.update/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.update(itemId, { text: "new text" }); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-content-id/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | return project.items.removeByContentId(contentId); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-loaded-items/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export async function test(project) { 7 | await project.items.list(); 8 | 9 | return project.items.addDraft({ title: "Draft Title" }); 10 | } 11 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-unsetting-text-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.update(itemId, { text: null }); 9 | } 10 | -------------------------------------------------------------------------------- /api/lib/remove-object-keys.js: -------------------------------------------------------------------------------- 1 | /** 2 | * basically the same is `lodash.omit` but simpler given the context 3 | * of this library 4 | */ 5 | export function removeObjectKeys(obj, keys) { 6 | return Object.fromEntries( 7 | Object.entries(obj).filter(([key]) => !keys.includes(key)) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-empty-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.addDraft( 8 | { 9 | title: "Draft Title", 10 | }, 11 | {} 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-id/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | return project.items.updateByContentId(contentId, { text: "new text" }); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-unsetting-single-select-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.update(itemId, { singleSelect: null }); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-repository-and-number-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.removeByContentRepositoryAndNumber( 8 | "unknown-repository", 9 | -1 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /test/recorded/_lib/clear-test-project.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export async function clearTestProject(project) { 7 | const items = await project.items.list(); 8 | for (const item of items) { 9 | await project.items.remove(item.id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-multiple-for-same-issue/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | project.items.add(contentId); 9 | return project.items.add(contentId); 10 | } 11 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-with-quotes-in-value/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | return project.items.add(contentId, { 9 | text: 'Is "it"?', 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-undefined-value/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.update(itemId, { text: "new text", number: undefined }); 9 | } 10 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-existing-item-after-api.items.list/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export async function test(project, contentId = "I_1") { 8 | await project.items.list(); 9 | return project.items.add(contentId); 10 | } 11 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.update/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export async function test(project, itemId = "PVTI_1") { 8 | await project.items.list(); 9 | 10 | return project.items.update(itemId, { text: "new text" }); 11 | } 12 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-id/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | // I_1 is the normalized Issue Node ID in `./fixtures.json` 9 | return project.items.getByContentId(contentId); 10 | } 11 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-repository-and-number-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.updateByContentRepositoryAndNumber( 8 | "unknown-repository", 9 | -1, 10 | { text: "new text" } 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-empty-string/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export function test(project, itemId = "PVTI_1") { 8 | return project.items.update(itemId, { 9 | text: "", 10 | number: "", 11 | date: "", 12 | singleSelect: "", 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-existing-item-with-fields-after-api.items.list/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export async function test(project, contentId = "I_1") { 8 | await project.items.list(); 9 | return project.items.add(contentId, { 10 | status: "Done", 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /api/lib/handle-not-found-graphql-error.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {any} error - error object thrown by `octokit.graphql()` 5 | * @returns void 6 | */ 7 | export function handleNotFoundGraphqlError(error) { 8 | /* c8 ignore next */ 9 | if (!error.errors) throw error; 10 | if (error.errors[0].type === "NOT_FOUND") return; 11 | /* c8 ignore next 2 */ 12 | throw error; 13 | } 14 | -------------------------------------------------------------------------------- /test/recorded/getInstance/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import GitHubProject from "../../../index.js"; 3 | 4 | /** 5 | * @param {import("../../..").default} defaultTestProject 6 | */ 7 | export function test(defaultTestProject) { 8 | return GitHubProject.getInstance({ 9 | owner: defaultTestProject.owner, 10 | number: defaultTestProject.number, 11 | octokit: defaultTestProject.octokit, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-with-custom-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export function test(project, contentId = "I_1") { 8 | return project.items.add(contentId, { 9 | text: "text", 10 | number: "1", 11 | singleSelect: "One", 12 | date: new Date("2020-02-02").toISOString(), 13 | status: "Done", 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-invalid-date/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.addDraft({ title: "test" }, { date: "" }).then( 8 | () => { 9 | throw new Error("Expected error"); 10 | }, 11 | (error) => ({ 12 | error, 13 | humanMessage: error.toHumanMessage(), 14 | }) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-repository-and-number/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [repositoryName] 6 | * @param {number} [issueNumber] 7 | */ 8 | export function test( 9 | project, 10 | repositoryName = "test-repository", 11 | issueNumber = 1 12 | ) { 13 | return project.items.removeByContentRepositoryAndNumber( 14 | repositoryName, 15 | issueNumber 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-content-repository-and-number/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [repositoryName] 6 | * @param {number} [issueNumber] 7 | */ 8 | export function test( 9 | project, 10 | repositoryName = "test-repository", 11 | issueNumber = 1 12 | ) { 13 | return project.items.getByContentRepositoryAndNumber( 14 | repositoryName, 15 | issueNumber 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-repository-and-number/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [repositoryName] 6 | * @param {number} [issueNumber] 7 | */ 8 | export function test( 9 | project, 10 | repositoryName = "test-repository", 11 | issueNumber = 1 12 | ) { 13 | return project.items.updateByContentRepositoryAndNumber( 14 | repositoryName, 15 | issueNumber, 16 | { text: "new text" } 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | */ 6 | export function test(project) { 7 | return project.items.addDraft( 8 | { 9 | title: "Draft Title", 10 | body: "Draft Body", 11 | }, 12 | { 13 | date: "2020-01-01", 14 | number: "123", 15 | singleSelect: "Two", 16 | status: "Done", 17 | text: "Some text", 18 | title: "the hack?", 19 | } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-without-custom-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | */ 8 | export function test(defaultTestProject) { 9 | const project = new GitHubProject({ 10 | owner: defaultTestProject.owner, 11 | number: defaultTestProject.number, 12 | octokit: defaultTestProject.octokit, 13 | fields: {}, 14 | }); 15 | 16 | return project.items.list(); 17 | } 18 | -------------------------------------------------------------------------------- /test/recorded/api.items.archive/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [itemId] 6 | */ 7 | export async function test(project, itemId = "PVTI_1") { 8 | const first = await project.items.archive(itemId); 9 | // 2nd time it won't send a mutation 10 | const second = await project.items.archive(itemId); 11 | // resolves with undefined if not found 12 | const third = await project.items.archive(""); 13 | 14 | return [first, second, third]; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.remove-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * `project.items.list()` creates an internal cache of all items in the project. 5 | * This tests makes sure that `project.items.remove(id)` correctly updates that cache, 6 | * so that `project.items.get(id)` does not return a non-existing item. 7 | * 8 | * @param {import("../../../").default} project 9 | */ 10 | export async function test(project) { 11 | await project.items.list(); 12 | return project.items.remove(""); 13 | } 14 | -------------------------------------------------------------------------------- /api/lib/project-node-to-properties.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Takes a GraphQL `projectItem` node and returns a `ProjectItem` object 5 | * in the format we return it from the GitHubProject API. 6 | * 7 | * @param {import("../..").GitHubProjectStateWithFields} state 8 | * * 9 | * @returns {import("../..").GitHubProjectProperties} 10 | */ 11 | export function projectNodeToProperties(state) { 12 | return { 13 | databaseId: state.databaseId, 14 | id: state.id, 15 | title: state.title, 16 | url: state.url, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-without-configuring-custom-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [contentId] 8 | */ 9 | export function test(defaultTestProject, contentId = "I_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: {}, 15 | }); 16 | 17 | return project.items.add(contentId); 18 | } 19 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-status-without-user-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | }); 15 | 16 | return project.items.update(itemId, { status: "In Progress" }); 17 | } 18 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-with-fields-using-wrong-capitalization/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | */ 8 | export function test(defaultTestProject) { 9 | const project = new GitHubProject({ 10 | owner: defaultTestProject.owner, 11 | number: defaultTestProject.number, 12 | octokit: defaultTestProject.octokit, 13 | fields: { 14 | text: "tExT", 15 | number: "nUMbEr", 16 | }, 17 | }); 18 | 19 | return project.items.list(); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | "on": 3 | push: 4 | branches: 5 | - "*.x" 6 | - main 7 | - next 8 | - beta 9 | jobs: 10 | release: 11 | name: release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-node@v6 16 | with: 17 | node-version: lts/* 18 | cache: npm 19 | - run: npm ci 20 | - run: npx semantic-release 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /test/recorded/api.items.archive-by-content-id/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | * @param {string} [contentId] 6 | */ 7 | export async function test(project, contentId = "I_1") { 8 | const first = await project.items.archiveByContentId(contentId); 9 | // 2nd time it won't send a mutation 10 | const second = await project.items.archiveByContentId(contentId); 11 | // resolves with undefined if not found 12 | const third = await project.items.archiveByContentId(""); 13 | 14 | return [first, second, third]; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/getInstance-field-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import GitHubProject from "../../../index.js"; 3 | 4 | /** 5 | * @param {import("../../..").default} defaultTestProject 6 | */ 7 | export function test(defaultTestProject) { 8 | return GitHubProject.getInstance({ 9 | owner: defaultTestProject.owner, 10 | number: defaultTestProject.number, 11 | octokit: defaultTestProject.octokit, 12 | fields: { 13 | nope: "NOPE", 14 | }, 15 | }).then( 16 | () => { 17 | throw new Error("Should not resolve"); 18 | }, 19 | (error) => error 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/getInstance-project-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import GitHubProject from "../../../index.js"; 3 | 4 | /** 5 | * @param {import("../../..").default} defaultTestProject 6 | */ 7 | export function test(defaultTestProject) { 8 | return GitHubProject.getInstance({ 9 | owner: defaultTestProject.owner, 10 | number: 99999, 11 | octokit: defaultTestProject.octokit, 12 | }).then( 13 | () => { 14 | throw new Error("Should not resolve"); 15 | }, 16 | (error) => ({ 17 | error, 18 | humanMessage: error.toHumanMessage(), 19 | }) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.remove-clears-cache/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * `project.items.list()` creates an internal cache of all items in the project. 5 | * This tests makes sure that `project.items.remove(id)` correctly updates that cache, 6 | * so that `project.items.get(id)` does not return a non-existing item. 7 | * 8 | * @param {import("../../../").default} project 9 | * @param {string} itemId 10 | */ 11 | export async function test(project, itemId = "PVTI_1") { 12 | await project.items.list(); 13 | await project.items.remove(itemId); 14 | return project.items.get(itemId); 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-iteration/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | iteration: "Iteration", 16 | }, 17 | }); 18 | 19 | return project.items.update(itemId, { iteration: "Iteration 2" }); 20 | } 21 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-user-defined-status-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | status: "Text", 16 | }, 17 | }); 18 | 19 | return project.items.update(itemId, { status: "new text" }); 20 | } 21 | -------------------------------------------------------------------------------- /test/recorded/getInstance/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.getProperties/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-status-with-spaces-in-field-keys/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | "My Text": "Text", 16 | }, 17 | }); 18 | 19 | return project.items.update(itemId, { "My Text": "new text" }); 20 | } 21 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-emoji-alias/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestingProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestingProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestingProject.owner, 12 | number: defaultTestingProject.number, 13 | octokit: defaultTestingProject.octokit, 14 | fields: { 15 | "🎯text": "Text", 16 | }, 17 | }); 18 | 19 | return project.items.update(itemId, { "🎯text": "new text" }); 20 | } 21 | -------------------------------------------------------------------------------- /test/recorded/api.getProperties-project-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import GitHubProject from "../../../index.js"; 3 | 4 | /** 5 | * @param {import("../../..").default} defaultTestProject 6 | */ 7 | export function test(defaultTestProject) { 8 | const project = new GitHubProject({ 9 | owner: defaultTestProject.owner, 10 | number: 99999, 11 | octokit: defaultTestProject.octokit, 12 | }); 13 | 14 | return project.getProperties().then( 15 | () => { 16 | throw new Error("Should not resolve"); 17 | }, 18 | (error) => ({ 19 | error, 20 | humanMessage: error.toHumanMessage(), 21 | }) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/getInstance-field-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/getInstance-project-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.getProperties-field-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-optional-non-existing-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | unknown: { name: "Unknown", optional: true }, 16 | }, 17 | }); 18 | 19 | return project.items.update(itemId, { unknown: "nope" }); 20 | } 21 | -------------------------------------------------------------------------------- /test/recorded/api.getProperties-project-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-content-id-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-id-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/update-prettier.yml: -------------------------------------------------------------------------------- 1 | name: Update Prettier 2 | "on": 3 | push: 4 | branches: 5 | - renovate/prettier-* 6 | workflow_dispatch: {} 7 | jobs: 8 | update_prettier: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | - uses: actions/setup-node@v6 13 | with: 14 | cache: npm 15 | node-version: lts/* 16 | - run: npm ci 17 | - run: npm run lint:fix 18 | - run: | 19 | git config user.name github-actions 20 | git config user.email github-actions@github.com 21 | git add . 22 | git commit -m "style: prettier" && git push || true 23 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // no preparation is needed 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-repository-and-number-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | return []; 14 | } 15 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // nothing to prepare 15 | return []; 16 | } 17 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-custom-truncate-function/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../..").default} defaultTestProject 7 | */ 8 | export async function test(defaultTestProject) { 9 | const project = new GitHubProject({ 10 | owner: defaultTestProject.owner, 11 | number: defaultTestProject.number, 12 | octokit: defaultTestProject.octokit, 13 | fields: defaultTestProject.fields, 14 | truncate: (text) => `return of custom truncate of "${text}"`, 15 | }); 16 | 17 | return project.items.addDraft( 18 | { title: "1024+ length test" }, 19 | { text: "text" } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // no preparation is needed 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-with-optional-non-existing-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [contentId] 8 | */ 9 | export function test(defaultTestProject, contentId = "I_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | nonExistingField: { name: "Nope", optional: true }, 16 | }, 17 | }); 18 | return project.items.add(contentId, { 19 | nonExistingField: "Nope?", 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-repository-and-number-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-empty-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // no preparation is needed 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-loaded-items/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // no preparation is needed 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-invalid-field-option/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { GitHubProjectInvalidValueError } from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} project 7 | * @param {string} [itemId] 8 | */ 9 | export function test(project, itemId = "PVTI_1") { 10 | return project.items.update(itemId, { singleSelect: "" }).then( 11 | () => { 12 | throw new Error("Expected error"); 13 | }, 14 | (error) => ({ 15 | error, 16 | humanMessage: error.toHumanMessage(), 17 | isInstanceOfGitHubProjectInvalidValueError: 18 | error instanceof GitHubProjectInvalidValueError, 19 | }) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /api/project.getProperties.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { projectNodeToProperties } from "./lib/project-node-to-properties.js"; 4 | import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js"; 5 | 6 | /** 7 | * Attempts to get a project's properties based on the owner and number. 8 | * 9 | * @param {import("..").default} project 10 | * @param {import("..").GitHubProjectState} state 11 | * 12 | * @returns {Promise} 13 | */ 14 | export async function getProperties(project, state) { 15 | const stateWithFields = await getStateWithProjectFields(project, state); 16 | 17 | return projectNodeToProperties(stateWithFields); 18 | } 19 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-invalid-date/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // no preparation is needed 15 | return []; 16 | } 17 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-too-long-text/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // no preparation is needed 15 | return []; 16 | } 17 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-id-with-optional-user-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../..").default} defaultTestProject 7 | * @param {string} [contentId] 8 | */ 9 | export function test(defaultTestProject, contentId = "I_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | ...defaultTestProject.fields, 16 | nonExistingField: { name: "Nope", optional: true }, 17 | }, 18 | }); 19 | 20 | return project.items.getByContentId(contentId).then(); 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.add-then-api.items.get-by-content-id/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * `project.items.list()` creates a cache. We want to test that this case is updated 5 | * when adding a new item 6 | * 7 | * In thist test, we have a repository with two issues, but only the first issue has 8 | * been added to the project. The 2nd issue (id: I_2) is not in the project yet. 9 | * 10 | * @param {import("../../..").default} project 11 | * @param {string} [contentId] 12 | */ 13 | export async function test(project, contentId = "I_2") { 14 | await project.items.list(); 15 | await project.items.add(contentId); 16 | return project.items.getByContentId(contentId); 17 | } 18 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-custom-truncate-function/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // no preparation is needed 15 | return []; 16 | } 17 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-draft-with-too-long-text/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../..").default} project 5 | */ 6 | export async function test(project) { 7 | // this should not fail, 1024 is the limit 8 | await project.items.addDraft( 9 | { title: "1024 length test" }, 10 | { text: ".".repeat(1024) } 11 | ); 12 | 13 | // this should fail 14 | return project.items 15 | .addDraft({ title: "1024+ length test" }, { text: ".".repeat(1025) }) 16 | .then( 17 | () => { 18 | throw new Error("Expected error"); 19 | }, 20 | (error) => ({ 21 | error, 22 | // humanMessage: error.toHumanMessage(), 23 | }) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /test/recorded/api.getProperties-field-not-found/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import GitHubProject from "../../../index.js"; 3 | 4 | /** 5 | * @param {import("../../..").default} defaultTestProject 6 | */ 7 | export function test(defaultTestProject) { 8 | const project = new GitHubProject({ 9 | owner: defaultTestProject.owner, 10 | number: defaultTestProject.number, 11 | octokit: defaultTestProject.octokit, 12 | fields: { 13 | nope: "NOPE", 14 | }, 15 | }); 16 | 17 | return project.getProperties().then( 18 | () => { 19 | throw new Error("Should not resolve"); 20 | }, 21 | (error) => ({ 22 | error, 23 | humanMessage: error.toHumanMessage(), 24 | }) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-id-optional-non-existing-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [contentId] 8 | */ 9 | export function test(defaultTestProject, contentId = "I_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | unknown: { 16 | name: "Unknown", 17 | optional: true, 18 | }, 19 | }, 20 | }); 21 | return project.items.updateByContentId(contentId, { unknown: "Unknown" }); 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Gregor Martynus 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /api/items.remove.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItem } from "./items.get.js"; 4 | import { removeProjectItem } from "./lib/remove-project-item.js"; 5 | 6 | /** 7 | * Removes an item if it exists. Resolves with the removed item 8 | * or with `undefined` if item was not found. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} itemNodeId 13 | * 14 | * @returns {Promise} 15 | */ 16 | export async function removeItem(project, state, itemNodeId) { 17 | const item = await getItem(project, state, itemNodeId); 18 | if (!item) return; 19 | 20 | await removeProjectItem(project, state, item.id); 21 | return item; 22 | } 23 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-draft-item/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // add issue to project 15 | const item = await project.items.addDraft({ 16 | title: "Draft Item title", 17 | }); 18 | 19 | return [item.id]; 20 | } 21 | -------------------------------------------------------------------------------- /api/items.remove-by-content-id.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItemByContentId } from "./items.get-by-content-id.js"; 4 | import { removeProjectItem } from "./lib/remove-project-item.js"; 5 | 6 | /** 7 | * Removes an item based on content ID. Resolves with the removed item 8 | * or with `undefined` if item was not found. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} contentId 13 | * 14 | * @returns {Promise} 15 | */ 16 | export async function removeItemByContentId(project, state, contentId) { 17 | const item = await getItemByContentId(project, state, contentId); 18 | if (!item) return; 19 | 20 | await removeProjectItem(project, state, item.id); 21 | return item; 22 | } 23 | -------------------------------------------------------------------------------- /test/recorded/_lib/create-test-repository.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {NodeJS.ProcessEnv} env 5 | * @param {import("@octokit/core").Octokit} octokit 6 | * @param {string} owner 7 | * @param {string} testName 8 | * @returns {Promise} 9 | */ 10 | export async function createRepository(env, octokit, owner, testName) { 11 | const prefix = env.TEST_REPOSITORY_NAME_PREFIX; 12 | const timestamp = new Date().toISOString().slice(0, 19).replace(/\D/g, ""); 13 | const name = [prefix, "test", testName, timestamp].filter(Boolean).join("-"); 14 | const { data: repository } = await octokit.request("POST /orgs/{org}/repos", { 15 | org: owner, 16 | name, 17 | auto_init: true, 18 | }); 19 | 20 | return repository; 21 | } 22 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-id-with-non-optional-missing-user-fields/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../..").default} defaultTestProject 7 | * @param {string} [contentId] 8 | */ 9 | export async function test(defaultTestProject, contentId = "I_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | ...defaultTestProject.fields, 16 | nonExistingField: { name: "Nope", optional: false }, 17 | }, 18 | }); 19 | 20 | return project.items.getByContentId(contentId).then( 21 | () => new Error("should have thrown"), 22 | (error) => error 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /api/items.archive.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItem } from "./items.get.js"; 4 | import { archiveProjectItem } from "./lib/archive-project-item.js"; 5 | 6 | /** 7 | * Archives an item if it exists. Resolves with the archived item 8 | * or with `undefined` if item was not found. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} itemNodeId 13 | * 14 | * @returns {Promise} 15 | */ 16 | export async function archiveItem(project, state, itemNodeId) { 17 | const item = await getItem(project, state, itemNodeId); 18 | if (!item) return; 19 | 20 | if (item.isArchived) return item; 21 | 22 | await archiveProjectItem(project, state, item.id); 23 | return { ...item, isArchived: true }; 24 | } 25 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-built-in-read-only-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | assignees: "Assignees", 16 | }, 17 | }); 18 | 19 | return project.items.update(itemId, { assignees: "something" }).then( 20 | () => { 21 | throw new Error("Should not resolve"); 22 | }, 23 | (error) => ({ 24 | error, 25 | humanMessage: error.toHumanMessage(), 26 | }) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-unknown-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export function test(defaultTestProject, itemId = "PVTI_1") { 10 | const project = new GitHubProject({ 11 | owner: defaultTestProject.owner, 12 | number: defaultTestProject.number, 13 | octokit: defaultTestProject.octokit, 14 | fields: { 15 | text: "Text", 16 | unknown: "Unknown", 17 | }, 18 | }); 19 | 20 | return project.items 21 | .update(itemId, { text: "new text", unknown: "nope" }) 22 | .then( 23 | () => { 24 | throw new Error("Should not resolve"); 25 | }, 26 | (error) => { 27 | return error; 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /test/recorded/_lib/delete-all-test-repositories.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {NodeJS.ProcessEnv} env 5 | * @param {InstanceType} octokit 6 | */ 7 | export async function deleteAllTestRepositories(env, octokit, owner) { 8 | const prefix = env.TEST_REPOSITORY_NAME_PREFIX 9 | ? `${env.TEST_REPOSITORY_NAME_PREFIX}-test-` 10 | : "test-"; 11 | const repositoryNames = await octokit.paginate( 12 | "GET /orgs/{org}/repos", 13 | { 14 | org: owner, 15 | }, 16 | (response) => { 17 | return response.data 18 | .map((repository) => repository.name) 19 | .filter((name) => name.startsWith(prefix)); 20 | } 21 | ); 22 | 23 | for (const repo of repositoryNames) { 24 | await octokit.request("DELETE /repos/{owner}/{repo}", { 25 | owner, 26 | repo, 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-repository-and-number-optional-non-existing-field/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../../").default} defaultTestProject 7 | * @param {string} [repositoryName] 8 | * @param {number} [issueNumber] 9 | */ 10 | export function test( 11 | defaultTestProject, 12 | repositoryName = "test-repository", 13 | issueNumber = 1 14 | ) { 15 | const project = new GitHubProject({ 16 | owner: defaultTestProject.owner, 17 | number: defaultTestProject.number, 18 | octokit: defaultTestProject.octokit, 19 | fields: { 20 | unknown: { name: "Unknown", optional: true }, 21 | }, 22 | }); 23 | 24 | return project.items.updateByContentRepositoryAndNumber( 25 | repositoryName, 26 | issueNumber, 27 | { unknown: "nope" } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /api/items.archive-by-content-id.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItemByContentId } from "./items.get-by-content-id.js"; 4 | import { archiveProjectItem } from "./lib/archive-project-item.js"; 5 | 6 | /** 7 | * Archives an item based on content ID. Resolves with the archived item 8 | * or with `undefined` if item was not found. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} contentId 13 | * 14 | * @returns {Promise} 15 | */ 16 | export async function archiveItemByContentId(project, state, contentId) { 17 | const item = await getItemByContentId(project, state, contentId); 18 | if (!item) return; 19 | 20 | if (item.isArchived) return item; 21 | 22 | await archiveProjectItem(project, state, item.id); 23 | return { ...item, isArchived: true }; 24 | } 25 | -------------------------------------------------------------------------------- /test/recorded/api.items.archive-by-content-repository-and-number/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import("../../../").default} project 5 | * @param {string} [repositoryName] 6 | * @param {number} [issueNumber] 7 | */ 8 | export async function test( 9 | project, 10 | repositoryName = "test-repository", 11 | issueNumber = 1 12 | ) { 13 | const first = await project.items.archiveByContentRepositoryAndNumber( 14 | repositoryName, 15 | issueNumber 16 | ); 17 | // 2nd time it won't send a mutation 18 | const second = await project.items.archiveByContentRepositoryAndNumber( 19 | repositoryName, 20 | issueNumber 21 | ); 22 | // resolves with undefined if not found 23 | const third = await project.items.archiveByContentRepositoryAndNumber( 24 | "", 25 | 1 26 | ); 27 | 28 | return [first, second, third]; 29 | } 30 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-with-match-field-name-option/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../..").default} defaultTestProject 7 | */ 8 | export async function test(defaultTestProject) { 9 | const matchFieldNameArguments = []; 10 | 11 | const project = new GitHubProject({ 12 | owner: defaultTestProject.owner, 13 | number: defaultTestProject.number, 14 | octokit: defaultTestProject.octokit, 15 | fields: { 16 | text: "Not Text", 17 | }, 18 | matchFieldName(projectFieldName, userFieldName) { 19 | if (userFieldName === "not text" && projectFieldName === "text") { 20 | return true; 21 | } 22 | 23 | return projectFieldName === userFieldName; 24 | }, 25 | }); 26 | 27 | const result = await project.items.list(); 28 | return { result, matchFieldNameArguments }; 29 | } 30 | -------------------------------------------------------------------------------- /api/items.update.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItem } from "./items.get.js"; 4 | import { updateItemFields } from "./lib/update-project-item-fields.js"; 5 | 6 | /** 7 | * Updates item fields if the item can be found and returns the full item 8 | * with all fields and content properties. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} itemNodeId 13 | * @param {Record} fields 14 | * 15 | * @returns {Promise} 16 | */ 17 | export async function updateItem(project, state, itemNodeId, fields) { 18 | const item = await getItem(project, state, itemNodeId); 19 | if (!item) return; 20 | 21 | const newFields = await updateItemFields(project, state, itemNodeId, fields); 22 | if (!newFields) return item; 23 | 24 | return { 25 | ...item, 26 | fields: newFields, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /api/lib/archive-project-item.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getStateWithProjectFields } from "./get-state-with-project-fields.js"; 4 | import { handleNotFoundGraphqlError } from "./handle-not-found-graphql-error.js"; 5 | import { archiveItemMutation } from "./queries.js"; 6 | 7 | /** 8 | * Helper method to archive an item from a project which is used 9 | * by all the `project.items.archive*` methods. 10 | * 11 | * @param {import("../..").default} project 12 | * @param {import("../..").GitHubProjectState} state 13 | * @param {string} itemNodeId 14 | * 15 | * @returns {Promise} 16 | */ 17 | export async function archiveProjectItem(project, state, itemNodeId) { 18 | const stateWithFields = await getStateWithProjectFields(project, state); 19 | 20 | await project.octokit 21 | .graphql(archiveItemMutation, { 22 | projectId: stateWithFields.id, 23 | itemId: itemNodeId, 24 | }) 25 | .catch(handleNotFoundGraphqlError); 26 | } 27 | -------------------------------------------------------------------------------- /test/recorded/api.items.add/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | return [issue.node_id]; 25 | } 26 | -------------------------------------------------------------------------------- /api/lib/remove-project-item.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getStateWithProjectFields } from "./get-state-with-project-fields.js"; 4 | import { handleNotFoundGraphqlError } from "./handle-not-found-graphql-error.js"; 5 | import { removeItemFromProjectMutation } from "./queries.js"; 6 | 7 | /** 8 | * Helper method to remove an item from a project which is used 9 | * by all the `project.items.remove*` methods. 10 | * 11 | * @param {import("../..").default} project 12 | * @param {import("../..").GitHubProjectState} state 13 | * @param {string} itemNodeId 14 | * 15 | * @returns {Promise} 16 | */ 17 | export async function removeProjectItem(project, state, itemNodeId) { 18 | const stateWithFields = await getStateWithProjectFields(project, state); 19 | 20 | await project.octokit 21 | .graphql(removeItemFromProjectMutation, { 22 | projectId: stateWithFields.id, 23 | itemId: itemNodeId, 24 | }) 25 | .catch(handleNotFoundGraphqlError); 26 | } 27 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-with-custom-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | return [issue.node_id]; 25 | } 26 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-multiple-for-same-issue/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | return [issue.node_id]; 25 | } 26 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-with-quotes-in-value/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | return [issue.node_id]; 25 | } 26 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-with-optional-non-existing-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | return [issue.node_id]; 25 | } 26 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-without-configuring-custom-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | return [issue.node_id]; 25 | } 26 | -------------------------------------------------------------------------------- /test/smoke.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | 3 | import GitHubProject from "../index.js"; 4 | 5 | test("GitHubProject", (t) => { 6 | t.is(typeof GitHubProject, "function"); 7 | }); 8 | 9 | test("getters", (t) => { 10 | const project = new GitHubProject({ 11 | owner: "owner", 12 | number: 1, 13 | token: "ghp_secret123", 14 | }); 15 | 16 | t.throws( 17 | () => { 18 | project.owner = "org2"; 19 | }, 20 | undefined, 21 | "Cannot set read-only property 'owner'" 22 | ); 23 | t.throws( 24 | () => { 25 | project.number = 2; 26 | }, 27 | undefined, 28 | "Cannot set read-only property 'number'" 29 | ); 30 | t.throws( 31 | () => { 32 | project.octokit = new Octokit(); 33 | }, 34 | undefined, 35 | "Cannot set read-only property 'octokit'" 36 | ); 37 | t.throws( 38 | () => { 39 | project.fields = {}; 40 | }, 41 | undefined, 42 | "Cannot set read-only property 'fields'" 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-match-field-option-value-constructor-option/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import GitHubProject from "../../../index.js"; 4 | 5 | /** 6 | * @param {import("../../..").default} defaultTestProject 7 | * @param {string} [itemId] 8 | */ 9 | export async function test(defaultTestProject, itemId = "PVTI_1") { 10 | const matchFieldOptionValueArguments = []; 11 | 12 | const project = new GitHubProject({ 13 | owner: defaultTestProject.owner, 14 | number: defaultTestProject.number, 15 | octokit: defaultTestProject.octokit, 16 | fields: defaultTestProject.fields, 17 | matchFieldOptionValue(fieldOptionValue, userValue) { 18 | matchFieldOptionValueArguments.push({ fieldOptionValue, userValue }); 19 | 20 | return fieldOptionValue === "One" && userValue === "1"; 21 | }, 22 | }); 23 | 24 | const result = await project.items.update(itemId, { singleSelect: "1" }); 25 | 26 | return { result, matchFieldOptionValueArguments }; 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | test_matrix: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node_version: 15 | - 18 16 | - 20 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | - name: Use Node.js ${{ matrix.node_version }} 21 | uses: actions/setup-node@v6 22 | with: 23 | node-version: ${{ matrix.node_version }} 24 | cache: npm 25 | - run: npm ci 26 | - run: npm run test:code 27 | 28 | test: 29 | runs-on: ubuntu-latest 30 | needs: test_matrix 31 | steps: 32 | - uses: actions/checkout@v6 33 | - uses: actions/setup-node@v6 34 | with: 35 | node-version: ${{ matrix.node_version }} 36 | cache: npm 37 | - run: npm ci 38 | - run: npm run test:tsc 39 | - run: npm run test:tsd 40 | - run: npm run lint 41 | -------------------------------------------------------------------------------- /api/items.update-by-content-id.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItemByContentId } from "./items.get-by-content-id.js"; 4 | import { updateItemFields } from "./lib/update-project-item-fields.js"; 5 | 6 | /** 7 | * Updates item fields if the item can be found. 8 | * In order to find an item by content ID, all items need to be loaded first. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} contentNodeId 13 | * @param {Record} fields 14 | * 15 | * @returns {Promise} 16 | */ 17 | export async function updateItemByContentId( 18 | project, 19 | state, 20 | contentNodeId, 21 | fields 22 | ) { 23 | const item = await getItemByContentId(project, state, contentNodeId); 24 | if (!item) return; 25 | 26 | const newFields = await updateItemFields(project, state, item.id, fields); 27 | if (!newFields) return item; 28 | 29 | return { 30 | ...item, 31 | fields: newFields, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-existing-item-after-api.items.list/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue.node_id); 26 | 27 | return [issue.node_id]; 28 | } 29 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-existing-item-with-fields-after-api.items.list/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue.node_id); 26 | 27 | return [issue.node_id]; 28 | } 29 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-unsetting-single-select-field/prepare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 3 | * passed as `test(project, ...arguments)`. 4 | * 5 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 6 | * @param {import("@octokit/core").Octokit} octokit 7 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 8 | * 9 | * @returns {Promise<[string]>} 10 | */ 11 | export async function prepare(repository, octokit, project) { 12 | // create a test issue 13 | const { data: issue } = await octokit.request( 14 | "POST /repos/{owner}/{repo}/issues", 15 | { 16 | owner: repository.owner.login, 17 | repo: repository.name, 18 | title: "Issue", 19 | body: "This is a test issue", 20 | } 21 | ); 22 | 23 | // add issue to project 24 | const item = await project.items.add(issue.node_id, { 25 | singleSelect: "One", 26 | }); 27 | 28 | return [item.id]; 29 | } 30 | -------------------------------------------------------------------------------- /api/items.remove-by-content-repository-and-name.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItemByContentRepositoryAndNumber } from "./items.get-by-content-repository-and-number.js"; 4 | import { removeProjectItem } from "./lib/remove-project-item.js"; 5 | 6 | /** 7 | * Removes an item based on content repository name and number. 8 | * Resolves with the removed item or with `undefined` if item was not found. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} repositoryName 13 | * @param {number} issueOrPullRequestNumber 14 | * 15 | * @returns {Promise} 16 | */ 17 | export async function removeItemByContentRepositoryAndNumber( 18 | project, 19 | state, 20 | repositoryName, 21 | issueOrPullRequestNumber 22 | ) { 23 | const item = await getItemByContentRepositoryAndNumber( 24 | project, 25 | state, 26 | repositoryName, 27 | issueOrPullRequestNumber 28 | ); 29 | if (!item) return; 30 | 31 | await removeProjectItem(project, state, item.id); 32 | return item; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/_lib/octokit.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { Octokit } from "@octokit/core"; 4 | import { paginateRest } from "@octokit/plugin-paginate-rest"; 5 | import { throttling } from "@octokit/plugin-throttling"; 6 | 7 | export default Octokit.plugin(paginateRest, throttling).defaults({ 8 | throttle: { 9 | onRateLimit: (retryAfter, options, octokit) => { 10 | octokit.log.warn( 11 | `Request quota exhausted for request ${options.method} ${options.url}` 12 | ); 13 | 14 | // retry up to 3 times 15 | if (options.request.retryCount < 2) { 16 | octokit.log.info(`Retrying after ${retryAfter} seconds!`); 17 | return true; 18 | } 19 | }, 20 | onSecondaryRateLimit: (retryAfter, options, octokit) => { 21 | octokit.log.warn( 22 | `SecondaryRateLimit detected for request ${options.method} ${options.url}` 23 | ); 24 | 25 | // retry up to 3 times 26 | if (options.request.retryCount < 2) { 27 | octokit.log.info(`Retrying after ${retryAfter} seconds!`); 28 | return true; 29 | } 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /api/items.get.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js"; 4 | import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js"; 5 | import { getItemQuery } from "./lib/queries.js"; 6 | import { handleNotFoundGraphqlError } from "./lib/handle-not-found-graphql-error.js"; 7 | 8 | /** 9 | * Attempts to find an item based on the issues/pull request node id. 10 | * Resolves with undefined if item could not be found. 11 | * 12 | * @param {import("..").default} project 13 | * @param {import("..").GitHubProjectState} state 14 | * @param {string} itemId 15 | * 16 | * @returns {Promise} 17 | */ 18 | export async function getItem(project, state, itemId) { 19 | const stateWithFields = await getStateWithProjectFields(project, state); 20 | 21 | const result = await project.octokit 22 | .graphql(getItemQuery, { 23 | id: itemId, 24 | }) 25 | .catch(handleNotFoundGraphqlError); 26 | 27 | if (!result?.node.id) return; 28 | 29 | return projectItemNodeToGitHubProjectItem(stateWithFields, result.node); 30 | } 31 | -------------------------------------------------------------------------------- /api/items.archive-by-content-repository-and-number.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItemByContentRepositoryAndNumber } from "./items.get-by-content-repository-and-number.js"; 4 | import { archiveProjectItem } from "./lib/archive-project-item.js"; 5 | 6 | /** 7 | * Removes an item based on content repository name and number. 8 | * Resolves with the archived item or with `undefined` if item was not found. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} repositoryName 13 | * @param {number} issueOrPullRequestNumber 14 | * 15 | * @returns {Promise} 16 | */ 17 | export async function archiveItemByContentRepositoryAndNumber( 18 | project, 19 | state, 20 | repositoryName, 21 | issueOrPullRequestNumber 22 | ) { 23 | const item = await getItemByContentRepositoryAndNumber( 24 | project, 25 | state, 26 | repositoryName, 27 | issueOrPullRequestNumber 28 | ); 29 | if (!item) return; 30 | 31 | if (item.isArchived) return item; 32 | 33 | await archiveProjectItem(project, state, item.id); 34 | return { ...item, isArchived: true }; 35 | } 36 | -------------------------------------------------------------------------------- /test/recorded/api.items.get/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.remove-not-found/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return []; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-unsetting-text-field/prepare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 3 | * passed as `test(project, ...arguments)`. 4 | * 5 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 6 | * @param {import("@octokit/core").Octokit} octokit 7 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 8 | * 9 | * @returns {Promise<[string]>} 10 | */ 11 | export async function prepare(repository, octokit, project) { 12 | // create a test issue 13 | const { data: issue } = await octokit.request( 14 | "POST /repos/{owner}/{repo}/issues", 15 | { 16 | owner: repository.owner.login, 17 | repo: repository.name, 18 | title: "Issue", 19 | body: "This is a test issue", 20 | } 21 | ); 22 | 23 | // add issue to project 24 | const item = await project.items.add(issue.node_id, { 25 | text: "text", 26 | number: "1", 27 | date: new Date("2020-02-02").toISOString(), 28 | singleSelect: "One", 29 | }); 30 | 31 | return [item.id]; 32 | } 33 | -------------------------------------------------------------------------------- /test/recorded/api.items.archive/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-content-id/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [issue.node_id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.archive-by-content-id/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [issue.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-id/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [issue.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-using-content-id/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [issue.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-id/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [issue.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.update/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-emoji-alias/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-built-in-read-only-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-empty-string/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-undefined-value/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-unknown-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-optional-non-existing-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-status-without-user-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-invalid-field-option/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-id-with-optional-user-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [issue.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.remove-clears-cache/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-status-with-spaces-in-field-keys/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-user-defined-status-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.remove-by-repository-and-number/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string, number]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [repository.name, issue.number]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-id-optional-non-existing-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [issue.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.archive-by-content-repository-and-number/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string, number]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [repository.name, issue.number]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-with-match-field-option-value-constructor-option/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [item.id]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-id-with-non-optional-missing-user-fields/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | const item = await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-repository-and-number/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string, number]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [repository.name, issue.number]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-by-content-content-repository-and-number/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string, number]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | return [repository.name, issue.number]; 33 | } 34 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-by-content-repository-and-number-optional-non-existing-field/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string, number]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create a test issue 15 | const { data: issue } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | return [repository.name, issue.number]; 34 | } 35 | -------------------------------------------------------------------------------- /api/items.update-by-content-repository-and-number.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getItemByContentRepositoryAndNumber } from "./items.get-by-content-repository-and-number.js"; 4 | import { updateItemFields } from "./lib/update-project-item-fields.js"; 5 | 6 | /** 7 | * Updates item fields if the item can be found. 8 | * In order to find an item by content ID, all items need to be loaded first. 9 | * 10 | * @param {import("..").default} project 11 | * @param {import("..").GitHubProjectState} state 12 | * @param {string} repositoryName 13 | * @param {number} issueOrPullRequestNumber 14 | * @param {Record} fields 15 | * 16 | * @returns {Promise} 17 | */ 18 | export async function updateItemByContentRepositoryAndNumber( 19 | project, 20 | state, 21 | repositoryName, 22 | issueOrPullRequestNumber, 23 | fields 24 | ) { 25 | const item = await getItemByContentRepositoryAndNumber( 26 | project, 27 | state, 28 | repositoryName, 29 | issueOrPullRequestNumber 30 | ); 31 | if (!item) return; 32 | 33 | const newFields = await updateItemFields(project, state, item.id, fields); 34 | if (!newFields) return item; 35 | 36 | return { 37 | ...item, 38 | fields: newFields, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /test/recorded/api.items.add-pull-request/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { composeCreatePullRequest } from "octokit-plugin-create-pull-request"; 4 | 5 | /** 6 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 7 | * passed as `test(project, ...arguments)`. 8 | * 9 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 10 | * @param {import("@octokit/core").Octokit} octokit 11 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 12 | * @returns {Promise<[string]>} 13 | */ 14 | export async function prepare(repository, octokit, project) { 15 | // create a test pull request 16 | // @ts-expect-error - is always set in our case 17 | const { data: pullRequest } = await composeCreatePullRequest(octokit, { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Test", 21 | body: "This is a test pull request", 22 | head: "test", 23 | changes: [ 24 | { 25 | files: { 26 | "README.md": "# Hello, there!", 27 | }, 28 | commit: "Hello, there!", 29 | }, 30 | ], 31 | }); 32 | 33 | return [pullRequest.node_id]; 34 | } 35 | -------------------------------------------------------------------------------- /test/recorded/api.items.update-iteration/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three", iteration: "Iteration 1" | "Iteration 2" | "Iteration 3"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | iteration: "Iteration 1", 31 | }); 32 | 33 | return [item.id]; 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🧚‍♂️ Feature Request" 3 | description: "Wouldn’t it be nice if 💭" 4 | labels: feature 5 | body: 6 | - type: checkboxes 7 | id: search 8 | attributes: 9 | label: Please avoid duplicates 10 | options: 11 | - label: I checked [all open feature requests](https://github.com/gr2m/github-project/issues?q=is%3Aissue+is%3Aopen+label%3Afeature) and none of them matched my problem. 12 | required: true 13 | - type: textarea 14 | id: summary 15 | attributes: 16 | label: What’s missing? 17 | description: Describe your feature idea 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: why 22 | attributes: 23 | label: Why? 24 | description: Describe the problem you are facing 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: alternatives 29 | attributes: 30 | label: Alternatives you tried 31 | description: Describe the workarounds you tried so far and how they worked for you 32 | validations: 33 | required: true 34 | - type: checkboxes 35 | id: contributor 36 | attributes: 37 | label: Would you be interested in contributing the feature? 38 | options: 39 | - label: "yes" 40 | -------------------------------------------------------------------------------- /test/constructor.test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { Octokit } from "@octokit/core"; 3 | 4 | import GitHubProject from "../index.js"; 5 | 6 | test("constructor", (t) => { 7 | const octokit = new Octokit(); 8 | const project = new GitHubProject({ 9 | owner: "owner", 10 | number: 1, 11 | octokit, 12 | }); 13 | 14 | t.deepEqual(project.owner, "owner"); 15 | t.deepEqual(project.number, 1); 16 | t.deepEqual(project.octokit, octokit); 17 | t.deepEqual(project.fields, { 18 | title: "Title", 19 | status: "Status", 20 | }); 21 | }); 22 | 23 | test("constructor with custom fields", (t) => { 24 | const octokit = new Octokit(); 25 | const project = new GitHubProject({ 26 | owner: "owner", 27 | number: 1, 28 | octokit, 29 | fields: { 30 | priority: "Priority", 31 | }, 32 | }); 33 | 34 | t.deepEqual(project.owner, "owner"); 35 | t.deepEqual(project.number, 1); 36 | t.deepEqual(project.octokit, octokit); 37 | t.deepEqual(project.fields, { 38 | title: "Title", 39 | status: "Status", 40 | priority: "Priority", 41 | }); 42 | }); 43 | 44 | test("constructor with token", (t) => { 45 | const project = new GitHubProject({ 46 | owner: "owner", 47 | number: 1, 48 | token: "ghp_secret123", 49 | }); 50 | 51 | t.true(project.octokit instanceof Octokit); 52 | }); 53 | -------------------------------------------------------------------------------- /api/items.get-by-content-id.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js"; 4 | import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js"; 5 | import { getItemByContentIdQuery } from "./lib/queries.js"; 6 | import { handleNotFoundGraphqlError } from "./lib/handle-not-found-graphql-error.js"; 7 | 8 | /** 9 | * Attempts to find an item based on the issues/pull request node id. 10 | * Resolves with undefined if item could not be found. 11 | * 12 | * @param {import("..").default} project 13 | * @param {import("..").GitHubProjectState} state 14 | * @param {string} contentId 15 | * 16 | * @returns {Promise} 17 | */ 18 | export async function getItemByContentId(project, state, contentId) { 19 | const stateWithFields = await getStateWithProjectFields(project, state); 20 | 21 | const result = await project.octokit 22 | .graphql(getItemByContentIdQuery, { 23 | id: contentId, 24 | }) 25 | .catch(handleNotFoundGraphqlError); 26 | 27 | const node = result?.node.projectItems?.nodes.find( 28 | (node) => node.project.number === project.number 29 | ); 30 | 31 | // TODO: add test where an item is added to two projects in order to cover the line below 32 | /* c8 ignore next */ 33 | if (!node) return; 34 | 35 | return projectItemNodeToGitHubProjectItem(stateWithFields, node); 36 | } 37 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-then-api.items.add-then-api.items.get-by-content-id/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[string]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create test issue 1 15 | const { data: issue1 } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue 1", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue1.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | // create test issue 2 34 | const { data: issue2 } = await octokit.request( 35 | "POST /repos/{owner}/{repo}/issues", 36 | { 37 | owner: repository.owner.login, 38 | repo: repository.name, 39 | title: "Issue 2", 40 | body: "This is a test issue", 41 | } 42 | ); 43 | 44 | return [issue2.node_id]; 45 | } 46 | -------------------------------------------------------------------------------- /api/items.get-by-content-repository-and-number.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js"; 4 | import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js"; 5 | import { getItemByContentRepositoryAndNameQuery } from "./lib/queries.js"; 6 | import { handleNotFoundGraphqlError } from "./lib/handle-not-found-graphql-error.js"; 7 | 8 | /** 9 | * Find an item based on the repository and issues/pull request number. 10 | * Resolves with `undefined` if item could not be found. 11 | * 12 | * @param {import("..").default} project 13 | * @param {import("..").GitHubProjectState} state 14 | * @param {string} repositoryName 15 | * @param {number} issueOrPullRequestNumber 16 | * 17 | * @returns {Promise} 18 | */ 19 | export async function getItemByContentRepositoryAndNumber( 20 | project, 21 | state, 22 | repositoryName, 23 | issueOrPullRequestNumber 24 | ) { 25 | const stateWithFields = await getStateWithProjectFields(project, state); 26 | 27 | const result = await project.octokit 28 | .graphql(getItemByContentRepositoryAndNameQuery, { 29 | owner: project.owner, 30 | repositoryName: repositoryName, 31 | number: issueOrPullRequestNumber, 32 | }) 33 | .catch(handleNotFoundGraphqlError); 34 | 35 | const node = 36 | result?.repositoryOwner.repository.issueOrPullRequest.projectItems.nodes.find( 37 | (node) => node.project.number === project.number 38 | ); 39 | 40 | if (!node) return; 41 | 42 | return projectItemNodeToGitHubProjectItem(stateWithFields, node); 43 | } 44 | -------------------------------------------------------------------------------- /test/recorded/api.items.list/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create test issue 1 15 | const { data: issue1 } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue 1", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue1.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | // create test issue 2 34 | const { data: issue2 } = await octokit.request( 35 | "POST /repos/{owner}/{repo}/issues", 36 | { 37 | owner: repository.owner.login, 38 | repo: repository.name, 39 | title: "Issue 2", 40 | body: "This is a test issue", 41 | } 42 | ); 43 | 44 | // add issue to project 45 | await project.items.add(issue2.node_id, { 46 | text: "text", 47 | number: "2", 48 | date: new Date("2020-02-02").toISOString(), 49 | singleSelect: "Two", 50 | }); 51 | 52 | return []; 53 | } 54 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-without-custom-fields/prepare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 3 | * passed as `test(project, ...arguments)`. 4 | * 5 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 6 | * @param {import("@octokit/core").Octokit} octokit 7 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 8 | * 9 | * @returns {Promise<[string]>} 10 | */ 11 | export async function prepare(repository, octokit, project) { 12 | // create test issue 1 13 | const { data: issue1 } = await octokit.request( 14 | "POST /repos/{owner}/{repo}/issues", 15 | { 16 | owner: repository.owner.login, 17 | repo: repository.name, 18 | title: "Issue 1", 19 | body: "This is a test issue", 20 | } 21 | ); 22 | 23 | // add issue to project 24 | await project.items.add(issue1.node_id, { 25 | text: "text", 26 | number: "1", 27 | date: new Date("2020-02-02").toISOString(), 28 | singleSelect: "One", 29 | }); 30 | 31 | // create test issue 2 32 | const { data: issue2 } = await octokit.request( 33 | "POST /repos/{owner}/{repo}/issues", 34 | { 35 | owner: repository.owner.login, 36 | repo: repository.name, 37 | title: "Issue 2", 38 | body: "This is a test issue", 39 | } 40 | ); 41 | 42 | // add issue to project 43 | await project.items.add(issue2.node_id, { 44 | text: "text", 45 | number: "2", 46 | date: new Date("2020-02-02").toISOString(), 47 | singleSelect: "Two", 48 | }); 49 | 50 | return []; 51 | } 52 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-multiple-calls/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create test issue 1 15 | const { data: issue1 } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue 1", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue1.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | // create test issue 2 34 | const { data: issue2 } = await octokit.request( 35 | "POST /repos/{owner}/{repo}/issues", 36 | { 37 | owner: repository.owner.login, 38 | repo: repository.name, 39 | title: "Issue 2", 40 | body: "This is a test issue", 41 | } 42 | ); 43 | 44 | // add issue to project 45 | await project.items.add(issue2.node_id, { 46 | text: "text", 47 | number: "2", 48 | date: new Date("2020-02-02").toISOString(), 49 | singleSelect: "Two", 50 | }); 51 | 52 | return []; 53 | } 54 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-with-fields-using-wrong-capitalization/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create test issue 1 14 | const { data: issue1 } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue 1", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | await project.items.add(issue1.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | // create test issue 2 33 | const { data: issue2 } = await octokit.request( 34 | "POST /repos/{owner}/{repo}/issues", 35 | { 36 | owner: repository.owner.login, 37 | repo: repository.name, 38 | title: "Issue 2", 39 | body: "This is a test issue", 40 | } 41 | ); 42 | 43 | // add issue to project 44 | await project.items.add(issue2.node_id, { 45 | text: "text", 46 | number: "2", 47 | date: new Date("2020-02-02").toISOString(), 48 | singleSelect: "Two", 49 | }); 50 | 51 | return []; 52 | } 53 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-with-match-field-name-option/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * 11 | * @returns {Promise<[]>} 12 | */ 13 | export async function prepare(repository, octokit, project) { 14 | // create test issue 1 15 | const { data: issue1 } = await octokit.request( 16 | "POST /repos/{owner}/{repo}/issues", 17 | { 18 | owner: repository.owner.login, 19 | repo: repository.name, 20 | title: "Issue 1", 21 | body: "This is a test issue", 22 | } 23 | ); 24 | 25 | // add issue to project 26 | await project.items.add(issue1.node_id, { 27 | text: "text", 28 | number: "1", 29 | date: new Date("2020-02-02").toISOString(), 30 | singleSelect: "One", 31 | }); 32 | 33 | // create test issue 2 34 | const { data: issue2 } = await octokit.request( 35 | "POST /repos/{owner}/{repo}/issues", 36 | { 37 | owner: repository.owner.login, 38 | repo: repository.name, 39 | title: "Issue 2", 40 | body: "This is a test issue", 41 | } 42 | ); 43 | 44 | // add issue to project 45 | await project.items.add(issue2.node_id, { 46 | text: "text", 47 | number: "2", 48 | date: new Date("2020-02-02").toISOString(), 49 | singleSelect: "Two", 50 | }); 51 | 52 | return []; 53 | } 54 | -------------------------------------------------------------------------------- /test/recorded/api.items.get-archived/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 8 | * @param {import("@octokit/core").Octokit} octokit 9 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 10 | * @returns {Promise<[string]>} 11 | */ 12 | export async function prepare(repository, octokit, project) { 13 | // create a test issue 14 | const { data: issue } = await octokit.request( 15 | "POST /repos/{owner}/{repo}/issues", 16 | { 17 | owner: repository.owner.login, 18 | repo: repository.name, 19 | title: "Issue", 20 | body: "This is a test issue", 21 | } 22 | ); 23 | 24 | // add issue to project 25 | const item = await project.items.add(issue.node_id, { 26 | text: "text", 27 | number: "1", 28 | date: new Date("2020-02-02").toISOString(), 29 | singleSelect: "One", 30 | }); 31 | 32 | // archive item 33 | // TODO: replace with `project.items.archive()` once its implemented 34 | await octokit.graphql( 35 | ` 36 | mutation addIssueToProject($projectId: ID!, $itemId: ID!) { 37 | archiveProjectV2Item(input:{projectId: $projectId, itemId: $itemId }) { 38 | clientMutationId 39 | } 40 | } 41 | `, 42 | { 43 | // hardcoded to https://github.com/orgs/github-project-fixtures/projects/2 44 | projectId: "PVT_kwDOBYMIeM4ADzd0", 45 | itemId: item.id, 46 | } 47 | ); 48 | 49 | return [item.id]; 50 | } 51 | -------------------------------------------------------------------------------- /test/recorded.test.js: -------------------------------------------------------------------------------- 1 | import { readFile, readdir } from "node:fs/promises"; 2 | 3 | import avaTest from "ava"; 4 | import { Octokit } from "@octokit/core"; 5 | 6 | import GitHubProject from "../index.js"; 7 | 8 | const OWNER = "github-project-fixtures"; 9 | const PROJECT_NUMBER = 2; 10 | 11 | runTests(OWNER, PROJECT_NUMBER); 12 | 13 | async function runTests(owner, projectNumber) { 14 | const testFolders = await readdir("test/recorded"); 15 | 16 | for (const testFolder of testFolders) { 17 | if ( 18 | [`_lib`, `snapshots`].includes(testFolder) || 19 | testFolder.endsWith(`.js`) 20 | ) { 21 | continue; 22 | } 23 | 24 | avaTest.serial(`${testFolder}`, async (t) => { 25 | const { test } = await import(`./recorded/${testFolder}/test.js`); 26 | const fixturesPath = `test/recorded/${testFolder}/fixtures.json`; 27 | const fixtures = JSON.parse(await readFile(fixturesPath, "utf8")); 28 | 29 | const octokit = new Octokit(); 30 | const project = new GitHubProject({ 31 | owner, 32 | number: projectNumber, 33 | octokit, 34 | fields: { 35 | text: "Text", 36 | number: "Number", 37 | date: "Date", 38 | singleSelect: "Single select", 39 | }, 40 | }); 41 | 42 | octokit.hook.wrap("request", async (request, options) => { 43 | console.log("[fixture] %s %s", options.method, options.url); 44 | if (options.url === "/graphql") { 45 | console.log(options.query.trim().split("\n")[0] + " … }"); 46 | } 47 | const { response } = fixtures.shift(); 48 | return response; 49 | }); 50 | 51 | const result = await test(project); 52 | t.snapshot(result); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /api/lib/item-fields-nodes-to-fields-map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Take GraphQL project item fieldValues nodes and turn them into 3 | * an object using the user-defined field names. 4 | * 5 | * @param {import("../..").GitHubProjectStateWithFields} state 6 | * @param {import("../..").ProjectFieldValueNode[]} nodes 7 | * 8 | * @returns {Record & Record} 9 | */ 10 | export function itemFieldsNodesToFieldsMap(state, nodes) { 11 | return Object.entries(state.fields).reduce( 12 | (acc, [projectFieldName, projectField]) => { 13 | // don't set optional fields on items that don't exist in project 14 | if (projectField.existsInProject === false) return acc; 15 | 16 | const node = nodes.find((node) => node.field?.id === projectField.id); 17 | const value = projectFieldValueNodeToValue(projectField, node); 18 | 19 | return { 20 | ...acc, 21 | [projectFieldName]: value, 22 | }; 23 | }, 24 | {} 25 | ); 26 | } 27 | 28 | /** 29 | * @param {import("../..").ProjectField} projectField 30 | * @param {import("../..").ProjectFieldValueNode} node 31 | * @returns {string} 32 | */ 33 | function projectFieldValueNodeToValue(projectField, node) { 34 | if (!node) return null; 35 | 36 | switch (node.__typename) { 37 | case "ProjectV2ItemFieldDateValue": 38 | return node.date; 39 | case "ProjectV2ItemFieldNumberValue": 40 | // we currently only work with strings 41 | return String(node.number); 42 | case "ProjectV2ItemFieldSingleSelectValue": 43 | return projectField.optionsById[node.optionId]; 44 | case "ProjectV2ItemFieldTextValue": 45 | return node.text; 46 | case "ProjectV2ItemFieldIterationValue": 47 | return node.title; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-project", 3 | "version": "0.0.0-development", 4 | "description": "JavaScript SDK for GitHub's new Projects", 5 | "type": "module", 6 | "exports": "./index.js", 7 | "types": "./index.d.ts", 8 | "scripts": { 9 | "test": "npm run test:code && npm run test:tsc && npm run test:tsd && npm run lint", 10 | "test:code": "c8 --100 ava test/*.test.js", 11 | "test:tsc": "tsc --allowJs --noEmit --esModuleInterop --skipLibCheck --lib es2020 index.js", 12 | "test:tsd": "tsd", 13 | "lint": "prettier --check \"*.{js,json,ts,md}\" \".github/**/*.yml\"", 14 | "lint:fix": "prettier --write \"*.{js,json,ts,md}\" \".github/**/*.yml\"", 15 | "coverage": "c8 report --reporter html", 16 | "postcoverage": "open-cli coverage/index.html" 17 | }, 18 | "repository": "github:gr2m/github-project", 19 | "keywords": [ 20 | "github", 21 | "project", 22 | "sdk" 23 | ], 24 | "author": "Gregor Martynus (https://github.com/gr2m)", 25 | "license": "ISC", 26 | "devDependencies": { 27 | "@octokit/app": "^16.0.0", 28 | "@octokit/openapi-types": "^26.0.0", 29 | "@octokit/plugin-paginate-rest": "^13.0.0", 30 | "@octokit/plugin-throttling": "^11.0.0", 31 | "ava": "^6.0.0", 32 | "c8": "^10.0.0", 33 | "dotenv": "^17.0.0", 34 | "octokit-plugin-create-pull-request": "^6.0.0", 35 | "open-cli": "^8.0.0", 36 | "prettier": "^3.0.0", 37 | "tsd": "^0.33.0", 38 | "typescript": "^5.0.0" 39 | }, 40 | "renovate": { 41 | "extends": [ 42 | "github>gr2m/.github" 43 | ] 44 | }, 45 | "release": { 46 | "branches": [ 47 | "+([0-9]).x", 48 | "main", 49 | "next", 50 | { 51 | "name": "beta", 52 | "prerelease": true 53 | } 54 | ] 55 | }, 56 | "dependencies": { 57 | "@octokit/core": "^7.0.0", 58 | "type-fest": "^5.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /api/items.add.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { addIssueToProjectMutation } from "./lib/queries.js"; 4 | import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js"; 5 | import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js"; 6 | import { removeObjectKeys } from "./lib/remove-object-keys.js"; 7 | import { updateItemFields } from "./lib/update-project-item-fields.js"; 8 | 9 | /** 10 | * Adds new item to project. 11 | * 12 | * @param {import("../").default} project 13 | * @param {import("..").GitHubProjectState} state 14 | * @param {string} contentNodeId 15 | * @param {Record} fields 16 | * 17 | * @returns {Promise} 18 | */ 19 | export async function addItem(project, state, contentNodeId, fields) { 20 | const stateWithFields = await getStateWithProjectFields(project, state); 21 | 22 | const { 23 | addProjectV2ItemById: { item }, 24 | } = await project.octokit.graphql(addIssueToProjectMutation, { 25 | projectId: stateWithFields.id, 26 | contentId: contentNodeId, 27 | }); 28 | 29 | const newItem = projectItemNodeToGitHubProjectItem(stateWithFields, item); 30 | 31 | if (!fields) return newItem; 32 | 33 | const nonExistingProjectFields = Object.entries(stateWithFields.fields) 34 | .filter(([, field]) => field.existsInProject === false) 35 | .map(([key]) => key); 36 | 37 | const fieldsAfterUpdate = await updateItemFields( 38 | project, 39 | state, 40 | newItem.id, 41 | fields 42 | ); 43 | 44 | if (!fieldsAfterUpdate) { 45 | return { 46 | ...newItem, 47 | // @ts-expect-error - complaints that built-in fields `title` and `status` might not exist, but we are good here 48 | fields: removeObjectKeys(newItem.fields, nonExistingProjectFields), 49 | }; 50 | } 51 | 52 | return { 53 | ...newItem, 54 | fields: fieldsAfterUpdate, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /api/lib/get-state-with-project-fields.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getProjectCoreDataQuery } from "./queries.js"; 4 | import { projectFieldsNodesToFieldsMap } from "./project-fields-nodes-to-fields-map.js"; 5 | import { GitHubProjectNotFoundError } from "../../index.js"; 6 | 7 | /** 8 | * This method assures that the project fields are loaded. It returns the new 9 | * state for simpler TypeScript IntelliSense, but also mutates the state directly 10 | * 11 | * @param {import("../..").default} project 12 | * @param {import("../..").GitHubProjectState} state 13 | * 14 | * @returns {Promise} 15 | */ 16 | export async function getStateWithProjectFields(project, state) { 17 | if (state.didLoadFields) { 18 | return state; 19 | } 20 | 21 | const response = await getProjectCoreData(project); 22 | 23 | const { 24 | userOrOrganization: { projectV2 }, 25 | } = response; 26 | 27 | const fields = projectFieldsNodesToFieldsMap( 28 | state, 29 | project, 30 | projectV2.fields.nodes 31 | ); 32 | 33 | const { id, title, url, databaseId } = projectV2; 34 | 35 | // mutate current state and return it 36 | // @ts-expect-error - TS can't handle Object.assign 37 | return Object.assign(state, { 38 | didLoadFields: true, 39 | id, 40 | title, 41 | url, 42 | databaseId, 43 | fields, 44 | }); 45 | } 46 | 47 | /** 48 | * 49 | * @param {import("../..").default} project 50 | * @returns {Promise} 51 | */ 52 | async function getProjectCoreData(project) { 53 | try { 54 | return await project.octokit.graphql(getProjectCoreDataQuery, { 55 | owner: project.owner, 56 | number: project.number, 57 | }); 58 | } catch (error) { 59 | /* c8 ignore next */ 60 | if (error?.response?.errors[0]?.type !== "NOT_FOUND") throw error; 61 | 62 | throw new GitHubProjectNotFoundError({ 63 | owner: project.owner, 64 | number: project.number, 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/recorded/api.items.list-with-pagination/prepare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Prepare state in order to record fixtures for test.js. Returns array of arguments that will be passed 5 | * passed as `test(project, ...arguments)`. 6 | * 7 | * For this test we reuse a special, persistant repository and project, as we need 101 issued and project 8 | * items in order to test pagination. Instead of creating all the items each time we record from scratch, 9 | * we check if the `special-test-pagination` repository exists 10 | * 11 | * @param {import("@octokit/openapi-types").components["schemas"]["repository"]} repository 12 | * @param {import("@octokit/core").Octokit} octokit 13 | * @param {import("../../..").default<{text: string, number: number, date: string, singleSelect: "One" | "Two" | "Three"}>} project 14 | * 15 | * @returns {Promise<[]>} 16 | */ 17 | export async function prepare(repository, octokit, project) { 18 | // We load 100 items at a time, and we have two separate queries and hence 19 | // two separate places where we check if there are more items, so we need 20 | // to create 201 items for this test ... 21 | const createIssuesCount = 201; 22 | 23 | console.log( 24 | `Creating ${createIssuesCount} issues and adding them to the project. This will take a while...` 25 | ); 26 | 27 | for (let number = 1; number <= createIssuesCount; number++) { 28 | // create test issue 1 29 | const { data: issue } = await octokit.request( 30 | "POST /repos/{owner}/{repo}/issues", 31 | { 32 | owner: repository.owner.login, 33 | repo: repository.name, 34 | title: "Issue " + number, 35 | body: "This is a test issue", 36 | } 37 | ); 38 | 39 | // add issue to project 40 | await project.items.add(issue.node_id, { 41 | text: "text", 42 | number: String(number), 43 | date: new Date("2020-02-02").toISOString(), 44 | singleSelect: "One", 45 | }); 46 | } 47 | 48 | return []; 49 | } 50 | -------------------------------------------------------------------------------- /api/items.add-draft.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { addDraftIssueToProjectMutation } from "./lib/queries.js"; 4 | import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js"; 5 | import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js"; 6 | import { removeObjectKeys } from "./lib/remove-object-keys.js"; 7 | import { updateItemFields } from "./lib/update-project-item-fields.js"; 8 | 9 | /** 10 | * Creates draft item in project. 11 | * 12 | * @param {import("..").default} project 13 | * @param {import("..").GitHubProjectState} state 14 | * @param {import("..").DraftItemContent} content 15 | * @param {Record} [fields] 16 | * 17 | * @returns {Promise} 18 | */ 19 | export async function addDraftItem(project, state, content, fields) { 20 | const stateWithFields = await getStateWithProjectFields(project, state); 21 | 22 | const { 23 | addProjectV2DraftIssue: { projectItem: itemNode }, 24 | } = await project.octokit.graphql(addDraftIssueToProjectMutation, { 25 | projectId: stateWithFields.id, 26 | title: content.title, 27 | body: content.body, 28 | assigneeIds: content.assigneeIds, 29 | }); 30 | 31 | const draftItem = projectItemNodeToGitHubProjectItem( 32 | stateWithFields, 33 | itemNode 34 | ); 35 | 36 | if (!fields) return draftItem; 37 | 38 | const nonExistingProjectFields = Object.entries(stateWithFields.fields) 39 | .filter(([, field]) => field.existsInProject === false) 40 | .map(([key]) => key); 41 | 42 | const fieldsAfterUpdate = await updateItemFields( 43 | project, 44 | state, 45 | draftItem.id, 46 | fields 47 | ); 48 | 49 | if (!fieldsAfterUpdate) { 50 | return { 51 | ...draftItem, 52 | // @ts-expect-error - complaints that built-in fields `title` and `status` might not exist, but we are good here 53 | fields: removeObjectKeys(draftItem.fields, nonExistingProjectFields), 54 | }; 55 | } 56 | 57 | return { 58 | ...draftItem, 59 | fields: fieldsAfterUpdate, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | description: "Something isn't working as expected 🤔" 4 | labels: bug 5 | body: 6 | - type: checkboxes 7 | id: search 8 | attributes: 9 | label: Please avoid duplicates 10 | options: 11 | - label: I checked [all open bugs](https://github.com/gr2m/github-project/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and none of them matched my problem. 12 | required: true 13 | - type: input 14 | id: testcase 15 | attributes: 16 | label: Reproducible test case 17 | description: | 18 | If possible, please create a minimal test case that reproduces your problem. For TypeScript-only problems, you can create TypeScript playground ([example](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgeQMYwga2PAvnAMyghDgCIABCdLHAelWgFMyAoAgVwDt1gIu4wAM4BlGFGBcA5gAoA7gAsAhjABccIeMlSAlIlytWjLprjUM2eAF44XJnJQ1LMnYeOmkAExVK4+G0pySjhmTjgAdFBMAI4cTJoyZADiAKIAKnB0ZK6sdHRwFDBCALRMAB5gTOilUMRQcABWHKa+nkwgEKzCYhLSMt4wSuGeEPEA+lwQMGPlwjA6QA)). For code-related issues, please create a RunKit Notebook ([example](https://runkit.com/gr2m/octokit-auth-oauth-app-js-178/1.0.0)). 19 | - type: checkboxes 20 | id: environment 21 | attributes: 22 | label: Please select the environment(s) that are relevant to your bug report 23 | options: 24 | - label: Browsers 25 | - label: Node 26 | - label: Deno 27 | - type: textarea 28 | id: versions 29 | attributes: 30 | label: Versions 31 | description: Please share the versions of `github-project` and Node/Deno/Browser that you are using. 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: summary 36 | attributes: 37 | label: What happened? 38 | description: Please share any other relevant information not mentioned above. What did you expect to happen? What do you think the problem might be? 39 | - type: checkboxes 40 | id: contributor 41 | attributes: 42 | label: Would you be interested in contributing a fix? 43 | options: 44 | - label: "yes" 45 | -------------------------------------------------------------------------------- /api/lib/update-project-item-fields.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { GitHubProjectInvalidValueError } from "../../index.js"; 4 | import { getFieldsUpdateQueryAndFields } from "./get-fields-update-query-and-fields.js"; 5 | import { getStateWithProjectFields } from "./get-state-with-project-fields.js"; 6 | 7 | /** 8 | * Helper method to update fields of a project item which is used 9 | * by all the `project.items.update*` methods. 10 | * 11 | * @param {import("../..").default} project 12 | * @param {import("../..").GitHubProjectState} state 13 | * @param {string} itemNodeId 14 | * @param {Record} fields 15 | * 16 | * @returns {Promise} 17 | */ 18 | export async function updateItemFields(project, state, itemNodeId, fields) { 19 | const stateWithFields = await getStateWithProjectFields(project, state); 20 | 21 | const existingProjectFieldKeys = Object.keys(fields).filter( 22 | (key) => stateWithFields.fields[key].existsInProject 23 | ); 24 | 25 | if (existingProjectFieldKeys.length === 0) return; 26 | 27 | const existingFields = Object.fromEntries( 28 | existingProjectFieldKeys.map((key) => [key, fields[key]]) 29 | ); 30 | 31 | const result = getFieldsUpdateQueryAndFields(stateWithFields, existingFields); 32 | 33 | try { 34 | await project.octokit.graphql(result.query, { 35 | projectId: stateWithFields.id, 36 | itemId: itemNodeId, 37 | }); 38 | } catch (error) { 39 | const isInvalidValueError = 40 | error?.response?.errors?.[0]?.extensions?.code === 41 | "argumentLiteralsIncompatible"; 42 | 43 | /* c8 ignore next */ 44 | if (!isInvalidValueError) throw error; 45 | 46 | const key = error.response.errors[0].path[1]; 47 | const field = stateWithFields.fields[key]; 48 | 49 | throw new GitHubProjectInvalidValueError({ 50 | userValue: fields[key], 51 | field: { 52 | // @ts-expect-error 53 | id: field.id, 54 | // @ts-expect-error 55 | name: field.name, 56 | // @ts-expect-error 57 | type: field.dataType, 58 | }, 59 | }); 60 | } 61 | 62 | return result.fields; 63 | } 64 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "gr2m", 12 | "name": "Gregor Martynus", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/39992?v=4", 14 | "profile": "https://dev.to/gr2m", 15 | "contributions": [ 16 | "ideas", 17 | "code", 18 | "test", 19 | "review", 20 | "maintenance", 21 | "infra" 22 | ] 23 | }, 24 | { 25 | "login": "mikesurowiec", 26 | "name": "Mike Surowiec", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/821435?v=4", 28 | "profile": "https://mikesurowiec.com", 29 | "contributions": [ 30 | "code", 31 | "test" 32 | ] 33 | }, 34 | { 35 | "login": "tmelliottjr", 36 | "name": "Tom Elliott", 37 | "avatar_url": "https://avatars.githubusercontent.com/u/13594679?v=4", 38 | "profile": "https://github.com/tmelliottjr", 39 | "contributions": [ 40 | "code", 41 | "test", 42 | "review" 43 | ] 44 | }, 45 | { 46 | "login": "maxisam", 47 | "name": "Sam Lin", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/456807?v=4", 49 | "profile": "http://maxisam.github.io/", 50 | "contributions": [ 51 | "code", 52 | "test" 53 | ] 54 | }, 55 | { 56 | "login": "Ebonsignori", 57 | "name": "Evan Bonsignori", 58 | "avatar_url": "https://avatars.githubusercontent.com/u/17055832?v=4", 59 | "profile": "http://evan.bio", 60 | "contributions": [ 61 | "code", 62 | "test", 63 | "doc" 64 | ] 65 | }, 66 | { 67 | "login": "blombard", 68 | "name": "Baptiste Lombard", 69 | "avatar_url": "https://avatars.githubusercontent.com/u/17877656?v=4", 70 | "profile": "https://www.leexi.ai/", 71 | "contributions": [ 72 | "code", 73 | "test" 74 | ] 75 | } 76 | ], 77 | "contributorsPerLine": 7, 78 | "skipCi": true, 79 | "repoType": "github", 80 | "repoHost": "https://github.com", 81 | "projectName": "github-project", 82 | "projectOwner": "gr2m" 83 | } 84 | -------------------------------------------------------------------------------- /api/lib/project-item-node-to-github-project-item.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { itemFieldsNodesToFieldsMap } from "./item-fields-nodes-to-fields-map.js"; 4 | 5 | /** 6 | * Takes a GraphQL `projectItem` node and returns a `ProjectItem` object 7 | * in the format we return it from the GitHubProject API. 8 | * 9 | * @param {import("../..").GitHubProjectStateWithFields} state 10 | * @param {any} itemNode 11 | * 12 | * @returns {import("../..").GitHubProjectItem} 13 | */ 14 | export function projectItemNodeToGitHubProjectItem(state, itemNode) { 15 | const fields = itemFieldsNodesToFieldsMap(state, itemNode.fieldValues.nodes); 16 | 17 | const common = { 18 | type: itemNode.type, 19 | id: itemNode.id, 20 | isArchived: itemNode.isArchived, 21 | fields, 22 | }; 23 | 24 | if (itemNode.type === "DRAFT_ISSUE") { 25 | return { 26 | ...common, 27 | content: { 28 | id: itemNode.content.id, 29 | title: itemNode.content.title, 30 | createdAt: itemNode.content.createdAt, 31 | assignees: itemNode.content.assignees.nodes.map((node) => node.login), 32 | }, 33 | }; 34 | } 35 | 36 | const isIssueOrPullRequest = 37 | itemNode.type === "ISSUE" || itemNode.type === "PULL_REQUEST"; 38 | // content might be unset for deleted issues / pull requests (e.g. in case of a deleted spam user) 39 | const hasContent = itemNode.content !== null; 40 | 41 | if (isIssueOrPullRequest && hasContent) { 42 | // item is issue or pull request 43 | const issue = { 44 | id: itemNode.content.id, 45 | number: itemNode.content.number, 46 | createdAt: itemNode.content.createdAt, 47 | closed: itemNode.content.closed, 48 | closedAt: itemNode.content.closedAt, 49 | assignees: itemNode.content.assignees.nodes.map((node) => node.login), 50 | labels: itemNode.content.labels.nodes.map((node) => node.name), 51 | repository: itemNode.content.repository.name, 52 | milestone: itemNode.content.milestone, 53 | title: itemNode.content.title, 54 | url: itemNode.content.url, 55 | databaseId: itemNode.content.databaseId, 56 | }; 57 | 58 | const content = 59 | itemNode.type === "ISSUE" 60 | ? issue 61 | : { ...issue, merged: itemNode.content.merged }; 62 | 63 | return { 64 | ...common, 65 | content, 66 | }; 67 | } 68 | /* c8 ignore next 9 */ 69 | 70 | // fallback: no content properties are set. Currently that's in case of "REDACTED" 71 | return { 72 | type: itemNode.type, 73 | id: itemNode.id, 74 | isArchived: itemNode.isArchived, 75 | fields, 76 | content: {}, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /api/errors.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | export class GitHubProjectError extends Error { 4 | constructor(message) { 5 | super(message); 6 | this.name = this.constructor.name; 7 | this.details = {}; 8 | } 9 | /* c8 ignore start */ 10 | toHumanMessage() { 11 | return this.message; 12 | } 13 | /* c8 ignore stop */ 14 | } 15 | 16 | export class GitHubProjectNotFoundError extends GitHubProjectError { 17 | constructor(details) { 18 | super("Project cannot be found"); 19 | this.details = details; 20 | } 21 | 22 | toHumanMessage() { 23 | return `Project #${this.details.number} could not be found for @${this.details.owner}`; 24 | } 25 | } 26 | 27 | export class GitHubProjectUnknownFieldError extends GitHubProjectError { 28 | constructor(details) { 29 | super("Project field cannot be found"); 30 | this.details = details; 31 | } 32 | 33 | toHumanMessage() { 34 | const projectFieldNames = this.details.projectFieldNames 35 | .map((node) => `"${node.name}"`) 36 | .join(", "); 37 | return `"${this.details.userFieldName}" could not be matched with any of the existing field names: ${projectFieldNames}. If the field should be considered optional, then set it to "${this.details.userFieldNameAlias}: { name: "${this.details.userFieldName}", optional: true}`; 38 | } 39 | } 40 | 41 | export class GitHubProjectInvalidValueError extends GitHubProjectError { 42 | constructor(details) { 43 | super("User value is incompatible with project field type"); 44 | this.details = details; 45 | } 46 | 47 | toHumanMessage() { 48 | return `"${this.details.userValue}" is not compatible with the "${this.details.field.name}" project field which expects a value of type "${this.details.field.type}"`; 49 | } 50 | } 51 | 52 | export class GitHubProjectUnknownFieldOptionError extends GitHubProjectInvalidValueError { 53 | constructor(details) { 54 | super(details); 55 | this.message = "Project field option cannot be found"; 56 | this.details = details; 57 | } 58 | 59 | toHumanMessage() { 60 | const existingOptionsString = this.details.field.options 61 | .map((option) => `"${option.name}"`) 62 | .join(", "); 63 | 64 | return `"${this.details.userValue}" is an invalid option for "${this.details.field.name}".\n\nKnown options are:\n${existingOptionsString}`; 65 | } 66 | } 67 | 68 | export class GitHubProjectUpdateReadOnlyFieldError extends GitHubProjectError { 69 | constructor(details) { 70 | super("Project read-only field cannot be updated"); 71 | this.details = details; 72 | } 73 | 74 | toHumanMessage() { 75 | return `Cannot update read-only fields: ${this.details.fields 76 | .map(({ userValue, userName }) => `"${userValue}" (.${userName})`) 77 | .join(", ")}`; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /api/items.list.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { 4 | getProjectWithItemsQuery, 5 | getProjectItemsPaginatedQuery, 6 | } from "./lib/queries.js"; 7 | import { projectFieldsNodesToFieldsMap } from "./lib/project-fields-nodes-to-fields-map.js"; 8 | import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js"; 9 | 10 | /** 11 | * Load all project items 12 | * 13 | * @param {import("..").default} project 14 | * @param {import("..").GitHubProjectState} state 15 | * @returns {Promise} 16 | */ 17 | export async function listItems(project, state) { 18 | const { 19 | userOrOrganization: { projectV2 }, 20 | } = await project.octokit.graphql(getProjectWithItemsQuery, { 21 | owner: project.owner, 22 | number: project.number, 23 | }); 24 | 25 | const fields = projectFieldsNodesToFieldsMap( 26 | state, 27 | project, 28 | projectV2.fields.nodes 29 | ); 30 | 31 | const items = projectV2.items.nodes.map((node) => { 32 | // @ts-expect-error - for simplicity only pass fields instead of a full state 33 | return projectItemNodeToGitHubProjectItem({ fields }, node); 34 | }); 35 | 36 | // Recursively fetch all additional items for this project 37 | if (projectV2.items.pageInfo.hasNextPage) { 38 | await fetchProjectItems(project, fields, { 39 | cursor: projectV2.items.pageInfo.endCursor, 40 | results: items, 41 | }); 42 | } 43 | 44 | const { id, title, url } = projectV2; 45 | 46 | // mutate current state 47 | Object.assign(state, { 48 | didLoadFields: true, 49 | id, 50 | title, 51 | url, 52 | fields, 53 | }); 54 | 55 | return items; 56 | } 57 | 58 | /** 59 | * This method recursively executes a paginated query to gather all project items for a project 60 | * It collects the items into options.results, and can start from an arbitrary GraphQL cursor 61 | * 62 | * @param {import("..").default} project 63 | * @param {import("..").ProjectFieldMap} fields 64 | * @param {{ cursor?: string | undefined, results?: Array }} options 65 | * 66 | * @returns {Promise} 67 | */ 68 | async function fetchProjectItems( 69 | project, 70 | fields, 71 | { cursor = undefined, results = [] } = {} 72 | ) { 73 | const { 74 | userOrOrganization: { 75 | projectV2: { items }, 76 | }, 77 | } = await project.octokit.graphql(getProjectItemsPaginatedQuery, { 78 | owner: project.owner, 79 | number: project.number, 80 | first: 100, 81 | after: cursor, 82 | }); 83 | 84 | results.push( 85 | ...items.nodes.map((node) => { 86 | // @ts-expect-error - for simplicity only pass fields instead of a full state 87 | return projectItemNodeToGitHubProjectItem({ fields }, node); 88 | }) 89 | ); 90 | 91 | if (items.pageInfo.hasNextPage) { 92 | await fetchProjectItems(project, fields, { 93 | results, 94 | cursor: items.pageInfo.endCursor, 95 | }); 96 | } 97 | 98 | return results; 99 | } 100 | -------------------------------------------------------------------------------- /test/recorded/getInstance-project-not-found/fixtures.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "query": "\n query getProjectCoreData($owner: String!, $number: Int!) {\n userOrOrganization: repositoryOwner(login: $owner) {\n ... on ProjectV2Owner {\n projectV2(number: $number) {\n \n id\n title\n url\n databaseId\n fields(first: 50) {\n nodes {\n ... on ProjectV2FieldCommon {\n id\n dataType\n name\n }\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n ... on ProjectV2IterationField {\n configuration {\n iterations {\n title\n duration\n startDate\n }\n completedIterations {\n title\n duration\n startDate\n }\n duration\n startDay\n }\n }\n }\n }\n\n }\n }\n }\n }\n", 4 | "variables": { 5 | "owner": "github-project-fixtures", 6 | "number": 99999 7 | }, 8 | "response": { 9 | "status": 200, 10 | "url": "https://api.github.com/graphql", 11 | "headers": { 12 | "access-control-allow-origin": "*", 13 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 14 | "connection": "close", 15 | "content-encoding": "gzip", 16 | "content-security-policy": "default-src 'none'", 17 | "content-type": "application/json; charset=utf-8", 18 | "date": "Sat, 20 May 2023 00:26:01 GMT", 19 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 20 | "server": "GitHub.com", 21 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 22 | "transfer-encoding": "chunked", 23 | "vary": "Accept-Encoding, Accept, X-Requested-With", 24 | "x-content-type-options": "nosniff", 25 | "x-frame-options": "deny", 26 | "x-github-media-type": "github.v3; format=json", 27 | "x-github-request-id": "E839:7581:96367AB:9ADE7CC:64681399", 28 | "x-ratelimit-limit": "5000", 29 | "x-ratelimit-remaining": "4496", 30 | "x-ratelimit-reset": "1684545298", 31 | "x-ratelimit-resource": "graphql", 32 | "x-ratelimit-used": "504", 33 | "x-xss-protection": "0" 34 | }, 35 | "data": { 36 | "data": { 37 | "userOrOrganization": { 38 | "projectV2": null 39 | } 40 | }, 41 | "errors": [ 42 | { 43 | "type": "NOT_FOUND", 44 | "path": [ 45 | "userOrOrganization", 46 | "projectV2" 47 | ], 48 | "locations": [ 49 | { 50 | "line": 5, 51 | "column": 9 52 | } 53 | ], 54 | "message": "Could not resolve to a ProjectV2 with the number 99999." 55 | } 56 | ] 57 | } 58 | } 59 | } 60 | ] -------------------------------------------------------------------------------- /test/recorded/api.getProperties-project-not-found/fixtures.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "query": "\n query getProjectCoreData($owner: String!, $number: Int!) {\n userOrOrganization: repositoryOwner(login: $owner) {\n ... on ProjectV2Owner {\n projectV2(number: $number) {\n \n id\n title\n url\n databaseId\n fields(first: 50) {\n nodes {\n ... on ProjectV2FieldCommon {\n id\n dataType\n name\n }\n ... on ProjectV2SingleSelectField {\n options {\n id\n name\n }\n }\n ... on ProjectV2IterationField {\n configuration {\n iterations {\n title\n duration\n startDate\n }\n completedIterations {\n title\n duration\n startDate\n }\n duration\n startDay\n }\n }\n }\n }\n\n }\n }\n }\n }\n", 4 | "variables": { 5 | "owner": "github-project-fixtures", 6 | "number": 99999 7 | }, 8 | "response": { 9 | "status": 200, 10 | "url": "https://api.github.com/graphql", 11 | "headers": { 12 | "access-control-allow-origin": "*", 13 | "access-control-expose-headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", 14 | "connection": "close", 15 | "content-encoding": "gzip", 16 | "content-security-policy": "default-src 'none'", 17 | "content-type": "application/json; charset=utf-8", 18 | "date": "Sat, 20 May 2023 00:15:13 GMT", 19 | "referrer-policy": "origin-when-cross-origin, strict-origin-when-cross-origin", 20 | "server": "GitHub.com", 21 | "strict-transport-security": "max-age=31536000; includeSubdomains; preload", 22 | "transfer-encoding": "chunked", 23 | "vary": "Accept-Encoding, Accept, X-Requested-With", 24 | "x-content-type-options": "nosniff", 25 | "x-frame-options": "deny", 26 | "x-github-media-type": "github.v3; format=json", 27 | "x-github-request-id": "E53B:4AD2:8F6A462:944B6F1:64681111", 28 | "x-ratelimit-limit": "5000", 29 | "x-ratelimit-remaining": "4989", 30 | "x-ratelimit-reset": "1684545298", 31 | "x-ratelimit-resource": "graphql", 32 | "x-ratelimit-used": "11", 33 | "x-xss-protection": "0" 34 | }, 35 | "data": { 36 | "data": { 37 | "userOrOrganization": { 38 | "projectV2": null 39 | } 40 | }, 41 | "errors": [ 42 | { 43 | "type": "NOT_FOUND", 44 | "path": [ 45 | "userOrOrganization", 46 | "projectV2" 47 | ], 48 | "locations": [ 49 | { 50 | "line": 5, 51 | "column": 9 52 | } 53 | ], 54 | "message": "Could not resolve to a ProjectV2 with the number 99999." 55 | } 56 | ] 57 | } 58 | } 59 | } 60 | ] -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at coc+github-project@martynus.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { Octokit } from "@octokit/core"; 4 | 5 | import { listItems } from "./api/items.list.js"; 6 | import { addDraftItem } from "./api/items.add-draft.js"; 7 | import { addItem } from "./api/items.add.js"; 8 | import { getItem } from "./api/items.get.js"; 9 | import { getItemByContentId } from "./api/items.get-by-content-id.js"; 10 | import { getItemByContentRepositoryAndNumber } from "./api/items.get-by-content-repository-and-number.js"; 11 | import { updateItem } from "./api/items.update.js"; 12 | import { updateItemByContentId } from "./api/items.update-by-content-id.js"; 13 | import { updateItemByContentRepositoryAndNumber } from "./api/items.update-by-content-repository-and-number.js"; 14 | import { archiveItem } from "./api/items.archive.js"; 15 | import { archiveItemByContentId } from "./api/items.archive-by-content-id.js"; 16 | import { archiveItemByContentRepositoryAndNumber } from "./api/items.archive-by-content-repository-and-number.js"; 17 | import { removeItem } from "./api/items.remove.js"; 18 | import { removeItemByContentId } from "./api/items.remove-by-content-id.js"; 19 | import { removeItemByContentRepositoryAndNumber } from "./api/items.remove-by-content-repository-and-name.js"; 20 | import { getProperties } from "./api/project.getProperties.js"; 21 | 22 | import { defaultMatchFunction } from "./api/lib/default-match-function.js"; 23 | import { defaultTruncateFunction } from "./api/lib/default-truncate-function.js"; 24 | 25 | /** @type {import("./").BUILT_IN_FIELDS} */ 26 | export const BUILT_IN_FIELDS = { 27 | title: "Title", 28 | status: "Status", 29 | }; 30 | 31 | export * from "./api/errors.js"; 32 | 33 | export default class GitHubProject { 34 | /** 35 | * @param {import(".").GitHubProjectOptions} options 36 | */ 37 | constructor(options) { 38 | const { owner, number, fields = {} } = options; 39 | 40 | // set octokit either from `options.octokit` or `options.token` 41 | const octokit = 42 | "token" in options 43 | ? new Octokit({ auth: options.token }) 44 | : options.octokit; 45 | 46 | /** @type {import(".").GitHubProjectState} */ 47 | const state = { 48 | didLoadFields: false, 49 | matchFieldName: options.matchFieldName || defaultMatchFunction, 50 | matchFieldOptionValue: 51 | options.matchFieldOptionValue || defaultMatchFunction, 52 | truncate: options.truncate || defaultTruncateFunction, 53 | }; 54 | 55 | // set API 56 | const itemsApi = { 57 | list: listItems.bind(null, this, state), 58 | addDraft: addDraftItem.bind(null, this, state), 59 | add: addItem.bind(null, this, state), 60 | get: getItem.bind(null, this, state), 61 | getByContentId: getItemByContentId.bind(null, this, state), 62 | getByContentRepositoryAndNumber: getItemByContentRepositoryAndNumber.bind( 63 | null, 64 | this, 65 | state, 66 | ), 67 | update: updateItem.bind(null, this, state), 68 | updateByContentId: updateItemByContentId.bind(null, this, state), 69 | updateByContentRepositoryAndNumber: 70 | updateItemByContentRepositoryAndNumber.bind(null, this, state), 71 | archive: archiveItem.bind(null, this, state), 72 | archiveByContentId: archiveItemByContentId.bind(null, this, state), 73 | archiveByContentRepositoryAndNumber: 74 | archiveItemByContentRepositoryAndNumber.bind(null, this, state), 75 | remove: removeItem.bind(null, this, state), 76 | removeByContentId: removeItemByContentId.bind(null, this, state), 77 | removeByContentRepositoryAndNumber: 78 | removeItemByContentRepositoryAndNumber.bind(null, this, state), 79 | }; 80 | 81 | Object.defineProperties(this, { 82 | owner: { get: () => owner }, 83 | number: { get: () => number }, 84 | fields: { get: () => ({ ...BUILT_IN_FIELDS, ...fields }) }, 85 | octokit: { get: () => octokit }, 86 | items: { get: () => itemsApi }, 87 | getProperties: { get: () => getProperties.bind(null, this, state) }, 88 | }); 89 | } 90 | 91 | /** 92 | * Returns a GithubProject instance and calls `getProperties()` to preload 93 | * project level properties. 94 | * 95 | * @param {import(".").GitHubProjectOptions} options 96 | * 97 | * @return {Promise} 98 | */ 99 | static async getInstance(options) { 100 | const project = /** @type {import(".").default} */ ( 101 | new GitHubProject(options) 102 | ); 103 | await project.getProperties(); 104 | 105 | return project; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /api/lib/project-fields-nodes-to-fields-map.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { GitHubProjectUnknownFieldError } from "../../index.js"; 4 | 5 | /** 6 | * Takes `project.fields` and the list of project item fieldValues nodes 7 | * from the GraphQL query result: 8 | * 9 | * ``` 10 | * fieldValues(...) { 11 | * nodes { 12 | * id 13 | * name 14 | * settings 15 | * } 16 | * } 17 | * ``` 18 | * 19 | * and turns them into a map 20 | * 21 | * ``` 22 | * { 23 | * "title": { 24 | * "id": "", 25 | * "name": "Title", 26 | * }, 27 | * "status": { 28 | * "id": "", 29 | * "name": "Status", 30 | * "optionsByValue": { 31 | * "In Progress": "