Skip to content

Commit 9437ed7

Browse files
gr2mtmelliottjr
andauthored
feat: GitHubProjectError, GitHubProjectUnknownFieldError, GitHubProjectUnknownFieldOptionError, GitHubProjectUpdateReadOnlyFieldError (#132)
BREAKING CHANGE: When a field could not be found, the message on the thrown error looked like this > [github-project] "NOPE" could not be matched with any of the existing field names: "Title", "Assignees", "Status", "Labels", "Linked pull requests", "Reviewers", "Repository", "Milestone", "Text", "Number", "Date", "Single select", "Iteration". If the field should be considered optional, then set it to "nope: { name: "NOPE", optional: true} it is now > Project field cannot be found The original message can still be retrieved using `error.toHumanMessage()` BREAKING CHANGE: When a user value cannot be matched with a field option, the message on the thrown error looked like this > [github-project] "unknown" is an invalid option for "Single select" it is now > Project field option cannot be found The original message can still be retrieved using `error.toHumanMessage()` BREAKING CHANGE: if a user tried to update a read-only field, the message on the thrown error looked like this > [github-project] Cannot update read-only fields: "Assignees" (.assignees) it is now > Project read-only field cannot be updated --------- Co-authored-by: Tom Elliott <[email protected]>
1 parent fe3a502 commit 9437ed7

File tree

12 files changed

+831
-59
lines changed

12 files changed

+831
-59
lines changed

README.md

+484
Large diffs are not rendered by default.

api/errors.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 GitHubProjectUnknownFieldError extends GitHubProjectError {
17+
constructor(details) {
18+
super("Project field cannot be found");
19+
this.details = details;
20+
}
21+
22+
toHumanMessage() {
23+
const projectFieldNames = this.details.projectFieldNames
24+
.map((node) => `"${node.name}"`)
25+
.join(", ");
26+
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.userInternalFieldName}: { name: "${this.details.userFieldName}", optional: true}`;
27+
}
28+
}
29+
30+
export class GitHubProjectUnknownFieldOptionError extends GitHubProjectError {
31+
constructor(details) {
32+
super("Project field option cannot be found");
33+
this.details = details;
34+
}
35+
36+
toHumanMessage() {
37+
const existingOptionsString = this.details.field.options
38+
.map((option) => `"${option.name}"`)
39+
.join(", ");
40+
41+
return `"${this.details.userValue}" is an invalid option for "${this.details.field.name}".\n\nKnown options are:\n${existingOptionsString}`;
42+
}
43+
}
44+
45+
export class GitHubProjectUpdateReadOnlyFieldError extends GitHubProjectError {
46+
constructor(details) {
47+
super("Project read-only field cannot be updated");
48+
this.details = details;
49+
}
50+
51+
toHumanMessage() {
52+
return `Cannot update read-only fields: ${this.details.fields
53+
.map(({ userValue, userName }) => `"${userValue}" (.${userName})`)
54+
.join(", ")}`;
55+
}
56+
}

api/lib/get-fields-update-query-and-fields.js

+32-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
// @ts-check
22

3+
import {
4+
GitHubProjectUnknownFieldOptionError,
5+
GitHubProjectUpdateReadOnlyFieldError,
6+
} from "../../index.js";
37
import { queryItemFieldNodes } from "./queries.js";
48

59
/**
@@ -45,26 +49,29 @@ export function getFieldsUpdateQueryAndFields(state, fields) {
4549
.map((key) => [key, fields[key] === "" ? null : fields[key]])
4650
);
4751

48-
const readOnlyFields = Object.keys(existingFields)
49-
.map((key) => [key, state.fields[key].userName])
50-
.filter(([key]) => {
51-
const field = state.fields[key];
52+
const readOnlyFields = Object.entries(existingFields)
53+
.map(([id, userValue]) => ({
54+
id,
55+
// @ts-expect-error - assume state.fields[id] is not OptionalNonExistingField
56+
name: String(state.fields[id].name),
57+
userName: state.fields[id].userName,
58+
userValue,
59+
}))
60+
.filter(({ id, name }) => {
61+
const field = state.fields[id];
5262
return READ_ONLY_FIELDS.some((readOnlyField) => {
5363
return state.matchFieldName(
5464
readOnlyField.toLowerCase(),
5565

56-
// @ts-expect-error - TODO: unclear why `field` is typed as potential "string" here
57-
field.name.toLowerCase().trim()
66+
name.toLowerCase().trim()
5867
);
5968
});
6069
});
6170

6271
if (readOnlyFields.length > 0) {
63-
throw new Error(
64-
`[github-project] Cannot update read-only fields: ${readOnlyFields
65-
.map(([key, value]) => `"${value}" (.${key})`)
66-
.join(", ")}`
67-
);
72+
throw new GitHubProjectUpdateReadOnlyFieldError({
73+
fields: readOnlyFields,
74+
});
6875
}
6976

7077
/** @type {Record<string, {query: string, key: string, value: string|undefined}>[]} */
@@ -103,9 +110,9 @@ export function getFieldsUpdateQueryAndFields(state, fields) {
103110

104111
const query = `
105112
${alias}: updateProjectV2ItemFieldValue(input: {projectId: $projectId, itemId: $itemId, fieldId: "${fieldId}", ${toItemFieldValueInput(
106-
field,
107-
valueOrOption
108-
)}}) {
113+
field,
114+
valueOrOption
115+
)}}) {
109116
${queryNodes}
110117
}
111118
`;
@@ -186,20 +193,19 @@ function findFieldOptionIdAndValue(state, field, value) {
186193
) || [];
187194

188195
if (!optionId) {
189-
const knownOptions = Object.keys(field.optionsByValue);
190-
const existingOptionsString = knownOptions
191-
.map((value) => `- ${value}`)
192-
.join("\n");
196+
const options = Object.entries(field.optionsByValue).map(([name, id]) => {
197+
return { name, id };
198+
});
193199

194200
throw Object.assign(
195-
new Error(
196-
`[github-project] "${value}" is an invalid option for "${field.name}".\n\nKnown options are:\n${existingOptionsString}`
197-
),
198-
{
199-
code: "E_GITHUB_PROJECT_UNKNOWN_FIELD_OPTION",
200-
knownOptions,
201-
userOption: value,
202-
}
201+
new GitHubProjectUnknownFieldOptionError({
202+
field: {
203+
id: field.id,
204+
name: field.name,
205+
options,
206+
},
207+
userValue: value,
208+
})
203209
);
204210
}
205211

api/lib/project-fields-nodes-to-fields-map.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// @ts-check
22

3+
import { GitHubProjectUnknownFieldError } from "../../index.js";
4+
35
/**
46
* Takes `project.fields` and the list of project item fieldValues nodes
57
* from the GraphQL query result:
@@ -81,16 +83,16 @@ export function projectFieldsNodesToFieldsMap(state, project, nodes) {
8183
);
8284

8385
if (!node) {
84-
const projectFieldNames = nodes
85-
.map((node) => `"${node.name}"`)
86-
.join(", ");
86+
const projectFieldNames = nodes.map((node) => node.name);
8787
if (!fieldOptional) {
88-
throw new Error(
89-
`[github-project] "${userFieldName}" could not be matched with any of the existing field names: ${projectFieldNames}. If the field should be considered optional, then set it to "${userInternalFieldName}: { name: "${userFieldName}", optional: true}`
90-
);
88+
throw new GitHubProjectUnknownFieldError({
89+
userFieldName,
90+
userInternalFieldName,
91+
projectFieldNames,
92+
});
9193
}
9294
project.octokit.log.info(
93-
`[github-project] optional field "${userFieldName}" was not matched with any existing field names: ${projectFieldNames}`
95+
`optional field "${userFieldName}" was not matched with any existing field names: ${projectFieldNames}`
9496
);
9597
return acc;
9698
}

index.d.ts

+59
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,62 @@ export type GitHubProjectStateWithFields = GitHubProjectStateCommon & {
302302
fields: ProjectFieldMap;
303303
databaseId: string;
304304
};
305+
306+
export declare class GitHubProjectError extends Error {
307+
// This causes an odd error that I don't know how to workaround
308+
// > Property name in type ... is not assignable to the same property in base type GitHubProjectError.
309+
// name: "GitHubProjectError";
310+
details: {};
311+
toHumanMessage(): string;
312+
}
313+
314+
type GitHubProjectUnknownFieldErrorDetails = {
315+
projectFieldNames: string[];
316+
userFieldName: string;
317+
userInternalFieldName: string;
318+
};
319+
320+
export declare class GitHubProjectUnknownFieldError<
321+
TDetails extends GitHubProjectUnknownFieldErrorDetails,
322+
> extends GitHubProjectError {
323+
name: "GitHubProjectUnknownFieldError";
324+
details: TDetails;
325+
constructor(details: TDetails);
326+
}
327+
328+
type GitHubProjectUnknownFieldOptionErrorDetails = {
329+
userValue: string;
330+
field: {
331+
id: string;
332+
name: string;
333+
options: {
334+
id: string;
335+
name: string;
336+
}[];
337+
};
338+
};
339+
340+
export declare class GitHubProjectUnknownFieldOptionError<
341+
TDetails extends GitHubProjectUnknownFieldOptionErrorDetails,
342+
> extends GitHubProjectError {
343+
name: "GitHubProjectUnknownFieldOptionError";
344+
details: TDetails;
345+
constructor(details: TDetails);
346+
}
347+
348+
type GitHubProjectUpdateReadOnlyFieldErrorDetails = {
349+
fields: {
350+
id: string;
351+
name: string;
352+
userName: string;
353+
userValue: string | null;
354+
}[];
355+
};
356+
357+
export declare class GitHubProjectUpdateReadOnlyFieldError<
358+
TDetails extends GitHubProjectUpdateReadOnlyFieldErrorDetails,
359+
> extends GitHubProjectError {
360+
name: "GitHubProjectUpdateReadOnlyFieldError";
361+
details: TDetails;
362+
constructor(details: TDetails);
363+
}

index.js

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const BUILT_IN_FIELDS = {
2727
status: "Status",
2828
};
2929

30+
export * from "./api/errors.js";
31+
3032
export default class GitHubProject {
3133
/**
3234
* @param {import(".").GitHubProjectOptions} options

index.test-d.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { expectType, expectNotType } from "tsd";
22
import { Octokit } from "@octokit/core";
3-
import GitHubProject from "./index";
3+
import GitHubProject, {
4+
GitHubProjectError,
5+
GitHubProjectUnknownFieldError,
6+
GitHubProjectUnknownFieldOptionError,
7+
} from "./index";
48

59
export function smokeTest() {
610
expectType<typeof GitHubProject>(GitHubProject);
@@ -641,3 +645,46 @@ export async function testGetProperties() {
641645
url: string;
642646
}>(properties);
643647
}
648+
649+
export function testGitHubProjectError() {
650+
const error = new GitHubProjectError();
651+
652+
// setting type for GitHubProjectError.name is causing a type error, see comment in index.d.ts
653+
// expectType<"GitHubProjectError">(error.name);
654+
expectType<{}>(error.details);
655+
expectType<string>(error.toHumanMessage());
656+
}
657+
658+
export function testGitHubProjectUnknownFieldError() {
659+
const details = {
660+
projectFieldNames: ["one", "two"],
661+
userFieldName: "Three",
662+
userInternalFieldName: "three",
663+
};
664+
const error = new GitHubProjectUnknownFieldError(details);
665+
666+
expectType<"GitHubProjectUnknownFieldError">(error.name);
667+
expectType<typeof details>(error.details);
668+
expectType<string>(error.toHumanMessage());
669+
}
670+
671+
export function testGitHubProjectUnknownFieldOptionError() {
672+
const details = {
673+
field: {
674+
id: "field id",
675+
name: "field name",
676+
options: [
677+
{
678+
id: "option id",
679+
name: "option name",
680+
},
681+
],
682+
},
683+
userValue: "user value",
684+
};
685+
const error = new GitHubProjectUnknownFieldOptionError(details);
686+
687+
expectType<"GitHubProjectUnknownFieldOptionError">(error.name);
688+
expectType<typeof details>(error.details);
689+
expectType<string>(error.toHumanMessage());
690+
}

test/recorded/api.getProperties-field-not-found/test.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export function test(defaultTestProject) {
1818
() => {
1919
throw new Error("Should not resolve");
2020
},
21-
(error) => error
21+
(error) => ({
22+
error,
23+
humanMessage: error.toHumanMessage(),
24+
})
2225
);
2326
}

test/recorded/api.items.update-built-in-read-only-field/test.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export function test(defaultTestProject, itemId = "PVTI_1") {
2020
() => {
2121
throw new Error("Should not resolve");
2222
},
23-
(error) => error
23+
(error) => ({
24+
error,
25+
humanMessage: error.toHumanMessage(),
26+
})
2427
);
2528
}

test/recorded/api.items.update-with-invalid-field-option/test.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export function test(project, itemId = "PVTI_1") {
99
() => {
1010
throw new Error("Expected error");
1111
},
12-
(error) => error
12+
(error) => ({
13+
error,
14+
humanMessage: error.toHumanMessage(),
15+
})
1316
);
1417
}

0 commit comments

Comments
 (0)