Skip to content

Commit 37e0e24

Browse files
Making project_field and project_field_value optional (#15)
* Making project_field and project_field_value optional If a rule without project_field exists, the issue will created without a status. If project_field is provided, project_field_value would be mandatory too * better names * remove unneeded async/await Co-authored-by: joao-paulo-parity <[email protected]>
1 parent 55c46a5 commit 37e0e24

File tree

3 files changed

+163
-77
lines changed

3 files changed

+163
-77
lines changed

src/core.ts

Lines changed: 126 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,27 @@ import Joi from "joi"
44

55
import { Issue } from "./types"
66

7-
export const syncIssue = async ({
8-
issue,
9-
graphql: gql,
10-
project,
11-
organization,
12-
}: {
13-
issue: Issue
14-
graphql: typeof OctokitGraphQL
15-
project: { number: number; targetField: string; targetValue: string }
16-
organization: string
17-
}) => {
18-
/*
19-
20-
Step: Fetch the project's data so that we'll be able to create an item on it
21-
for the issue given as input
22-
23-
*/
24-
const projectData = await gql<{
25-
organization: {
26-
projectNext: {
27-
id: string
28-
fields: {
29-
nodes: {
30-
id: string
31-
name: string
32-
settings?: string | null
33-
}[]
34-
}
7+
type ProjectData = {
8+
organization: {
9+
projectNext: {
10+
id: string
11+
fields: {
12+
nodes: {
13+
id: string
14+
name: string
15+
settings?: string | null
16+
}[]
3517
}
3618
}
37-
}>(
19+
}
20+
}
21+
22+
const fetchProjectData: (params: {
23+
gql: typeof OctokitGraphQL
24+
organization: string
25+
number: number
26+
}) => Promise<ProjectData> = ({ gql, organization, number }) => {
27+
return gql<ProjectData>(
3828
`
3929
query($organization: String!, $number: Int!) {
4030
organization(login: $organization){
@@ -51,29 +41,34 @@ export const syncIssue = async ({
5141
}
5242
}
5343
`,
54-
{ organization, number: project.number },
44+
{ organization, number },
5545
)
46+
}
5647

57-
/*
58-
59-
Step: Find the target field which we were given as input
48+
const resolveProjectTargetField: (params: {
49+
projectData: ProjectData
50+
targetField: string
51+
targetValue: string
52+
}) => { targetFieldId: string; targetValueId: string } = ({
53+
projectData,
54+
targetField,
55+
targetValue,
56+
}) => {
57+
const targetFieldNode =
58+
projectData.organization.projectNext.fields.nodes.find(
59+
({ name }) => name === targetField,
60+
)
6061

61-
*/
62-
const targetField = projectData.organization.projectNext.fields.nodes.find(
63-
({ name }) => {
64-
return name === project.targetField
65-
},
66-
)
6762
assert(
68-
targetField,
69-
`No field named "${project.targetField}" was found in the project`,
63+
targetFieldNode,
64+
`No field named "${targetField}" was found in the project`,
7065
)
7166

7267
assert(
73-
targetField.settings,
74-
`Settings for the field "${project.targetField}" are empty`,
68+
targetFieldNode.settings,
69+
`Settings for the field "${targetField}" are empty`,
7570
)
76-
const parsedSettings = JSON.parse(targetField.settings)
71+
const parsedSettings = JSON.parse(targetFieldNode.settings)
7772

7873
const settingsValidator = Joi.object<{
7974
options: [{ id: string; name: string }]
@@ -91,28 +86,34 @@ export const syncIssue = async ({
9186
assert(settingsResult.error === undefined, settingsResult.error)
9287
const { value: settings } = settingsResult
9388

94-
/*
95-
96-
Step: Find the target value given as input in the possible values for the
97-
target field
98-
99-
*/
100-
const targetValue = settings.options.find(({ name }) => {
101-
return name === project.targetValue
89+
const targetValueNode = settings.options.find(({ name }) => {
90+
return name === targetValue
10291
})
10392
assert(
104-
targetValue,
105-
`No value named "${project.targetValue}" was found for the "${project.targetField}" field`,
93+
targetValueNode,
94+
`No value named "${targetValue}" was found for the "${targetField}" field`,
10695
)
10796

108-
/*
97+
return {
98+
targetFieldId: targetFieldNode.id,
99+
targetValueId: targetValueNode.id,
100+
}
101+
}
109102

110-
Step: Create an item for the issue in the board
103+
type CreatedProjectItemForIssue = {
104+
addProjectNextItem: { projectNextItem: { id: string } }
105+
}
111106

112-
*/
113-
const createItemResult = await gql<{
114-
addProjectNextItem: { projectNextItem: { id: string } }
115-
}>(
107+
const createProjectItemForIssue: (params: {
108+
gql: typeof OctokitGraphQL
109+
projectId: string
110+
issueNodeId: string
111+
}) => Promise<CreatedProjectItemForIssue> = ({
112+
gql,
113+
projectId,
114+
issueNodeId,
115+
}) => {
116+
return gql<CreatedProjectItemForIssue>(
116117
`
117118
mutation($project: ID!, $issue: ID!) {
118119
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
@@ -122,17 +123,24 @@ export const syncIssue = async ({
122123
}
123124
}
124125
`,
125-
{ project: projectData.organization.projectNext.id, issue: issue.nodeId },
126+
{ project: projectId, issue: issueNodeId },
126127
)
128+
}
127129

128-
/*
129-
130-
Step: Assign the issue to the target field and value we were given as input
131-
Apparently it's not (yet?) possible to provide this assignment in the
132-
initial mutation, hence why two separate requests are made.
133-
134-
*/
135-
await gql(
130+
const updateProjectNextItemField: (params: {
131+
gql: typeof OctokitGraphQL
132+
project: string
133+
item: string
134+
targetField: string
135+
targetFieldValue: string
136+
}) => Promise<void> = ({
137+
gql,
138+
project,
139+
item,
140+
targetField,
141+
targetFieldValue,
142+
}) => {
143+
return gql(
136144
`
137145
mutation (
138146
$project: ID!
@@ -152,11 +160,54 @@ export const syncIssue = async ({
152160
}
153161
}
154162
`,
155-
{
156-
project: projectData.organization.projectNext.id,
157-
item: createItemResult.addProjectNextItem.projectNextItem.id,
158-
targetField: targetField.id,
159-
targetFieldValue: targetValue.id,
160-
},
163+
{ project, item, targetField, targetFieldValue },
161164
)
162165
}
166+
167+
export const syncIssue = async ({
168+
issue,
169+
graphql: gql,
170+
project,
171+
organization,
172+
}: {
173+
issue: Issue
174+
graphql: typeof OctokitGraphQL
175+
project: { number: number; targetField?: string; targetValue?: string }
176+
organization: string
177+
}) => {
178+
const projectData = await fetchProjectData({
179+
gql,
180+
organization,
181+
number: project.number,
182+
})
183+
184+
const targetField =
185+
project.targetField && project.targetValue
186+
? resolveProjectTargetField({
187+
projectData,
188+
targetField: project.targetField,
189+
targetValue: project.targetValue,
190+
})
191+
: null
192+
193+
const createdProjectItemForIssue = await createProjectItemForIssue({
194+
gql,
195+
projectId: projectData.organization.projectNext.id,
196+
issueNodeId: issue.nodeId,
197+
})
198+
199+
if (targetField !== null) {
200+
/*
201+
Assign the issue to the target field and value we were given as input
202+
Apparently it's not (yet?) possible to provide this assignment in the
203+
initial mutation, hence why two separate requests are made.
204+
*/
205+
await updateProjectNextItemField({
206+
gql,
207+
project: projectData.organization.projectNext.id,
208+
item: createdProjectItemForIssue.addProjectNextItem.projectNextItem.id,
209+
targetField: targetField.targetFieldId,
210+
targetFieldValue: targetField.targetValueId,
211+
})
212+
}
213+
}

src/server/api.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Context, IssueToProjectFieldRule } from "./types"
1212
enum ApiVersion {
1313
v1 = "v1",
1414
}
15+
1516
const getApiRoute = (version: ApiVersion, route: string) => {
1617
return `/api/${version}/${route}`
1718
}
@@ -286,10 +287,15 @@ export const setupApi = (
286287
>
287288
>().keys({
288289
project_number: Joi.number().required(),
289-
project_field: Joi.string().required(),
290-
project_field_value: Joi.string().required(),
290+
project_field: Joi.string(),
291+
project_field_value: Joi.string().when("project_field", {
292+
is: Joi.exist(),
293+
then: Joi.required(),
294+
otherwise: Joi.forbidden(),
295+
}),
291296
filter: Joi.string(),
292297
})
298+
293299
setupRoute(
294300
"post",
295301
ApiVersion.v1,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { MigrationBuilder } from "node-pg-migrate"
2+
3+
const issueToProjectFieldRuleTable = "issue_to_project_field_rule"
4+
const projectField = "project_field"
5+
const projectFieldValue = "project_field_value"
6+
7+
export const up = async (pgm: MigrationBuilder) => {
8+
pgm.alterColumn(issueToProjectFieldRuleTable, projectField, {
9+
allowNull: true,
10+
})
11+
pgm.alterColumn(issueToProjectFieldRuleTable, projectFieldValue, {
12+
allowNull: true,
13+
})
14+
}
15+
16+
export const down = async (pgm: MigrationBuilder) => {
17+
pgm.sql(`
18+
DELETE FROM ${issueToProjectFieldRuleTable}
19+
WHERE
20+
${projectField} IS NULL
21+
OR ${projectFieldValue} IS NULL
22+
`)
23+
pgm.alterColumn(issueToProjectFieldRuleTable, projectField, {
24+
allowNull: false,
25+
})
26+
pgm.alterColumn(issueToProjectFieldRuleTable, projectFieldValue, {
27+
allowNull: false,
28+
})
29+
}

0 commit comments

Comments
 (0)