Skip to content

gr2m/github-project

Repository files navigation

github-project

JavaScript SDK for GitHub's new Projects

Test

Features

  • 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.

Usage

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 npm install github-project

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);
}

API

Constructor

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.

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 instance, or an instance of any Octokit class that is built upon it, such as octokit.

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

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

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(), as byte size is not the same as text length.

project.getProperties()

const projectData = await project.getProperties();

Returns project level data url, title, description and databaseId

project.items.list()

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

Returns all items in the project.

project.items.addDraft()

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()

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()

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()

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()

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()

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()

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()

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()

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()

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()

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()

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()

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()

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.

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.

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.

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.

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.

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.

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.

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):

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 specification. Contributions of any kind welcome!

License

ISC