@enrichlayer/el-linear 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/claude-skills/linear-operations/SKILL.md +315 -0
- package/claude-skills/linear-operations/evals/evals.json +46 -0
- package/dist/commands/attachments.d.ts +2 -0
- package/dist/commands/attachments.js +57 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +309 -0
- package/dist/commands/comments.d.ts +2 -0
- package/dist/commands/comments.js +272 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +15 -0
- package/dist/commands/cycles.d.ts +2 -0
- package/dist/commands/cycles.js +63 -0
- package/dist/commands/documents.d.ts +2 -0
- package/dist/commands/documents.js +175 -0
- package/dist/commands/embeds.d.ts +2 -0
- package/dist/commands/embeds.js +65 -0
- package/dist/commands/gdoc.d.ts +2 -0
- package/dist/commands/gdoc.js +37 -0
- package/dist/commands/graphql.d.ts +2 -0
- package/dist/commands/graphql.js +70 -0
- package/dist/commands/init/aliases.d.ts +109 -0
- package/dist/commands/init/aliases.js +569 -0
- package/dist/commands/init/defaults.d.ts +25 -0
- package/dist/commands/init/defaults.js +112 -0
- package/dist/commands/init/index.d.ts +18 -0
- package/dist/commands/init/index.js +182 -0
- package/dist/commands/init/shared.d.ts +88 -0
- package/dist/commands/init/shared.js +164 -0
- package/dist/commands/init/token.d.ts +50 -0
- package/dist/commands/init/token.js +141 -0
- package/dist/commands/init/workspace.d.ts +20 -0
- package/dist/commands/init/workspace.js +80 -0
- package/dist/commands/issue-id.d.ts +28 -0
- package/dist/commands/issue-id.js +81 -0
- package/dist/commands/issues.d.ts +2 -0
- package/dist/commands/issues.js +1145 -0
- package/dist/commands/labels.d.ts +2 -0
- package/dist/commands/labels.js +100 -0
- package/dist/commands/project-milestones.d.ts +2 -0
- package/dist/commands/project-milestones.js +143 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +336 -0
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +69 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +142 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +171 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +19 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.js +58 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +17 -0
- package/dist/config/config.d.ts +43 -0
- package/dist/config/config.js +81 -0
- package/dist/config/issue-validation.d.ts +39 -0
- package/dist/config/issue-validation.js +264 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/resolver.d.ts +25 -0
- package/dist/config/resolver.js +183 -0
- package/dist/config/status-defaults.d.ts +13 -0
- package/dist/config/status-defaults.js +20 -0
- package/dist/config/term-enforcer.d.ts +31 -0
- package/dist/config/term-enforcer.js +69 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +76 -0
- package/dist/queries/attachments.d.ts +3 -0
- package/dist/queries/attachments.js +35 -0
- package/dist/queries/comments.d.ts +3 -0
- package/dist/queries/comments.js +64 -0
- package/dist/queries/common.d.ts +2 -0
- package/dist/queries/common.js +109 -0
- package/dist/queries/cycles.d.ts +2 -0
- package/dist/queries/cycles.js +40 -0
- package/dist/queries/documents.d.ts +5 -0
- package/dist/queries/documents.js +67 -0
- package/dist/queries/introspect.d.ts +2 -0
- package/dist/queries/introspect.js +24 -0
- package/dist/queries/issues.d.ts +23 -0
- package/dist/queries/issues.js +380 -0
- package/dist/queries/labels.d.ts +4 -0
- package/dist/queries/labels.js +53 -0
- package/dist/queries/project-milestones.d.ts +6 -0
- package/dist/queries/project-milestones.js +125 -0
- package/dist/queries/projects.d.ts +7 -0
- package/dist/queries/projects.js +104 -0
- package/dist/queries/releases.d.ts +4 -0
- package/dist/queries/releases.js +85 -0
- package/dist/queries/search.d.ts +1 -0
- package/dist/queries/search.js +35 -0
- package/dist/queries/templates.d.ts +2 -0
- package/dist/queries/templates.js +30 -0
- package/dist/types/linear.d.ts +217 -0
- package/dist/types/linear.js +6 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.js +23 -0
- package/dist/utils/auto-link-references.d.ts +47 -0
- package/dist/utils/auto-link-references.js +188 -0
- package/dist/utils/date-format.d.ts +4 -0
- package/dist/utils/date-format.js +8 -0
- package/dist/utils/download-uploads.d.ts +7 -0
- package/dist/utils/download-uploads.js +88 -0
- package/dist/utils/embed-parser.d.ts +8 -0
- package/dist/utils/embed-parser.js +54 -0
- package/dist/utils/error-messages.d.ts +4 -0
- package/dist/utils/error-messages.js +17 -0
- package/dist/utils/file-service.d.ts +13 -0
- package/dist/utils/file-service.js +239 -0
- package/dist/utils/gdoc-parser.d.ts +45 -0
- package/dist/utils/gdoc-parser.js +107 -0
- package/dist/utils/graphql-attachments-service.d.ts +12 -0
- package/dist/utils/graphql-attachments-service.js +46 -0
- package/dist/utils/graphql-documents-service.d.ts +18 -0
- package/dist/utils/graphql-documents-service.js +97 -0
- package/dist/utils/graphql-issues-service.d.ts +49 -0
- package/dist/utils/graphql-issues-service.js +925 -0
- package/dist/utils/graphql-service.d.ts +8 -0
- package/dist/utils/graphql-service.js +35 -0
- package/dist/utils/identifier-parser.d.ts +7 -0
- package/dist/utils/identifier-parser.js +20 -0
- package/dist/utils/issue-reference-extractor.d.ts +18 -0
- package/dist/utils/issue-reference-extractor.js +95 -0
- package/dist/utils/issue-reference-wrapper.d.ts +12 -0
- package/dist/utils/issue-reference-wrapper.js +91 -0
- package/dist/utils/linear-service.d.ts +26 -0
- package/dist/utils/linear-service.js +442 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/markdown-prosemirror.d.ts +24 -0
- package/dist/utils/markdown-prosemirror.js +325 -0
- package/dist/utils/mention-resolver.d.ts +31 -0
- package/dist/utils/mention-resolver.js +234 -0
- package/dist/utils/output.d.ts +7 -0
- package/dist/utils/output.js +125 -0
- package/dist/utils/table-formatter.d.ts +4 -0
- package/dist/utils/table-formatter.js +249 -0
- package/dist/utils/usage.d.ts +2 -0
- package/dist/utils/usage.js +24 -0
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.js +8 -0
- package/dist/utils/validate-references.d.ts +10 -0
- package/dist/utils/validate-references.js +33 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +71 -0
- package/dist/utils/workspace-url.d.ts +4 -0
- package/dist/utils/workspace-url.js +44 -0
- package/package.json +71 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
import { resolveUserDisplayName } from "../config/resolver.js";
|
|
2
|
+
import { BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
3
|
+
import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
|
|
4
|
+
import { toISOStringOrNow } from "./date-format.js";
|
|
5
|
+
import { extractEmbeds } from "./embed-parser.js";
|
|
6
|
+
import { notFoundError } from "./error-messages.js";
|
|
7
|
+
import { parseIssueIdentifier, tryParseIssueIdentifier, } from "./identifier-parser.js";
|
|
8
|
+
import { isUuid } from "./uuid.js";
|
|
9
|
+
const TEAM_KEY_REGEX = /^[A-Z0-9]+$/i;
|
|
10
|
+
function extractSummaryText(summary) {
|
|
11
|
+
if (summary.generationStatus !== "completed" || !summary.content) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const parts = [];
|
|
15
|
+
const walk = (node) => {
|
|
16
|
+
if (node.text) {
|
|
17
|
+
parts.push(node.text);
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(node.content)) {
|
|
20
|
+
for (const child of node.content) {
|
|
21
|
+
walk(child);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
walk(summary.content);
|
|
26
|
+
return parts.length > 0 ? parts.join("") : undefined;
|
|
27
|
+
}
|
|
28
|
+
export class GraphQLIssuesService {
|
|
29
|
+
graphQLService;
|
|
30
|
+
linearService;
|
|
31
|
+
constructor(graphQLService, linearService) {
|
|
32
|
+
this.graphQLService = graphQLService;
|
|
33
|
+
this.linearService = linearService;
|
|
34
|
+
}
|
|
35
|
+
async getIssues(limit = 25) {
|
|
36
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, {
|
|
37
|
+
first: limit,
|
|
38
|
+
orderBy: "updatedAt",
|
|
39
|
+
});
|
|
40
|
+
const issues = result.issues;
|
|
41
|
+
if (!issues?.nodes) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return issues.nodes.map((issue) => this.transformIssueData(issue));
|
|
45
|
+
}
|
|
46
|
+
async getIssueById(issueId) {
|
|
47
|
+
let issueData;
|
|
48
|
+
if (isUuid(issueId)) {
|
|
49
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_ID_QUERY, { id: issueId });
|
|
50
|
+
if (!result.issue) {
|
|
51
|
+
throw notFoundError("Issue", issueId);
|
|
52
|
+
}
|
|
53
|
+
issueData = result.issue;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
|
|
57
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, {
|
|
58
|
+
teamKey,
|
|
59
|
+
number: issueNumber,
|
|
60
|
+
});
|
|
61
|
+
const issues = result.issues;
|
|
62
|
+
const nodes = issues?.nodes;
|
|
63
|
+
if (!nodes?.length) {
|
|
64
|
+
throw notFoundError("Issue", issueId);
|
|
65
|
+
}
|
|
66
|
+
issueData = nodes[0];
|
|
67
|
+
}
|
|
68
|
+
return this.transformIssueData(issueData);
|
|
69
|
+
}
|
|
70
|
+
async updateIssue(args, labelMode = "overwriting") {
|
|
71
|
+
const { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult } = await this.resolveUpdateContext(args);
|
|
72
|
+
let teamIdForLabels = issueTeamId;
|
|
73
|
+
if (!teamIdForLabels && args.labelIds && resolvedIssueId) {
|
|
74
|
+
teamIdForLabels = await this.fetchIssueTeamId(resolvedIssueId);
|
|
75
|
+
}
|
|
76
|
+
const finalLabelIds = await this.resolveLabelsWithMode(args.labelIds, resolveResult, labelMode, currentIssueLabels, teamIdForLabels, args.teamId);
|
|
77
|
+
const finalProjectId = args.projectId
|
|
78
|
+
? this.resolveProjectId(args.projectId, resolveResult)
|
|
79
|
+
: args.projectId;
|
|
80
|
+
const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(args, resolveResult);
|
|
81
|
+
const finalMilestoneId = this.resolveMilestoneId(args.milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes);
|
|
82
|
+
const finalCycleId = await this.resolveCycleIdForUpdate(args, resolveResult, resolvedIssueId);
|
|
83
|
+
const resolvedStatusId = await this.resolveStatusIdForUpdate(args, resolvedIssueId);
|
|
84
|
+
if (args.assigneeId && !isUuid(args.assigneeId)) {
|
|
85
|
+
args.assigneeId = await this.linearService.resolveUserId(args.assigneeId);
|
|
86
|
+
}
|
|
87
|
+
const updateInput = this.buildUpdateInput(args, {
|
|
88
|
+
statusId: resolvedStatusId,
|
|
89
|
+
projectId: finalProjectId,
|
|
90
|
+
labelIds: finalLabelIds,
|
|
91
|
+
milestoneId: finalMilestoneId,
|
|
92
|
+
cycleId: finalCycleId,
|
|
93
|
+
});
|
|
94
|
+
return this.executeUpdateMutation(resolvedIssueId, args.id, updateInput);
|
|
95
|
+
}
|
|
96
|
+
extractMilestoneNodes(args, resolveResult) {
|
|
97
|
+
const projectNodes = resolveResult.projects?.nodes;
|
|
98
|
+
const projectMilestoneNodes = args.projectId
|
|
99
|
+
? projectNodes?.[0]?.projectMilestones?.nodes
|
|
100
|
+
: undefined;
|
|
101
|
+
const issueNodes = resolveResult.issues
|
|
102
|
+
?.nodes;
|
|
103
|
+
const issueProjectMilestoneNodes = issueNodes?.[0]?.project
|
|
104
|
+
?.projectMilestones?.nodes;
|
|
105
|
+
return { projectMilestoneNodes, issueProjectMilestoneNodes };
|
|
106
|
+
}
|
|
107
|
+
async executeUpdateMutation(resolvedIssueId, originalId, updateInput) {
|
|
108
|
+
let updateResult;
|
|
109
|
+
try {
|
|
110
|
+
updateResult = await this.graphQLService.rawRequest(UPDATE_ISSUE_MUTATION, {
|
|
111
|
+
id: resolvedIssueId,
|
|
112
|
+
input: updateInput,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
117
|
+
if (msg.includes("Discrepancy between issue team")) {
|
|
118
|
+
const hint = updateInput.projectId
|
|
119
|
+
? `The project may not be associated with this issue's team. Fix with: el-linear projects add-team "<project>" <team>`
|
|
120
|
+
: "The issue's team doesn't match the assigned project, cycle, or status.";
|
|
121
|
+
throw new Error(`Failed to update issue ${originalId}: ${hint}`);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Failed to update issue ${originalId}: ${msg}`);
|
|
124
|
+
}
|
|
125
|
+
const issueUpdate = updateResult.issueUpdate;
|
|
126
|
+
if (!issueUpdate.success) {
|
|
127
|
+
throw new Error(`Failed to update issue ${originalId}`);
|
|
128
|
+
}
|
|
129
|
+
if (!issueUpdate.issue) {
|
|
130
|
+
throw new Error("Failed to retrieve updated issue");
|
|
131
|
+
}
|
|
132
|
+
return this.transformIssueData(issueUpdate.issue);
|
|
133
|
+
}
|
|
134
|
+
async createIssue(args) {
|
|
135
|
+
const resolveVariables = this.buildCreateResolveVariables(args);
|
|
136
|
+
let resolveResult = {};
|
|
137
|
+
if (Object.keys(resolveVariables).length > 0) {
|
|
138
|
+
const { __labelNames, ...queryVars } = resolveVariables;
|
|
139
|
+
const requests = [
|
|
140
|
+
this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars),
|
|
141
|
+
];
|
|
142
|
+
if (__labelNames) {
|
|
143
|
+
const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
|
|
144
|
+
requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
|
|
145
|
+
}
|
|
146
|
+
const results = await Promise.all(requests);
|
|
147
|
+
resolveResult = results[0];
|
|
148
|
+
if (results[1]) {
|
|
149
|
+
resolveResult.labels = results[1].labels;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const resolved = await this.resolveCreateFields(args, resolveResult);
|
|
153
|
+
if (args.assigneeId && !isUuid(args.assigneeId)) {
|
|
154
|
+
args.assigneeId = await this.linearService.resolveUserId(args.assigneeId);
|
|
155
|
+
}
|
|
156
|
+
const createInput = this.buildCreateInput(args, resolved);
|
|
157
|
+
return this.executeCreateMutation(createInput);
|
|
158
|
+
}
|
|
159
|
+
async resolveCreateFields(args, resolveResult) {
|
|
160
|
+
let teamId = args.teamId
|
|
161
|
+
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
162
|
+
: args.teamId;
|
|
163
|
+
const projectId = args.projectId
|
|
164
|
+
? this.resolveProjectId(args.projectId, resolveResult)
|
|
165
|
+
: args.projectId;
|
|
166
|
+
// Validate team-project compatibility and auto-correct when possible
|
|
167
|
+
if (projectId && teamId) {
|
|
168
|
+
teamId = this.validateProjectTeam(teamId, args.projectId, resolveResult);
|
|
169
|
+
}
|
|
170
|
+
const labelIds = await this.resolveLabels(args.labelIds, resolveResult, teamId, args.teamInput);
|
|
171
|
+
const parentId = this.resolveParentId(args.parentId, resolveResult);
|
|
172
|
+
const milestoneId = this.resolveMilestoneIdForCreate(args.milestoneId, resolveResult, projectId);
|
|
173
|
+
let cycleId = args.cycleId;
|
|
174
|
+
if (args.cycleId &&
|
|
175
|
+
typeof args.cycleId === "string" &&
|
|
176
|
+
!isUuid(args.cycleId)) {
|
|
177
|
+
cycleId = await this.linearService.resolveCycleId(args.cycleId, teamId);
|
|
178
|
+
}
|
|
179
|
+
let statusId = args.statusId;
|
|
180
|
+
if (args.statusId && !isUuid(args.statusId)) {
|
|
181
|
+
statusId = await this.linearService.resolveStatusId(args.statusId, teamId);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
teamId,
|
|
185
|
+
projectId,
|
|
186
|
+
statusId,
|
|
187
|
+
labelIds,
|
|
188
|
+
parentId,
|
|
189
|
+
milestoneId,
|
|
190
|
+
cycleId,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async executeCreateMutation(createInput) {
|
|
194
|
+
let createResult;
|
|
195
|
+
try {
|
|
196
|
+
createResult = await this.graphQLService.rawRequest(CREATE_ISSUE_MUTATION, {
|
|
197
|
+
input: createInput,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
202
|
+
throw new Error(`Failed to create issue: ${msg}`);
|
|
203
|
+
}
|
|
204
|
+
const issueCreate = createResult.issueCreate;
|
|
205
|
+
if (!issueCreate.success) {
|
|
206
|
+
throw new Error("Failed to create issue: the API reported failure.");
|
|
207
|
+
}
|
|
208
|
+
if (!issueCreate.issue) {
|
|
209
|
+
throw new Error("Failed to retrieve created issue");
|
|
210
|
+
}
|
|
211
|
+
return this.transformIssueData(issueCreate.issue);
|
|
212
|
+
}
|
|
213
|
+
async searchIssues(args) {
|
|
214
|
+
const resolveVariables = {};
|
|
215
|
+
if (args.teamId && !isUuid(args.teamId)) {
|
|
216
|
+
Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
|
|
217
|
+
}
|
|
218
|
+
if (args.projectId && !isUuid(args.projectId)) {
|
|
219
|
+
resolveVariables.projectName = args.projectId;
|
|
220
|
+
}
|
|
221
|
+
if (args.assigneeId &&
|
|
222
|
+
!isUuid(args.assigneeId) &&
|
|
223
|
+
args.assigneeId.includes("@")) {
|
|
224
|
+
resolveVariables.assigneeEmail = args.assigneeId;
|
|
225
|
+
}
|
|
226
|
+
let resolveResult = {};
|
|
227
|
+
if (Object.keys(resolveVariables).length > 0) {
|
|
228
|
+
resolveResult = await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
|
|
229
|
+
}
|
|
230
|
+
const finalTeamId = args.teamId
|
|
231
|
+
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
232
|
+
: args.teamId;
|
|
233
|
+
const finalProjectId = args.projectId
|
|
234
|
+
? this.resolveProjectId(args.projectId, resolveResult)
|
|
235
|
+
: args.projectId;
|
|
236
|
+
const finalAssigneeId = this.resolveAssigneeId(args.assigneeId, resolveResult);
|
|
237
|
+
if (args.query) {
|
|
238
|
+
const searchResult = await this.graphQLService.rawRequest(SEARCH_ISSUES_QUERY, {
|
|
239
|
+
term: args.query,
|
|
240
|
+
first: args.limit || 10,
|
|
241
|
+
});
|
|
242
|
+
const searchIssues = searchResult.searchIssues;
|
|
243
|
+
if (!searchIssues?.nodes) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
const results = searchIssues.nodes.map((issue) => this.transformIssueData(issue));
|
|
247
|
+
return this.applySearchFilters(results, {
|
|
248
|
+
teamId: finalTeamId,
|
|
249
|
+
assigneeId: finalAssigneeId,
|
|
250
|
+
projectId: finalProjectId,
|
|
251
|
+
status: args.status,
|
|
252
|
+
labelNames: args.labelNames,
|
|
253
|
+
priority: args.priority,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const filter = this.buildSearchFilter({
|
|
257
|
+
teamId: finalTeamId,
|
|
258
|
+
assigneeId: finalAssigneeId,
|
|
259
|
+
projectId: finalProjectId,
|
|
260
|
+
noProject: args.noProject,
|
|
261
|
+
status: args.status,
|
|
262
|
+
labelNames: args.labelNames,
|
|
263
|
+
priority: args.priority,
|
|
264
|
+
});
|
|
265
|
+
const searchResult = await this.graphQLService.rawRequest(FILTERED_SEARCH_ISSUES_QUERY, {
|
|
266
|
+
first: args.limit || 10,
|
|
267
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
268
|
+
orderBy: args.orderBy || "updatedAt",
|
|
269
|
+
});
|
|
270
|
+
const filteredIssues = searchResult.issues;
|
|
271
|
+
if (!filteredIssues?.nodes) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
return filteredIssues.nodes.map((issue) => this.transformIssueData(issue));
|
|
275
|
+
}
|
|
276
|
+
// --- Private helper methods for field resolution ---
|
|
277
|
+
async resolveTeamId(teamId, resolveResult) {
|
|
278
|
+
if (isUuid(teamId)) {
|
|
279
|
+
return teamId;
|
|
280
|
+
}
|
|
281
|
+
const teams = resolveResult.teams;
|
|
282
|
+
const teamNodes = teams?.nodes;
|
|
283
|
+
const resolvedTeam = teamNodes?.[0];
|
|
284
|
+
if (resolvedTeam &&
|
|
285
|
+
(resolvedTeam.key.toUpperCase() === teamId.toUpperCase() ||
|
|
286
|
+
resolvedTeam.name.toLowerCase() === teamId.toLowerCase())) {
|
|
287
|
+
return resolvedTeam.id;
|
|
288
|
+
}
|
|
289
|
+
// Exact GraphQL match failed — fall back to prefix matching via LinearService
|
|
290
|
+
return this.linearService.resolveTeamId(teamId);
|
|
291
|
+
}
|
|
292
|
+
resolveProjectId(projectId, resolveResult) {
|
|
293
|
+
if (isUuid(projectId)) {
|
|
294
|
+
return projectId;
|
|
295
|
+
}
|
|
296
|
+
const projects = resolveResult.projects;
|
|
297
|
+
const projectNodes = projects?.nodes;
|
|
298
|
+
if (!projectNodes?.length) {
|
|
299
|
+
throw notFoundError("Project", projectId);
|
|
300
|
+
}
|
|
301
|
+
return projectNodes[0].id;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Check that the resolved team is associated with the project.
|
|
305
|
+
* If not, auto-switch when the project belongs to exactly one team.
|
|
306
|
+
*/
|
|
307
|
+
validateProjectTeam(teamId, projectInput, resolveResult) {
|
|
308
|
+
const projects = resolveResult.projects;
|
|
309
|
+
const projectNode = projects?.nodes?.[0];
|
|
310
|
+
if (!projectNode) {
|
|
311
|
+
return teamId;
|
|
312
|
+
}
|
|
313
|
+
const teamsConn = projectNode.teams;
|
|
314
|
+
const teamNodes = teamsConn?.nodes;
|
|
315
|
+
if (!teamNodes?.length) {
|
|
316
|
+
return teamId;
|
|
317
|
+
}
|
|
318
|
+
const isAssociated = teamNodes.some((t) => t.id === teamId);
|
|
319
|
+
if (isAssociated) {
|
|
320
|
+
return teamId;
|
|
321
|
+
}
|
|
322
|
+
const projectName = projectNode.name || projectInput;
|
|
323
|
+
const teamKeys = teamNodes.map((t) => t.key);
|
|
324
|
+
if (teamNodes.length === 1) {
|
|
325
|
+
const correctTeamId = teamNodes[0].id;
|
|
326
|
+
const correctKey = teamKeys[0];
|
|
327
|
+
process.stderr.write(`Auto-switched team to ${correctKey} (the only team associated with project "${projectName}").\n`);
|
|
328
|
+
return correctTeamId;
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Project "${projectName}" is not associated with the specified team.\n` +
|
|
331
|
+
`Associated teams: ${teamKeys.join(", ")}\n` +
|
|
332
|
+
`Hint: Use --team ${teamKeys[0]} or run \`el-linear projects add-team "${projectName}" TEAM\` to add your team.`);
|
|
333
|
+
}
|
|
334
|
+
async resolveLabels(labelIds, resolveResult, teamId, teamInput) {
|
|
335
|
+
return this.resolveLabelsWithMode(labelIds, resolveResult, "overwriting", [], teamId, teamInput);
|
|
336
|
+
}
|
|
337
|
+
async resolveLabelsWithMode(labelIds, resolveResult, labelMode, currentIssueLabels, teamId, teamInput) {
|
|
338
|
+
if (!(labelIds && Array.isArray(labelIds))) {
|
|
339
|
+
return labelIds;
|
|
340
|
+
}
|
|
341
|
+
const resolvedLabels = [];
|
|
342
|
+
const labels = resolveResult.labels;
|
|
343
|
+
const labelNodes = labels?.nodes;
|
|
344
|
+
for (const labelIdOrName of labelIds) {
|
|
345
|
+
if (isUuid(labelIdOrName)) {
|
|
346
|
+
resolvedLabels.push(labelIdOrName);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const lowerName = labelIdOrName.toLowerCase();
|
|
350
|
+
const candidates = labelNodes?.filter((l) => l.name.toLowerCase() === lowerName);
|
|
351
|
+
let label;
|
|
352
|
+
if (candidates && candidates.length > 1 && teamId) {
|
|
353
|
+
label = candidates.find((l) => {
|
|
354
|
+
const team = l.team;
|
|
355
|
+
return team?.id === teamId;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (!label) {
|
|
359
|
+
label = candidates?.[0];
|
|
360
|
+
}
|
|
361
|
+
if (!label) {
|
|
362
|
+
// Auto-create the missing label on the team
|
|
363
|
+
const created = await this.autoCreateLabel(labelIdOrName, teamId);
|
|
364
|
+
if (created) {
|
|
365
|
+
resolvedLabels.push(created);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const teamKey = this.resolveTeamKeyFromResult(resolveResult, teamId, teamInput);
|
|
369
|
+
const hint = teamKey
|
|
370
|
+
? `— check available labels with: el-linear labels list --team ${teamKey}`
|
|
371
|
+
: undefined;
|
|
372
|
+
throw notFoundError("Label", labelIdOrName, undefined, hint);
|
|
373
|
+
}
|
|
374
|
+
if (label.isGroup) {
|
|
375
|
+
const teamKey = this.resolveTeamKeyFromResult(resolveResult, teamId, teamInput);
|
|
376
|
+
const hint = teamKey
|
|
377
|
+
? ` Run: el-linear labels list --team ${teamKey}`
|
|
378
|
+
: "";
|
|
379
|
+
throw new Error(`Label "${labelIdOrName}" is a group label. Use a specific child label instead.${hint}`);
|
|
380
|
+
}
|
|
381
|
+
resolvedLabels.push(label.id);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (labelMode === "adding") {
|
|
385
|
+
return [...new Set([...currentIssueLabels, ...resolvedLabels])];
|
|
386
|
+
}
|
|
387
|
+
return resolvedLabels;
|
|
388
|
+
}
|
|
389
|
+
/** Auto-create a missing label on the team. Returns the new label ID or null on failure. */
|
|
390
|
+
async autoCreateLabel(name, teamId) {
|
|
391
|
+
try {
|
|
392
|
+
const input = { name };
|
|
393
|
+
if (teamId) {
|
|
394
|
+
input.teamId = teamId;
|
|
395
|
+
}
|
|
396
|
+
const result = await this.graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
|
|
397
|
+
const created = result.issueLabelCreate;
|
|
398
|
+
if (created?.success && created.issueLabel) {
|
|
399
|
+
const labelData = created.issueLabel;
|
|
400
|
+
const teamInfo = labelData.team;
|
|
401
|
+
const teamKey = teamInfo?.key ?? "";
|
|
402
|
+
process.stderr.write(`Auto-created label "${name}" on team ${teamKey}\n`);
|
|
403
|
+
return labelData.id;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
resolveTeamKeyFromResult(resolveResult, teamId, teamInput) {
|
|
412
|
+
// Collect team nodes from both the top-level teams query and the project's teams.
|
|
413
|
+
// When the team was pre-resolved to a UUID by config, the top-level teams query
|
|
414
|
+
// runs with null filters and may return an unrelated team. The project's teams
|
|
415
|
+
// always include the correct team when a project is specified.
|
|
416
|
+
const allTeamNodes = this.collectTeamNodes(resolveResult);
|
|
417
|
+
if (teamId) {
|
|
418
|
+
const match = allTeamNodes.find((t) => t.id === teamId);
|
|
419
|
+
if (match) {
|
|
420
|
+
return match.key;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// If the team was pre-resolved to UUID by config and not found in the result,
|
|
424
|
+
// use the original input (e.g., "DEV") as the hint.
|
|
425
|
+
if (teamInput && !isUuid(teamInput)) {
|
|
426
|
+
return teamInput.toUpperCase();
|
|
427
|
+
}
|
|
428
|
+
// Only fall back to first team if there's exactly one (unambiguous).
|
|
429
|
+
// With multiple teams, returning an arbitrary one produces misleading error hints.
|
|
430
|
+
const topLevelNodes = resolveResult.teams?.nodes;
|
|
431
|
+
return topLevelNodes?.length === 1
|
|
432
|
+
? topLevelNodes[0].key
|
|
433
|
+
: undefined;
|
|
434
|
+
}
|
|
435
|
+
collectTeamNodes(resolveResult) {
|
|
436
|
+
const nodes = [];
|
|
437
|
+
const seen = new Set();
|
|
438
|
+
const topTeams = resolveResult.teams;
|
|
439
|
+
const topNodes = topTeams?.nodes;
|
|
440
|
+
if (topNodes) {
|
|
441
|
+
for (const t of topNodes) {
|
|
442
|
+
const id = t.id;
|
|
443
|
+
if (!seen.has(id)) {
|
|
444
|
+
seen.add(id);
|
|
445
|
+
nodes.push(t);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Also check project teams (which are always fetched when --project is used)
|
|
450
|
+
const projects = resolveResult.projects;
|
|
451
|
+
const projectNode = projects?.nodes?.[0];
|
|
452
|
+
if (projectNode) {
|
|
453
|
+
const projectTeams = projectNode.teams;
|
|
454
|
+
const projectTeamNodes = projectTeams?.nodes;
|
|
455
|
+
if (projectTeamNodes) {
|
|
456
|
+
for (const t of projectTeamNodes) {
|
|
457
|
+
const id = t.id;
|
|
458
|
+
if (!seen.has(id)) {
|
|
459
|
+
seen.add(id);
|
|
460
|
+
nodes.push(t);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return nodes;
|
|
466
|
+
}
|
|
467
|
+
resolveMilestoneId(milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes) {
|
|
468
|
+
if (!milestoneId || isUuid(milestoneId)) {
|
|
469
|
+
return milestoneId;
|
|
470
|
+
}
|
|
471
|
+
let resolved = milestoneId;
|
|
472
|
+
if (projectMilestoneNodes) {
|
|
473
|
+
const projectMilestone = projectMilestoneNodes.find((m) => m.name === milestoneId);
|
|
474
|
+
if (projectMilestone) {
|
|
475
|
+
resolved = projectMilestone.id;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!isUuid(resolved) && issueProjectMilestoneNodes) {
|
|
479
|
+
const issueMilestone = issueProjectMilestoneNodes.find((m) => m.name === milestoneId);
|
|
480
|
+
if (issueMilestone) {
|
|
481
|
+
resolved = issueMilestone.id;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const milestones = resolveResult.milestones;
|
|
485
|
+
const milestoneNodes = milestones?.nodes;
|
|
486
|
+
if (!isUuid(resolved) && milestoneNodes?.length) {
|
|
487
|
+
resolved = milestoneNodes[0].id;
|
|
488
|
+
}
|
|
489
|
+
if (!isUuid(resolved)) {
|
|
490
|
+
throw notFoundError("Milestone", milestoneId);
|
|
491
|
+
}
|
|
492
|
+
return resolved;
|
|
493
|
+
}
|
|
494
|
+
resolveMilestoneIdForCreate(milestoneId, resolveResult, finalProjectId) {
|
|
495
|
+
if (!milestoneId || isUuid(milestoneId)) {
|
|
496
|
+
return milestoneId;
|
|
497
|
+
}
|
|
498
|
+
let resolved;
|
|
499
|
+
const createProjects = resolveResult.projects;
|
|
500
|
+
const createProjectNodes = createProjects?.nodes;
|
|
501
|
+
const createProjectMilestones = createProjectNodes?.[0]
|
|
502
|
+
?.projectMilestones;
|
|
503
|
+
const createProjectMilestoneNodes = createProjectMilestones?.nodes;
|
|
504
|
+
if (createProjectMilestoneNodes) {
|
|
505
|
+
const projectMilestone = createProjectMilestoneNodes.find((m) => m.name === milestoneId);
|
|
506
|
+
if (projectMilestone) {
|
|
507
|
+
resolved = projectMilestone.id;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const createMilestones = resolveResult.milestones;
|
|
511
|
+
const createMilestoneNodes = createMilestones?.nodes;
|
|
512
|
+
if (!resolved && createMilestoneNodes?.length) {
|
|
513
|
+
resolved = createMilestoneNodes[0].id;
|
|
514
|
+
}
|
|
515
|
+
if (!resolved) {
|
|
516
|
+
if (finalProjectId) {
|
|
517
|
+
throw notFoundError("Milestone", milestoneId, "in project");
|
|
518
|
+
}
|
|
519
|
+
throw notFoundError("Milestone", milestoneId, undefined, "Consider specifying --project.");
|
|
520
|
+
}
|
|
521
|
+
return resolved;
|
|
522
|
+
}
|
|
523
|
+
async fetchIssueTeamId(issueId) {
|
|
524
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, {
|
|
525
|
+
issueId,
|
|
526
|
+
});
|
|
527
|
+
const fetchedIssue = result.issue;
|
|
528
|
+
const fetchedTeam = fetchedIssue?.team;
|
|
529
|
+
return fetchedTeam?.id;
|
|
530
|
+
}
|
|
531
|
+
buildCreateInput(args, resolved) {
|
|
532
|
+
const input = { title: args.title };
|
|
533
|
+
if (resolved.teamId) {
|
|
534
|
+
input.teamId = resolved.teamId;
|
|
535
|
+
}
|
|
536
|
+
if (args.description) {
|
|
537
|
+
input.description = args.description;
|
|
538
|
+
}
|
|
539
|
+
if (args.assigneeId) {
|
|
540
|
+
input.assigneeId = args.assigneeId;
|
|
541
|
+
}
|
|
542
|
+
if (args.priority !== undefined) {
|
|
543
|
+
input.priority = args.priority;
|
|
544
|
+
}
|
|
545
|
+
if (resolved.projectId) {
|
|
546
|
+
input.projectId = resolved.projectId;
|
|
547
|
+
}
|
|
548
|
+
if (resolved.statusId) {
|
|
549
|
+
input.stateId = resolved.statusId;
|
|
550
|
+
}
|
|
551
|
+
if (resolved.labelIds &&
|
|
552
|
+
Array.isArray(resolved.labelIds) &&
|
|
553
|
+
resolved.labelIds.length > 0) {
|
|
554
|
+
input.labelIds = resolved.labelIds;
|
|
555
|
+
}
|
|
556
|
+
if (args.estimate !== undefined) {
|
|
557
|
+
input.estimate = args.estimate;
|
|
558
|
+
}
|
|
559
|
+
if (resolved.parentId) {
|
|
560
|
+
input.parentId = resolved.parentId;
|
|
561
|
+
}
|
|
562
|
+
if (resolved.milestoneId) {
|
|
563
|
+
input.projectMilestoneId = resolved.milestoneId;
|
|
564
|
+
}
|
|
565
|
+
if (resolved.cycleId) {
|
|
566
|
+
input.cycleId = resolved.cycleId;
|
|
567
|
+
}
|
|
568
|
+
if (args.subscriberIds &&
|
|
569
|
+
Array.isArray(args.subscriberIds) &&
|
|
570
|
+
args.subscriberIds.length > 0) {
|
|
571
|
+
input.subscriberIds = args.subscriberIds;
|
|
572
|
+
}
|
|
573
|
+
if (args.dueDate !== undefined) {
|
|
574
|
+
input.dueDate = args.dueDate;
|
|
575
|
+
}
|
|
576
|
+
return input;
|
|
577
|
+
}
|
|
578
|
+
buildUpdateInput(args, resolved) {
|
|
579
|
+
const input = {};
|
|
580
|
+
if (args.title !== undefined) {
|
|
581
|
+
input.title = args.title;
|
|
582
|
+
}
|
|
583
|
+
if (args.description !== undefined) {
|
|
584
|
+
input.description = args.description;
|
|
585
|
+
}
|
|
586
|
+
if (resolved.statusId !== undefined) {
|
|
587
|
+
input.stateId = resolved.statusId;
|
|
588
|
+
}
|
|
589
|
+
if (args.priority !== undefined) {
|
|
590
|
+
input.priority = args.priority;
|
|
591
|
+
}
|
|
592
|
+
if (args.assigneeId !== undefined) {
|
|
593
|
+
input.assigneeId = args.assigneeId;
|
|
594
|
+
}
|
|
595
|
+
if (resolved.projectId !== undefined) {
|
|
596
|
+
input.projectId = resolved.projectId;
|
|
597
|
+
}
|
|
598
|
+
if (resolved.cycleId !== undefined) {
|
|
599
|
+
input.cycleId = resolved.cycleId;
|
|
600
|
+
}
|
|
601
|
+
if (args.estimate !== undefined) {
|
|
602
|
+
input.estimate = args.estimate;
|
|
603
|
+
}
|
|
604
|
+
if (args.parentId !== undefined) {
|
|
605
|
+
input.parentId = args.parentId;
|
|
606
|
+
}
|
|
607
|
+
if (resolved.milestoneId !== undefined) {
|
|
608
|
+
input.projectMilestoneId = resolved.milestoneId;
|
|
609
|
+
}
|
|
610
|
+
if (resolved.labelIds !== undefined) {
|
|
611
|
+
input.labelIds = resolved.labelIds;
|
|
612
|
+
}
|
|
613
|
+
if (args.dueDate !== undefined) {
|
|
614
|
+
input.dueDate = args.dueDate;
|
|
615
|
+
}
|
|
616
|
+
return input;
|
|
617
|
+
}
|
|
618
|
+
buildResolveVariablesForTeam(teamId) {
|
|
619
|
+
const isTeamKey = teamId.length <= 5 && TEAM_KEY_REGEX.test(teamId);
|
|
620
|
+
if (isTeamKey) {
|
|
621
|
+
return { teamKey: teamId, teamName: null };
|
|
622
|
+
}
|
|
623
|
+
return { teamKey: null, teamName: teamId };
|
|
624
|
+
}
|
|
625
|
+
applySearchFilters(results, filters) {
|
|
626
|
+
let filtered = results;
|
|
627
|
+
if (filters.teamId) {
|
|
628
|
+
filtered = filtered.filter((issue) => issue.team?.id === filters.teamId);
|
|
629
|
+
}
|
|
630
|
+
if (filters.assigneeId) {
|
|
631
|
+
filtered = filtered.filter((issue) => issue.assignee?.id === filters.assigneeId);
|
|
632
|
+
}
|
|
633
|
+
if (filters.projectId) {
|
|
634
|
+
filtered = filtered.filter((issue) => issue.project?.id === filters.projectId);
|
|
635
|
+
}
|
|
636
|
+
if (filters.status && filters.status.length > 0) {
|
|
637
|
+
filtered = filtered.filter((issue) => filters.status.includes(issue.state?.name ?? ""));
|
|
638
|
+
}
|
|
639
|
+
if (filters.labelNames && filters.labelNames.length > 0) {
|
|
640
|
+
const lowerNames = filters.labelNames.map((n) => n.toLowerCase());
|
|
641
|
+
filtered = filtered.filter((issue) => {
|
|
642
|
+
const issueLabels = issue.labels.map((l) => l.name.toLowerCase());
|
|
643
|
+
return lowerNames.every((name) => issueLabels.includes(name));
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (filters.priority && filters.priority.length > 0) {
|
|
647
|
+
filtered = filtered.filter((issue) => filters.priority.includes(issue.priority));
|
|
648
|
+
}
|
|
649
|
+
return filtered;
|
|
650
|
+
}
|
|
651
|
+
buildSearchFilter(filters) {
|
|
652
|
+
const filter = {};
|
|
653
|
+
if (filters.teamId) {
|
|
654
|
+
filter.team = { id: { eq: filters.teamId } };
|
|
655
|
+
}
|
|
656
|
+
if (filters.assigneeId) {
|
|
657
|
+
filter.assignee = { id: { eq: filters.assigneeId } };
|
|
658
|
+
}
|
|
659
|
+
if (filters.noProject) {
|
|
660
|
+
filter.project = { null: true };
|
|
661
|
+
}
|
|
662
|
+
else if (filters.projectId) {
|
|
663
|
+
filter.project = { id: { eq: filters.projectId } };
|
|
664
|
+
}
|
|
665
|
+
if (filters.status && filters.status.length > 0) {
|
|
666
|
+
filter.state = { name: { in: filters.status } };
|
|
667
|
+
}
|
|
668
|
+
if (filters.labelNames && filters.labelNames.length > 0) {
|
|
669
|
+
if (filters.labelNames.length === 1) {
|
|
670
|
+
filter.labels = {
|
|
671
|
+
some: { name: { eqIgnoreCase: filters.labelNames[0] } },
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
filter.labels = {
|
|
676
|
+
and: filters.labelNames.map((name) => ({
|
|
677
|
+
some: { name: { eqIgnoreCase: name } },
|
|
678
|
+
})),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (filters.priority && filters.priority.length > 0) {
|
|
683
|
+
filter.priority = { in: filters.priority };
|
|
684
|
+
}
|
|
685
|
+
return filter;
|
|
686
|
+
}
|
|
687
|
+
resolveAssigneeId(assigneeId, resolveResult) {
|
|
688
|
+
if (!assigneeId || isUuid(assigneeId) || !assigneeId.includes("@")) {
|
|
689
|
+
return assigneeId;
|
|
690
|
+
}
|
|
691
|
+
const users = resolveResult.users;
|
|
692
|
+
const userNodes = users?.nodes;
|
|
693
|
+
if (!userNodes?.length) {
|
|
694
|
+
throw notFoundError("User", assigneeId);
|
|
695
|
+
}
|
|
696
|
+
return userNodes[0].id;
|
|
697
|
+
}
|
|
698
|
+
async resolveUpdateContext(args) {
|
|
699
|
+
let resolvedIssueId = args.id;
|
|
700
|
+
let issueTeamId;
|
|
701
|
+
let currentIssueLabels = [];
|
|
702
|
+
const resolveVariables = {};
|
|
703
|
+
if (!isUuid(args.id)) {
|
|
704
|
+
const { teamKey, issueNumber } = parseIssueIdentifier(args.id);
|
|
705
|
+
resolveVariables.teamKey = teamKey;
|
|
706
|
+
resolveVariables.issueNumber = issueNumber;
|
|
707
|
+
}
|
|
708
|
+
if (args.labelIds && Array.isArray(args.labelIds)) {
|
|
709
|
+
const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
|
|
710
|
+
if (nonUuidLabels.length > 0) {
|
|
711
|
+
resolveVariables.__labelNames = nonUuidLabels;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (args.projectId && !isUuid(args.projectId)) {
|
|
715
|
+
resolveVariables.projectName = args.projectId;
|
|
716
|
+
}
|
|
717
|
+
if (args.milestoneId &&
|
|
718
|
+
typeof args.milestoneId === "string" &&
|
|
719
|
+
!isUuid(args.milestoneId)) {
|
|
720
|
+
resolveVariables.milestoneName = args.milestoneId;
|
|
721
|
+
}
|
|
722
|
+
const { __labelNames, ...updateQueryVars } = resolveVariables;
|
|
723
|
+
const requests = [
|
|
724
|
+
this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars),
|
|
725
|
+
];
|
|
726
|
+
if (__labelNames) {
|
|
727
|
+
const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
|
|
728
|
+
requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
|
|
729
|
+
}
|
|
730
|
+
const results = await Promise.all(requests);
|
|
731
|
+
const resolveResult = results[0];
|
|
732
|
+
if (results[1]) {
|
|
733
|
+
resolveResult.labels = results[1].labels;
|
|
734
|
+
}
|
|
735
|
+
if (!isUuid(args.id)) {
|
|
736
|
+
const resolvedIssues = resolveResult.issues;
|
|
737
|
+
const resolvedIssueNodes = resolvedIssues.nodes;
|
|
738
|
+
if (!resolvedIssueNodes.length) {
|
|
739
|
+
throw notFoundError("Issue", args.id);
|
|
740
|
+
}
|
|
741
|
+
resolvedIssueId = resolvedIssueNodes[0].id;
|
|
742
|
+
const team = resolvedIssueNodes[0].team;
|
|
743
|
+
issueTeamId = team?.id;
|
|
744
|
+
const issueLabels = resolvedIssueNodes[0].labels;
|
|
745
|
+
currentIssueLabels = issueLabels.nodes.map((l) => l.id);
|
|
746
|
+
}
|
|
747
|
+
return { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult };
|
|
748
|
+
}
|
|
749
|
+
async resolveCycleIdForUpdate(args, resolveResult, resolvedIssueId) {
|
|
750
|
+
if (args.cycleId === undefined || args.cycleId === null) {
|
|
751
|
+
return args.cycleId;
|
|
752
|
+
}
|
|
753
|
+
if (typeof args.cycleId !== "string" || isUuid(args.cycleId)) {
|
|
754
|
+
return args.cycleId;
|
|
755
|
+
}
|
|
756
|
+
let teamIdForCycle = resolveResult.issues?.nodes?.[0]?.team?.id;
|
|
757
|
+
if (!teamIdForCycle && resolvedIssueId && isUuid(resolvedIssueId)) {
|
|
758
|
+
teamIdForCycle = await this.fetchIssueTeamId(resolvedIssueId);
|
|
759
|
+
}
|
|
760
|
+
return this.linearService.resolveCycleId(args.cycleId, teamIdForCycle);
|
|
761
|
+
}
|
|
762
|
+
async resolveStatusIdForUpdate(args, resolvedIssueId) {
|
|
763
|
+
if (!args.statusId || isUuid(args.statusId)) {
|
|
764
|
+
return args.statusId;
|
|
765
|
+
}
|
|
766
|
+
let teamId;
|
|
767
|
+
if (resolvedIssueId && isUuid(resolvedIssueId)) {
|
|
768
|
+
teamId = await this.fetchIssueTeamId(resolvedIssueId);
|
|
769
|
+
}
|
|
770
|
+
return this.linearService.resolveStatusId(args.statusId, teamId);
|
|
771
|
+
}
|
|
772
|
+
buildCreateResolveVariables(args) {
|
|
773
|
+
const resolveVariables = {};
|
|
774
|
+
if (args.teamId && !isUuid(args.teamId)) {
|
|
775
|
+
Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
|
|
776
|
+
}
|
|
777
|
+
if (args.projectId && !isUuid(args.projectId)) {
|
|
778
|
+
resolveVariables.projectName = args.projectId;
|
|
779
|
+
}
|
|
780
|
+
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
781
|
+
resolveVariables.milestoneName = args.milestoneId;
|
|
782
|
+
}
|
|
783
|
+
if (args.labelIds && Array.isArray(args.labelIds)) {
|
|
784
|
+
const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
|
|
785
|
+
if (nonUuidLabels.length > 0) {
|
|
786
|
+
resolveVariables.__labelNames = nonUuidLabels;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (args.parentId && !isUuid(args.parentId)) {
|
|
790
|
+
const parentParsed = tryParseIssueIdentifier(args.parentId);
|
|
791
|
+
if (parentParsed) {
|
|
792
|
+
resolveVariables.parentTeamKey = parentParsed.teamKey;
|
|
793
|
+
resolveVariables.parentIssueNumber = parentParsed.issueNumber;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return resolveVariables;
|
|
797
|
+
}
|
|
798
|
+
resolveParentId(parentId, resolveResult) {
|
|
799
|
+
if (!parentId || isUuid(parentId)) {
|
|
800
|
+
return parentId;
|
|
801
|
+
}
|
|
802
|
+
const parentIssues = resolveResult.parentIssues;
|
|
803
|
+
const parentNodes = parentIssues?.nodes;
|
|
804
|
+
if (!parentNodes?.length) {
|
|
805
|
+
throw notFoundError("Parent issue", parentId);
|
|
806
|
+
}
|
|
807
|
+
return parentNodes[0].id;
|
|
808
|
+
}
|
|
809
|
+
transformIssueData(issue) {
|
|
810
|
+
const labels = issue.labels;
|
|
811
|
+
return {
|
|
812
|
+
...this.transformIssueCoreFields(issue),
|
|
813
|
+
...this.transformIssueRelations(issue),
|
|
814
|
+
summary: issue.summary
|
|
815
|
+
? extractSummaryText(issue.summary)
|
|
816
|
+
: undefined,
|
|
817
|
+
priority: issue.priority,
|
|
818
|
+
estimate: issue.estimate || undefined,
|
|
819
|
+
dueDate: issue.dueDate || undefined,
|
|
820
|
+
labels: labels.nodes.map((label) => ({
|
|
821
|
+
id: label.id,
|
|
822
|
+
name: label.name,
|
|
823
|
+
})),
|
|
824
|
+
...this.transformIssueHierarchy(issue),
|
|
825
|
+
comments: this.transformIssueComments(issue),
|
|
826
|
+
createdAt: toISOStringOrNow(issue.createdAt),
|
|
827
|
+
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
transformIssueCoreFields(issue) {
|
|
831
|
+
return {
|
|
832
|
+
id: issue.id,
|
|
833
|
+
identifier: issue.identifier,
|
|
834
|
+
url: issue.url,
|
|
835
|
+
title: issue.title,
|
|
836
|
+
description: issue.description || undefined,
|
|
837
|
+
branchName: issue.branchName || undefined,
|
|
838
|
+
embeds: issue.description
|
|
839
|
+
? extractEmbeds(issue.description)
|
|
840
|
+
: undefined,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
transformIssueRelations(issue) {
|
|
844
|
+
const state = issue.state;
|
|
845
|
+
const assignee = issue.assignee;
|
|
846
|
+
const team = issue.team;
|
|
847
|
+
const project = issue.project;
|
|
848
|
+
const cycle = issue.cycle;
|
|
849
|
+
const milestone = issue.projectMilestone;
|
|
850
|
+
return {
|
|
851
|
+
state: state
|
|
852
|
+
? { id: state.id, name: state.name }
|
|
853
|
+
: undefined,
|
|
854
|
+
assignee: assignee
|
|
855
|
+
? {
|
|
856
|
+
id: assignee.id,
|
|
857
|
+
name: resolveUserDisplayName(assignee.id, assignee.name),
|
|
858
|
+
url: assignee.url || undefined,
|
|
859
|
+
}
|
|
860
|
+
: undefined,
|
|
861
|
+
team: team
|
|
862
|
+
? {
|
|
863
|
+
id: team.id,
|
|
864
|
+
key: team.key,
|
|
865
|
+
name: team.name,
|
|
866
|
+
}
|
|
867
|
+
: undefined,
|
|
868
|
+
project: project
|
|
869
|
+
? { id: project.id, name: project.name }
|
|
870
|
+
: undefined,
|
|
871
|
+
cycle: cycle
|
|
872
|
+
? {
|
|
873
|
+
id: cycle.id,
|
|
874
|
+
name: cycle.name,
|
|
875
|
+
number: cycle.number,
|
|
876
|
+
}
|
|
877
|
+
: undefined,
|
|
878
|
+
projectMilestone: milestone
|
|
879
|
+
? {
|
|
880
|
+
id: milestone.id,
|
|
881
|
+
name: milestone.name,
|
|
882
|
+
targetDate: milestone.targetDate || undefined,
|
|
883
|
+
}
|
|
884
|
+
: undefined,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
transformIssueHierarchy(issue) {
|
|
888
|
+
const parent = issue.parent;
|
|
889
|
+
const children = issue.children;
|
|
890
|
+
return {
|
|
891
|
+
parentIssue: parent
|
|
892
|
+
? {
|
|
893
|
+
id: parent.id,
|
|
894
|
+
identifier: parent.identifier,
|
|
895
|
+
title: parent.title,
|
|
896
|
+
}
|
|
897
|
+
: undefined,
|
|
898
|
+
subIssues: children?.nodes?.map((child) => ({
|
|
899
|
+
id: child.id,
|
|
900
|
+
identifier: child.identifier,
|
|
901
|
+
title: child.title,
|
|
902
|
+
})) || undefined,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
transformIssueComments(issue) {
|
|
906
|
+
const comments = issue.comments;
|
|
907
|
+
return (comments?.nodes?.map((comment) => {
|
|
908
|
+
const commentUser = comment.user;
|
|
909
|
+
return {
|
|
910
|
+
id: comment.id,
|
|
911
|
+
body: comment.body,
|
|
912
|
+
embeds: extractEmbeds(comment.body),
|
|
913
|
+
user: commentUser
|
|
914
|
+
? {
|
|
915
|
+
id: commentUser.id,
|
|
916
|
+
name: resolveUserDisplayName(commentUser.id, commentUser.name),
|
|
917
|
+
url: commentUser.url || undefined,
|
|
918
|
+
}
|
|
919
|
+
: undefined,
|
|
920
|
+
createdAt: toISOStringOrNow(comment.createdAt),
|
|
921
|
+
updatedAt: toISOStringOrNow(comment.updatedAt),
|
|
922
|
+
};
|
|
923
|
+
}) || []);
|
|
924
|
+
}
|
|
925
|
+
}
|