Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f1690c6
use new ProjectV2 nodes
gr2m Jul 20, 2022
be34f3e
update fixtures for api.items.get
gr2m Jul 20, 2022
e3ebe15
adapt test for `api.items.get`
gr2m Jul 20, 2022
c0245a5
adapt tests for api.items.get-by-content-content-repository-and-number
gr2m Jul 20, 2022
aab6155
update tests for `api.items.get-by-content-id`
gr2m Jul 20, 2022
dd7d66f
update test for `api.items.get-by-content-id-with-non-optional-missin…
gr2m Jul 20, 2022
bdfe021
update test for `api.items.get-by-content-id-with-optional-user-fields`
gr2m Jul 20, 2022
216dabc
remove description/readme field
gr2m Jul 20, 2022
f872fdd
update tests for `api.items.update`
gr2m Jul 20, 2022
d62834b
update test for `api.items.remove-by-content-id`
gr2m Jul 20, 2022
6255317
PNI_...` is now `PVTI_...`
gr2m Jul 20, 2022
e418289
update all the tests
gr2m Jul 20, 2022
df5be85
update comment
gr2m Jul 20, 2022
67db0f0
move "matchFieldName` constructor option" test to recorded tests
gr2m Aug 9, 2022
edac3a0
Move "`matchFieldOptionValue` constructor option" test to recorded tests
gr2m Aug 9, 2022
8d49bc1
return all fields for updated items, not only the ones that got set
gr2m Aug 9, 2022
7b874b4
refactor: remove no longer used internal `removeUndefinedValues` func…
gr2m Aug 9, 2022
3eb6817
paginate test with 201 items to cover all relevant code
gr2m Aug 10, 2022
81dc949
100% test coverage and type fix
gr2m Aug 11, 2022
d5f4106
README: add `project.items.addDraft()` API
gr2m Aug 11, 2022
20f93d1
`project.items.addDraft`
gr2m Aug 11, 2022
6c11bd7
test: api.items.get-draft-item
gr2m Aug 13, 2022
2c918fb
return `item.content` for draft items (some properties) and reducted …
gr2m Aug 13, 2022
3e8857c
replace `org` parameter with `owner`
gr2m Aug 13, 2022
c622024
test: normalize `databaseId` in fixtures
gr2m Aug 14, 2022
366db31
remove obsolete test fixtures
gr2m Aug 14, 2022
899ae19
WIP remove pre-loading and caching of all items
gr2m Aug 14, 2022
883ccf1
remove internal caching, remove APIs now return removed item if found
gr2m Aug 15, 2022
2e4aa96
refactorings, oh sweat refactorings
gr2m Aug 15, 2022
ccef0b8
fix release config
gr2m Aug 15, 2022
43f441c
comment updates
gr2m Aug 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ name: Release
- main
- next
- beta
- alpha
jobs:
release:
name: release
Expand Down
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,41 @@ If a test snapshot needs to be updated, run `ava` with `--update-snapshots`, e.g
npx ava test/recorded.test.js --match api.items.get --update-snapshots
```

To create a new test, copy an existing folder and delete the `fixtures.json` file in it. Update the `prepare.js` and `test.js` files to what's needed for your test. The `prepare.js` file is only used when recording fixtures using [`test/recorded/record-fixtures.js`](test/recorded/record-fixtures.js), this is where you create the state you need for your tests, e.g. create issues and add them as project items with custom properties. Any requests made in `prepare.js` are not part of the test fixtures.

When recording fixtures, all requests made in the `test.js` code are recorded and later replayed when running tests via [`test/recorded.test.js`](test/recorded.test.js).

The `project` instance passed to both the `test()` and `prepare()` functions is setting the default options. If you need a customized `project` instance for your test, you can do the following:

```js
// @ts-check

import GitHubProject from "../../../index.js";

/**
* @param {import("../../..").default} defaultTestProject
* @param {string} [contentId]
*/
export async function test(defaultTestProject, contentId = "I_1") {
const project = new GitHubProject({
owner: defaultTestProject.owner,
number: defaultTestProject.number,
octokit: defaultTestProject.octokit,
fields: {
...defaultTestProject.fields,
nonExistingField: { name: "Nope", optional: false },
},
});

return project.items.getByContentId(contentId).then(
() => new Error("should have thrown"),
(error) => error
);
}
```

The above example also shows how to test for errors: simply return an error instance, without throwing it. That way it's tested with a snapshot, the same way as all the other tests.

## Maintainers only

### Merging the Pull Request & releasing a new version
Expand Down
106 changes: 91 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

## Features

- Use the new Projects as a database of issues and pull requests with custom fields.
- Simple interaction with item fields and content (issue/pull request) properties.
- Look up items by issue/pull request node IDs.
- Use [GitHub Projects (beta)](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects) as a database of issues and pull requests with custom fields.
- Simple interaction with item fields and content (draft/issue/pull request) properties.
- Look up items by issue/pull request node IDs or number and repository name.
- 100% test coverage and type definitions.

## Usage
Expand Down Expand Up @@ -41,13 +41,13 @@ import GitHubProject from "github-project";
</tbody>
</table>

A project always belongs to an organization and has a number. For authentication you can pass [a personal access token with the `write:org` scope](https://github.com/settings/tokens/new?scopes=write:org&description=github-project). For read-only access the `read:org` scope is sufficient.
A project always belongs to a user or organization account and has a number. For authentication you can pass [a personal access token with `project` and `write:org` scopes](https://github.com/settings/tokens/new?scopes=write:org,project&description=github-project). For read-only access the `read:org` and `read:project` scopes are sufficient.

`fields` is map of internal field names to the project's column labels. The comparison is case-insensitive. `"Priority"` will match both a field with the label `"Priority"` and one with the label `"priority"`. An error will be thrown if a project field isn't found, unless the field is set to `optional: true`.

```js
const project = new GitHubProject({
org: "my-org",
owner: "my-org",
number: 1,
token: "ghp_s3cR3t",
fields: {
Expand All @@ -67,8 +67,8 @@ for (const item of items) {
item.fields.title,
item.fields.dueAt,
item.fields.priority,
item.type === "DRAFT_ISSUE"
? "_draft_"
item.type === "REDACTED"
? "_redacted_"
: item.content.assignees.map(({ login }) => login).join(",")
);
}
Expand All @@ -78,7 +78,7 @@ for (const item of items) {
const newItem = await project.items.add(issue.node_id, { priority: 1 });

// retrieve a single item using the issue node ID (passing item node ID as string works, too)
const item = await project.items.get({ contentId: issue.node_id });
const item = await project.items.ggetByContentIdet(issue.node_id);

// item is undefined when not found
if (item) {
Expand Down Expand Up @@ -115,7 +115,7 @@ const project = new GitHubProject(options);
<tbody align=left valign=top>
<tr>
<th>
<code>options.org</code>
<code>options.owner</code>
</th>
<td>
<code>string</code>
Expand Down Expand Up @@ -216,7 +216,7 @@ function (projectFieldName, userFieldName) {
</td>
<td>

Customize how field options are matched with the field values set in `project.items.add()` or `project.items.update*()` methods. The function accepts two arguments:
Customize how field options are matched with the field values set in `project.items.add()`, `project.items.addDraft()`, or `project.items.update*()` methods. The function accepts two arguments:

1. `fieldOptionValue`
2. `userValue`
Expand Down Expand Up @@ -244,6 +244,84 @@ const items = await project.items.list();

Returns the first 100 items of the project.

### `project.items.addDraft()`

```js
const newItem = await project.items.addDraft(content /*, fields*/);
```

Adds a new draft issue item to the project, sets the fields if any were passed, and returns the new item.

<table>
<thead align=left>
<tr>
<th>
name
</th>
<th>
type
</th>
<th width=100%>
description
</th>
</tr>
</thead>
<tbody align=left valign=top>
<tr>
<th>
<code>content.title</code>
</th>
<td>
<code>string</code>
</td>
<td>

**Required**. The title of the issue draft.

</td>
</tr>
<tr>
<th>
<code>content.body</code>
</th>
<td>
<code>string</code>
</td>
<td>

The body of the issue draft.

</td>
</tr>
<tr>
<th>
<code>content.assigneeIds</code>
</th>
<td>
<code>string[]</code>
</td>
<td>

Node IDs of user accounts the issue should be assigned to when created.

</td>
</tr>
<tr>
<th>
<code>fields</code>
</th>
<td>
<code>object</code>
</td>
<td>

Map of internal field names to their values.

</td>
</tr>
</tbody>
</table>

### `project.items.add()`

```js
Expand All @@ -252,8 +330,6 @@ const newItem = await project.items.add(contentId /*, fields*/);

Adds a new item to the project, sets the fields if any were passed, and returns the new item. If the item already exists then it's a no-op, the existing item is still updated with the passed fields if any were passed.

**Note**: GitHub has currently no API to add a draft issue to a project.

<table>
<thead align=left>
<tr>
Expand Down Expand Up @@ -616,7 +692,7 @@ Map of internal field names to their values.
await project.items.remove(itemNodeId);
```

Removes a single item. Resolves with `undefined`, no matter if item was found or not.
Removes a single item. Resolves with the removed item or with `undefined` if item was not found.

<table>
<thead align=left>
Expand Down Expand Up @@ -655,7 +731,7 @@ Removes a single item. Resolves with `undefined`, no matter if item was found or
await project.items.removeByContentId(contentId);
```

Removes a single item based on the Node ID of its linked issue or pull request. Resolves with `undefined`, no matter if item was found or not.
Removes a single item based on the Node ID of its linked issue or pull request. Removes a single item. Resolves with the removed item or with `undefined` if item was not found.

<table>
<thead align=left>
Expand Down Expand Up @@ -697,7 +773,7 @@ await project.items.removeByContentRepositoryAndNumber(
);
```

Removes a single item based on the Node ID of its linked issue or pull request. Resolves with `undefined`, no matter if item was found or not.
Removes a single item based on the Node ID of its linked issue or pull request. Removes a single item. Resolves with the removed item or with `undefined` if item was not found.

<table>
<thead align=left>
Expand Down
67 changes: 67 additions & 0 deletions api/items.add-draft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// @ts-check

import { addDraftIssueToProjectMutation } from "./lib/queries.js";
import { projectItemNodeToGitHubProjectItem } from "./lib/project-item-node-to-github-project-item.js";
import { getStateWithProjectFields } from "./lib/get-state-with-project-fields.js";
import { getFieldsUpdateQueryAndFields } from "./lib/get-fields-update-query-and-fields.js";
import { removeObjectKeys } from "./lib/remove-object-keys.js";

/**
* Creates draft item in project.
*
* @param {import("..").default} project
* @param {import("..").GitHubProjectState} state
* @param {import("..").DraftItemContent} content
* @param {Record<string, string>} [fields]
*
* @returns {Promise<import("..").GitHubProjectItem>}
*/
export async function addDraftItem(project, state, content, fields) {
const stateWithFields = await getStateWithProjectFields(project, state);

const {
addProjectV2DraftIssue: { projectItem: itemNode },
} = await project.octokit.graphql(addDraftIssueToProjectMutation, {
projectId: stateWithFields.id,
title: content.title,
body: content.body,
assigneeIds: content.assigneeIds,
});

const draftItem = projectItemNodeToGitHubProjectItem(
stateWithFields,
itemNode
);

if (!fields) return draftItem;

const nonExistingProjectFields = Object.entries(stateWithFields.fields)
.filter(([, field]) => field.existsInProject === false)
.map(([key]) => key);
const existingProjectFieldKeys = Object.keys(fields).filter(
(key) => !nonExistingProjectFields.includes(key)
);

if (existingProjectFieldKeys.length === 0)
return {
...draftItem,
// @ts-expect-error - complaints that built-in fields `title` and `status` might not exist, but we are good here
fields: removeObjectKeys(draftItem.fields, nonExistingProjectFields),
};

const existingFields = Object.fromEntries(
existingProjectFieldKeys.map((key) => [key, fields[key]])
);

const result = getFieldsUpdateQueryAndFields(stateWithFields, existingFields);

await project.octokit.graphql(result.query, {
projectId: stateWithFields.id,
itemId: draftItem.id,
});

return {
...draftItem,
fields: result.fields,
};
}
Loading