Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/gr2m/github-project

JavaScript SDK for GitHub's new Projects
https://github.com/gr2m/github-project

api github github-projects project-next projectnext projects sdk

Last synced: 13 days ago
JSON representation

JavaScript SDK for GitHub's new Projects

Awesome Lists containing this project

README

        

# github-project

> JavaScript SDK for GitHub's new Projects

[![Test](https://github.com/gr2m/github-project/actions/workflows/test.yml/badge.svg)](https://github.com/gr2m/github-project/actions/workflows/test.yml)

## Features

- 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

Browsers

Load github-project directly from cdn.skypack.dev

```html

import GitHubProject from "https://cdn.skypack.dev/github-project";

```

Node

Install with npm install github-project

```js
import GitHubProject from "github-project";
```

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 options = {
owner: "my-org",
number: 1,
token: "ghp_s3cR3t",
fields: {
priority: "Priority",
dueAt: "Due",
lastUpdate: { name: "Last Update", optional: true },
},
};

const project = new GitHubProject(options);

// Alternatively, you can call the factory method to get a project instance
// const project = await GithubProject.getInstance(options)

// get project data
const projectData = await project.get();
console.log(projectData.description);

// log out all items
const items = await project.items.list();
for (const item of items) {
// every item has a `.fields` property for the custom fields
// and an `.content` property which is set unless the item is a draft
console.log(
"%s is due on %s (Priority: %d, Assignees: %j)",
item.fields.title,
item.fields.dueAt,
item.fields.priority,
item.type === "REDACTED"
? "_redacted_"
: item.content.assignees.map(({ login }) => login).join(","),
);
}

// add a new item using an existing issue
// You would usually retrieve the issue node ID from an event payload, such as `event.issue.node_id`
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.getByContentId(issue.node_id);

// item is undefined when not found
if (item) {
// update an item
const updatedItem = await project.items.update(item.id, { priority: 2 });

// remove item
await project.items.remove(item.id);
}
```

## API

### Constructor

```js
const project = new GitHubProject(options);
```

### Factory method

The factory method is useful when you want immediate access to the project's data, for example to get the project's title. Will throw an error if the project doesn't exist.

```js
const project = GitHubProject.getInstance(options);
```




name


type


description






options.owner


string

**Required**. The account name of the GitHub organization.




options.number


Number

**Required**. Project number as you see it in the URL of the project.




options.token


String

**Required unless `options.octokit` is set**. When set to a personal access token or an OAuth token, the `read:org` scope is required for read-only access, and the `write:org` scope is required for read-write access. When set to an installation access token, the `organization_projects:read` permission is required for read-only access, and the `organization_projects:write` permission is required for read-write access.



options.octokit


Octokit

**Required unless `options.token` is set**. You can pass an [`@octokit/core`](https://github.com/octokit/core.js/#readme) instance, or an instance of any `Octokit` class that is built upon it, such as [`octokit`](https://github.com/octokit/octokit.js/#readme).




options.fields


Object

**Required**. A map of internal names for fields to the column names or field option objects. The `title` key will always be set to `"Title"` and `status` to `"Status"` to account for the built-in fields. The other built-in columns `Assignees`, `Labels`, `Linked Pull Requests`, `Milestone`, `Repository`, and `Reviewers` cannot be set through the project and are not considered fields. You have to set them on the issue or pull request, and you can access them by `item.content.assignees`, `item.content.labels` etc (for both issues and pull requests).

A field option object must include a `name` key and can include an `optional` key.

When `optional` is `false` or omitted, an error will be thrown if the field is not found in the project. When `optional` is `true`, the error will be replaced by an `info` log via the [Octokit Logger](https://octokit.github.io/rest.js/v18#logging). Optional fields that don't exist in the project are not set on items returned by the `project.items.*` methods.




options.matchFieldName


Function

Customize how field names are matched with the values provided in `options.fields`. The function accepts two arguments:

1. `projectFieldName`
2. `userFieldName`

Both are strings. Both arguments are lower-cased and trimmed before passed to the function. The function must return `true` or `false`.

Defaults to

```js
function (projectFieldName, userFieldName) {
return projectFieldName === userFieldName
}
```




options.matchFieldOptionValue


Function

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`

Both are strings. Both arguments are trimmed before passed to the function. The function must return `true` or `false`.

Defaults to

```js
function (fieldOptionValue, userValue) {
return fieldOptionValue === userValue
}
```




options.truncate


Function

Text field values cannot exceed 1024 characters. By default, the `options.truncate` just returns text as is. We recommend to use an establish truncate function such as [loadsh's `_.truncate()`](https://lodash.com/docs/4.17.15#truncate), as byte size is not the same as text length.


### `project.getProperties()`

```js
const projectData = await project.getProperties();
```

Returns project level data `url`, `title`, `description` and `databaseId`

### `project.items.list()`

```js
const items = await project.items.list();
```

Returns all items in 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.




name


type


description






content.title


string

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




content.body


string

The body of the issue draft.




content.assigneeIds


string[]

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




fields


object

Map of internal field names to their values.


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

```js
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.




name


type


description






contentId


string

**Required**. The graphql node ID of the issue or pull request you want to add.




fields


object

Map of internal field names to their values.


### `project.items.get()`

```js
const item = await project.items.get(itemNodeId);
```

Retrieve a single item based on its issue or pull request node ID.
Resolves with `undefined` if item cannot be found.




name


type


description






itemNodeId


string

**Required**. The graphql node ID of the project item


### `project.items.getByContentId()`

```js
const item = await project.items.getByContentId(contentId);
```

Retrieve a single item based on its issue or pull request node ID.
Resolves with `undefined` if item cannot be found.




name


type


description






contentId


string

**Required**. The graphql node ID of the issue/pull request the item is linked to.


### `project.items.getByContentRepositoryAndNumber()`

```js
const item = await project.items.getByContentRepositoryAndNumber(
repositoryName,
issueOrPullRequestNumber,
);
```

Retrieve a single item based on its issue or pull request node ID.
Resolves with `undefined` if item cannot be found.




name


type


description






repositoryName


string

**Required**. The repository name, without the `owner/`.




issueOrPullRequestNumber


number

**Required**. The number of the issue or pull request.


### `project.items.update()`

```js
const updatedItem = await project.items.update(itemNodeId, fields);
```

Update an exist item. To unset a field, set it to `null`.
Returns undefined if item cannot be found.




name


type


description






itemNodeId


string

**Required**. The graphql node ID of the project item




fields


object

Map of internal field names to their values.


### `project.items.updateByContentId()`

```js
const updatedItem = await project.items.updateByContentId(contentId, fields);
```

Update an exist item based on the node ID of its linked issue or pull request. To unset a field, set it to `null`.
Returns undefined if item cannot be found.




name


type


description






contentId


string

**Required**. The graphql node ID of the issue/pull request the item is linked to.




fields


object

Map of internal field names to their values.


### `project.items.updateByContentRepositoryAndNumber()`

```js
const updatedItem = await project.items.updateByContentRepositoryAndNumber(
repositoryName,
issueOrPullRequestNumber
fields
);
```

Update an exist item based on the node ID of its linked issue or pull request. To unset a field, set it to `null`.
Returns undefined if item cannot be found.




name


type


description






repositoryName


string

**Required**. The repository name, without the `owner/`.




issueOrPullRequestNumber


number

**Required**. The number of the issue or pull request.




fields


object

Map of internal field names to their values.


### `project.items.archive()`

```js
await project.items.archive(itemNodeId);
```

Archives a single item. Resolves with the archived item or with `undefined` if item was not found.




name


type


description






itemNodeId


string

**Required**. The graphql node ID of the project item


### `project.items.archiveByContentId()`

```js
await project.items.archiveByContentId(contentId);
```

Archives a single item based on the Node ID of its linked issue or pull request. Resolves with the archived item or with `undefined` if item was not found.




name


type


description






contentId


string

**Required**. The graphql node ID of the issue/pull request the item is linked to.


### `project.items.archiveByContentRepositoryAndNumber()`

```js
await project.items.archiveByContentRepositoryAndNumber(
repositoryName,
issueOrPullRequestNumber,
);
```

Archives a single item based on the Node ID of its linked issue or pull request. Resolves with the archived item or with `undefined` if item was not found.




name


type


description






repositoryName


string

**Required**. The repository name, without the `owner/`.




issueOrPullRequestNumber


number

**Required**. The number of the issue or pull request.


### `project.items.remove()`

```js
await project.items.remove(itemNodeId);
```

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




name


type


description






itemNodeId


string

**Required**. The graphql node ID of the project item


### `project.items.removeByContentId()`

```js
await project.items.removeByContentId(contentId);
```

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




name


type


description






contentId


string

**Required**. The graphql node ID of the issue/pull request the item is linked to.


### `project.items.removeByContentRepositoryAndNumber()`

```js
await project.items.removeByContentRepositoryAndNumber(
repositoryName,
issueOrPullRequestNumber,
);
```

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




name


type


description






repositoryName


string

**Required**. The repository name, without the `owner/`.




issueOrPullRequestNumber


number

**Required**. The number of the issue or pull request.


### Errors

Expected errors are thrown using custom `Error` classes. You can check for any error thrown by `github-project` or for specific errors.

Custom errors are designed in a way that `error.message` does not leak any user content. All errors do provide a `.toHumanMessage()` method if you want to provide a more helpful error message which includes both project data as well ase user-provided data.

```js
import Project, { GitHubProjectError } from "github-project";

try {
await myScript(new Project(options));
} catch (error) {
if (error instanceof GitHubProjectError) {
myLogger.error(
{
// .code and .details are always set on GitHubProjectError instances
code: error.code,
details: error.details,
// log out helpful human-readable error message, but beware that it likely contains user content
},
error.toHumanMessage(),
);
} else {
// handle any other error
myLogger.error({ error }, `An unexpected error occurred`);
}

throw error;
}
```

#### `GitHubProjectNotFoundError`

Thrown when a project cannot be found based on the `owner` and `number` passed to the `Project` constructor. The error is also thrown if the project exists but cannot be found based on authentication.

```js
import Project, { GitHubProjectNotFoundError } from "github-project";

try {
await myScript(new Project(options));
} catch (error) {
if (error instanceof GitHubProjectNotFoundError) {
analytics.track("GitHubProjectNotFoundError", {
owner: error.details.owner,
number: error.details.number,
});

myLogger.error(
{
code: error.code,
details: error.details,
},
error.toHumanMessage(),
);
}

throw error;
}
```




name


type


description






name


constant

GitHubProjectNotFoundError



message


constant

> Project cannot be found



details


object

Object with error details




details.owner


string

Login of owner of the project




details.number


number

Number of the project


Example for `error.toHumanMessage()`:

> Project #1 could not be found for @gr2m

#### `GitHubProjectUnknownFieldError`

Thrown when a configured field configured in the `Project` constructor cannot be found in the project.

```js
import Project, { GitHubProjectUnknownFieldError } from "github-project";

try {
await myScript(new Project(options));
} catch (error) {
if (error instanceof GitHubProjectUnknownFieldError) {
analytics.track("GitHubProjectUnknownFieldError", {
projectFieldNames: error.details.projectFieldNames,
userFieldName: error.details.userFieldName,
});

myLogger.error(
{
code: error.code,
details: error.details,
},
error.toHumanMessage(),
);
}

throw error;
}
```




name


type


description






name


constant

GitHubProjectUnknownFieldError



message


constant

> Project field cannot be found



details


object

Object with error details




details.projectFieldNames


string[]

Names of all project fields as shown in the project




details.userFieldName


object

Name of the field provided by the user




details.userFieldNameAlias


object

Alias of the field name provided by the user


Example for `error.toHumanMessage()`:

> "NOPE" could not be matched with any of the existing field names: "My text", "My number", "My Date". If the field should be considered optional, then set it to "nope: { name: "NOPE", optional: true}

#### `GitHubProjectInvalidValueError`

Thrown when attempting to set a single select project field to a value that is not included in the field's configured options.

```js
import Project, { GitHubProjectInvalidValueError } from "github-project";

try {
await myScript(new Project(options));
} catch (error) {
if (error instanceof GitHubProjectInvalidValueError) {
analytics.track("GitHubProjectInvalidValueError", {
fieldName: error.details.field.name,
userValue: error.details.userValue,
});

myLogger.error(
{
code: error.code,
details: error.details,
},
error.toHumanMessage(),
);
}

throw error;
}
```




name


type


description






name


constant

GitHubProjectInvalidValueError



message


constant

> User value is incompatible with project field type



details


object

Object with error details




details.field


object

Object with field details




details.field.id


string

`details.field.id` is the project field GraphQL node ID




details.field.name


string

The field name as shown in the project




details.field.type


string

Is always either `DATE`, `NUMBER`, or `SINGLE_SELECT`. If it's `SINGLE_SELECT`, then the error is a [`GitHubProjectUnknownFieldOptionError`](#githubprojectunknownfieldoptionerror).




details.userValue


string

The stringified value set in the API call.


Example for `error.toHumanMessage()`:

> "unknown" is not compatible with the "My Date" project field

#### `GitHubProjectUnknownFieldOptionError`

Thrown when attempting to set a single select project field to a value that is not included in the field's configured options. Inherits from [`GitHubProjectInvalidValueError`](#githubprojectinvalidvalueerror).

```js
import Project, { GitHubProjectUnknownFieldOptionError } from "github-project";

try {
await myScript(new Project(options));
} catch (error) {
if (error instanceof GitHubProjectUnknownFieldOptionError) {
analytics.track("GitHubProjectUnknownFieldOptionError", {
fieldName: error.details.field.name,
userValue: error.details.userValue,
});

myLogger.error(
{
code: error.code,
details: error.details,
},
error.toHumanMessage(),
);
}

throw error;
}
```




name


type


description






name


constant

GitHubProjectUnknownFieldOptionError



message


constant

> Project field option cannot be found



details


object

Object with error details




details.field


object

Object with field details




details.field.id


string

`details.field.id` is the project field GraphQL node ID




details.field.name


string

The field name as shown in the project




details.field.type


constant

`SINGLE_SELECT`




details.field.options


object[]

Array of objects with project field details




details.field.options[].id


string

The GraphQL node ID of the option




details.field.options[].name


string

The option name as shown in the project.




details.userValue


string

The stringified value set in the API call.


Example for `error.toHumanMessage()`:

> "unknown" is an invalid option for "Single select"

#### `GitHubProjectUpdateReadOnlyFieldError`

Thrown when attempting to set a single select project field to a value that is not included in the field's configured options.

```js
import Project, { GitHubProjectUpdateReadOnlyFieldError } from "github-project";

try {
await myScript(new Project(options));
} catch (error) {
if (error instanceof GitHubProjectUpdateReadOnlyFieldError) {
analytics.track("GitHubProjectUpdateReadOnlyFieldError", {
fieldName: error.details.field.name,
userValue: error.details.userValue,
});

myLogger.error(
{
code: error.code,
details: error.details,
},
error.toHumanMessage(),
);
}

throw error;
}
```




name


type


description






name


constant

GitHubProjectUpdateReadOnlyFieldError



message


constant

> Project read-only field cannot be updated



details


object

Object with error details




details.fields


object[]

Array of objects with read-only fields and their user-provided values




details.fields[].id


string

GraphQL node ID of the project field




details.fields[].name


string

The project field name




details.fields[].userName


string

The user-provided alias for the project field




details.fields[].userValue


string

The user provided value that the user attempted to set the field to.


Example for `error.toHumanMessage()`:

> Cannot update read-only fields: "Assignees" (.assignees) to "gr2m", "Labels" (.labels) to "bug"

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):



Gregor Martynus
Gregor Martynus

🤔 💻 ⚠️ 👀 🚧 🚇
Mike Surowiec
Mike Surowiec

💻 ⚠️
Tom Elliott
Tom Elliott

💻 ⚠️ 👀
Sam Lin
Sam Lin

💻 ⚠️
Evan Bonsignori
Evan Bonsignori

💻 ⚠️ 📖
Baptiste Lombard
Baptiste Lombard

💻 ⚠️

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

## License

[ISC](LICENSE.md)