Skip to content

Commit cd206a2

Browse files
committed
fix(zapier): align issue payloads and error handling
1 parent a236911 commit cd206a2

7 files changed

Lines changed: 258 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 4.15.0
2+
3+
- Added team details to issue trigger test data.
4+
- Added GraphQL error handling to the "Find Issues by Name" search action.
5+
16
## 4.14.0
27

38
- Added attachments (with metadata) to the issue searches and the issue trigger.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "linear-zapier",
3-
"version": "4.14.0",
3+
"version": "4.15.0",
44
"description": "Linear's Zapier integration",
55
"main": "index.js",
66
"license": "MIT",

src/samples/issueInstant.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"id": "044a462c-1acd-4092-8e66-51e4d7c9c7ca",
3+
"identifier": "SAMPLE-15",
4+
"url": "https://linear.app/example/issue/SAMPLE-15",
5+
"title": "Zapier sample",
6+
"description": "More information about the issue",
7+
"priority": 0,
8+
"estimate": null,
9+
"dueDate": null,
10+
"slaBreachesAt": null,
11+
"slaStartedAt": null,
12+
"createdAt": "2022-10-27T21:20:59.199Z",
13+
"updatedAt": "2022-10-27T21:20:59.199Z",
14+
"teamId": "2b03b41f-4b55-4779-bd22-4786eb76e11f",
15+
"team": {
16+
"id": "2b03b41f-4b55-4779-bd22-4786eb76e11f",
17+
"key": "SAMPLE",
18+
"name": "Sample Team"
19+
},
20+
"project": {
21+
"id": "04a48f13-c39f-4522-85bb-a17225cbfdf3",
22+
"name": "My Project"
23+
},
24+
"projectMilestone": {
25+
"id": "84cb5a2f-4b84-4e90-a06a-d0b38f9ac2dd",
26+
"name": "V1"
27+
},
28+
"creator": {
29+
"id": "45920bc2-c4be-434f-b588-f422155667b5",
30+
"name": "Zapier User",
31+
"email": "creator@example.com"
32+
},
33+
"assignee": {
34+
"id": "8a225115-706e-4389-87da-e687b9b5ae76",
35+
"name": "Zapier User 2",
36+
"email": "assignee@example.com"
37+
},
38+
"status": {
39+
"id": "e6d2eb84-b03b-4827-9230-94b0a1099419",
40+
"name": "In Progress",
41+
"type": "started"
42+
},
43+
"parent": {
44+
"id": "e6d2eb84-b03b-4827-9230-94b0a1099419",
45+
"identifier": "SAMPLE-10",
46+
"url": "https://linear.app/example/issue/SAMPLE-10",
47+
"title": "Zapier parent sample"
48+
},
49+
"addedLabels": [],
50+
"attachments": {
51+
"nodes": [
52+
{
53+
"id": "2d08eab7-8f8c-44e4-b3cd-4a5e6297fcc4",
54+
"title": "Question #1174",
55+
"subtitle": "Testing screenshots",
56+
"url": "https://example.zendesk.com/tickets/1174",
57+
"source": { "type": "zendesk" },
58+
"sourceType": "zendesk",
59+
"metadata": {
60+
"id": 1174,
61+
"title": "Testing screenshots",
62+
"priority": "normal",
63+
"attributes": [
64+
{ "name": "Type", "value": "Question" },
65+
{ "name": "Priority", "value": "normal" }
66+
]
67+
}
68+
}
69+
]
70+
}
71+
}

src/searches/issueByName.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ interface IssueByNameResponse {
1414
issues: {
1515
nodes: IssueByNameApi[];
1616
};
17-
};
17+
} | null;
18+
errors?: {
19+
message: string;
20+
extensions?: {
21+
userPresentableMessage?: string;
22+
};
23+
}[];
1824
}
1925

2026
interface IssueByNameApi extends IssueCommon {
@@ -130,8 +136,18 @@ const getIssuesByName = async (z: ZObject, bundle: Bundle<IssueByNameInput>) =>
130136
});
131137

132138
const response = await fetchFromLinear(z, bundle, query, variables);
133-
const data = (response.json as IssueByNameResponse).data;
134-
return data.issues.nodes;
139+
const body = response.json as IssueByNameResponse;
140+
const firstError = body.errors?.[0];
141+
142+
if (firstError) {
143+
throw new z.errors.Error(
144+
firstError.extensions?.userPresentableMessage ?? firstError.message,
145+
"request_execution_failed",
146+
400
147+
);
148+
}
149+
150+
return body.data?.issues.nodes ?? [];
135151
};
136152

137153
export const findIssueByName = {

src/test/searches/issueByName.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,62 @@ describe("searches.issue_by_name", () => {
8686
});
8787
});
8888

89+
it("returns an empty array when Linear returns no issue data", async () => {
90+
const requestMock = jest.fn().mockResolvedValue({
91+
json: { data: null },
92+
});
93+
94+
const results = await appTester(
95+
(z, bundle) => {
96+
z.request = requestMock;
97+
return perform(z, bundle);
98+
},
99+
{
100+
authData: {
101+
api_key: "test-linear-api-key",
102+
},
103+
inputData: {
104+
name: "Setup SSO",
105+
},
106+
}
107+
);
108+
109+
expect(results).toEqual([]);
110+
});
111+
112+
it("throws the user-presentable message for GraphQL errors", async () => {
113+
const requestMock = jest.fn().mockResolvedValue({
114+
json: {
115+
data: null,
116+
errors: [
117+
{
118+
message: "Internal server error",
119+
extensions: {
120+
userPresentableMessage: "Something went wrong on our end.",
121+
},
122+
},
123+
],
124+
},
125+
});
126+
127+
await expect(
128+
appTester(
129+
(z, bundle) => {
130+
z.request = requestMock;
131+
return perform(z, bundle);
132+
},
133+
{
134+
authData: {
135+
api_key: "test-linear-api-key",
136+
},
137+
inputData: {
138+
name: "Setup SSO",
139+
},
140+
}
141+
)
142+
).rejects.toThrow("Something went wrong on our end.");
143+
});
144+
89145
it("adds teamId to query variables when teamId is provided", async () => {
90146
const requestMock = jest.fn().mockResolvedValue({
91147
json: issuesByNameResponse,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const zapier = require("zapier-platform-core");
2+
const App = require("../../../index");
3+
4+
const appTester = zapier.createAppTester(App);
5+
const newIssueInstant = App.triggers.newIssueInstant;
6+
const newIssueLegacy = App.triggers.newIssue;
7+
8+
describe("triggers.newIssueInstant", () => {
9+
it("uses a team sample without changing the legacy sample", () => {
10+
expect(newIssueInstant.operation.sample).toEqual(
11+
expect.objectContaining({
12+
teamId: "2b03b41f-4b55-4779-bd22-4786eb76e11f",
13+
team: {
14+
id: "2b03b41f-4b55-4779-bd22-4786eb76e11f",
15+
key: "SAMPLE",
16+
name: "Sample Team",
17+
},
18+
})
19+
);
20+
expect(newIssueInstant.operation.sample.attachments).toEqual(newIssueLegacy.operation.sample.attachments);
21+
expect(newIssueLegacy.operation.sample).not.toHaveProperty("teamId");
22+
expect(newIssueLegacy.operation.sample).not.toHaveProperty("team");
23+
});
24+
25+
it("includes the team in polling test data", async () => {
26+
const requestMock = jest.fn().mockResolvedValue({
27+
json: {
28+
data: {
29+
team: {
30+
id: "team-id",
31+
key: "ENG",
32+
name: "Engineering",
33+
issues: {
34+
nodes: [
35+
{
36+
id: "issue-id",
37+
identifier: "ENG-123",
38+
url: "https://linear.app/example/issue/ENG-123",
39+
title: "Test issue",
40+
description: "Test description",
41+
priority: 0,
42+
createdAt: "2026-07-31T00:00:00.000Z",
43+
updatedAt: "2026-07-31T00:00:00.000Z",
44+
creator: {
45+
id: "creator-id",
46+
name: "Creator",
47+
email: "creator@example.com",
48+
},
49+
state: {
50+
id: "state-id",
51+
name: "Backlog",
52+
type: "backlog",
53+
},
54+
labels: { nodes: [] },
55+
},
56+
],
57+
},
58+
},
59+
},
60+
},
61+
});
62+
63+
const [issue] = await appTester(
64+
(z, bundle) => {
65+
z.request = requestMock;
66+
return newIssueInstant.operation.performList(z, bundle);
67+
},
68+
{
69+
authData: { api_key: "test-linear-api-key" },
70+
inputData: { teamId: "team-id" },
71+
}
72+
);
73+
74+
const compactQuery = requestMock.mock.calls[0][0].body.query.replace(/\s/g, "");
75+
expect(compactQuery).toContain("team(id:$teamId){idkeynameissues");
76+
expect(issue).toEqual(
77+
expect.objectContaining({
78+
teamId: "team-id",
79+
team: {
80+
id: "team-id",
81+
key: "ENG",
82+
name: "Engineering",
83+
},
84+
})
85+
);
86+
});
87+
});

src/triggers/issue.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { omitBy, pick } from "lodash";
22
import { ZObject, Bundle } from "zapier-platform-core";
3-
import sample from "../samples/issue.json";
3+
import sample from "../samples/issueInstant.json";
44
import { unsubscribeHook } from "../handleWebhook";
55
import { jsonToGraphQLQuery, VariableType } from "json-to-graphql-query";
66
import { fetchFromLinear, LinearGraphQLVariables } from "../fetchFromLinear";
@@ -60,6 +60,12 @@ export interface IssueCommon {
6060
};
6161
}
6262

63+
interface IssueTeam {
64+
id: string;
65+
key: string;
66+
name: string;
67+
}
68+
6369
interface IssueApi extends IssueCommon {
6470
labels?: {
6571
nodes: {
@@ -74,6 +80,8 @@ interface IssueApi extends IssueCommon {
7480
}
7581

7682
interface IssueWebhook extends IssueCommon {
83+
teamId: string;
84+
team: IssueTeam;
7785
labels?: {
7886
id: string;
7987
color: string;
@@ -84,7 +92,7 @@ interface IssueWebhook extends IssueCommon {
8492

8593
interface TeamIssuesResponse {
8694
data: {
87-
team: {
95+
team: IssueTeam & {
8896
issues: {
8997
nodes: IssueApi[];
9098
};
@@ -189,6 +197,9 @@ const getIssueList =
189197
__args: {
190198
id: new VariableType("teamId"),
191199
},
200+
id: true,
201+
key: true,
202+
name: true,
192203
issues: {
193204
__args: {
194205
first: 10,
@@ -269,6 +280,12 @@ const getIssueList =
269280
// We need to map the API schema to the webhook schema
270281
return issuesRaw.map((issueRaw) => ({
271282
...issueRaw,
283+
teamId: data.team.id,
284+
team: {
285+
id: data.team.id,
286+
key: data.team.key,
287+
name: data.team.name,
288+
},
272289
labels: issueRaw.labels?.nodes.map((label) => ({
273290
id: label.id,
274291
color: label.color,

0 commit comments

Comments
 (0)