JavaScript SDK for GitHub's new Projects
- Use GitHub Projects (beta) 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.
Browsers |
Load github-project directly from cdn.skypack.dev
<script type="module">
import GitHubProject from "https://cdn.skypack.dev/github-project";
</script> |
---|---|
Node |
Install with 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. 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
.
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);
}
const project = new GitHubProject(options);
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.
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
|
Octokit
|
Required unless |
options.fields
|
Object
|
Required. A map of internal names for fields to the column names or field option objects. The A field option object must include a When |
options.matchFieldName
|
Function
|
Customize how field names are matched with the values provided in
Both are strings. Both arguments are lower-cased and trimmed before passed to the function. The function must return Defaults to function (projectFieldName, userFieldName) {
return projectFieldName === userFieldName
} |
options.matchFieldOptionValue
|
Function
|
Customize how field options are matched with the field values set in
Both are strings. Both arguments are trimmed before passed to the function. The function must return Defaults to function (fieldOptionValue, userValue) {
return fieldOptionValue === userValue
} |
options.truncate
|
Function
|
Text field values cannot exceed 1024 characters. By default, the |
const projectData = await project.getProperties();
Returns project level data url
, title
, description
and databaseId
const items = await project.items.list();
Returns the first 100 items of the project.
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. |
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. |
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 |
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. |
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 |
issueOrPullRequestNumber
|
number
|
Required. The number of the issue or pull request. |
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. |
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. |
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 |
issueOrPullRequestNumber
|
number
|
Required. The number of the issue or pull request. |
fields
|
object
|
Map of internal field names to their values. |
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 |
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. |
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 |
issueOrPullRequestNumber
|
number
|
Required. The number of the issue or pull request. |
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 |
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. |
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 |
issueOrPullRequestNumber
|
number
|
Required. The number of the issue or pull request. |
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.
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;
}
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.
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
|
|
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
Thrown when a configured field configured in the Project
constructor cannot be found in the project.
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
|
|
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}
Thrown when attempting to set a single select project field to a value that is not included in the field's configured options.
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
|
|
details
|
object
|
Object with error details |
details.field
|
object
|
Object with field details |
details.field.id
|
string
|
|
details.field.name
|
string
|
The field name as shown in the project |
details.field.type
|
string
|
Is always either |
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
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
.
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
|
|
details
|
object
|
Object with error details |
details.field
|
object
|
Object with field details |
details.field.id
|
string
|
|
details.field.name
|
string
|
The field name as shown in the project |
details.field.type
|
constant
|
|
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"
Thrown when attempting to set a single select project field to a value that is not included in the field's configured options.
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
|
|
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"
Thanks goes to these wonderful people (emoji key):
Gregor Martynus 🤔 💻 |
Mike Surowiec 💻 |
Tom Elliott 💻 |
Sam Lin 💻 |
Evan Bonsignori 💻 |
Baptiste Lombard 💻 |
This project follows the all-contributors specification. Contributions of any kind welcome!