@enrichlayer/el-linear 1.7.0 → 1.10.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/README.md +82 -1
- package/claude-skills/linear-operations/SKILL.md +55 -0
- package/dist/auth/oauth-callback.js +14 -7
- package/dist/auth/oauth-fs.d.ts +22 -0
- package/dist/auth/oauth-fs.js +77 -6
- package/dist/auth/oauth-headless.d.ts +13 -8
- package/dist/auth/oauth-headless.js +18 -11
- package/dist/auth/oauth-storage.d.ts +11 -3
- package/dist/auth/oauth-storage.js +15 -8
- package/dist/auth/token-resolver.d.ts +9 -0
- package/dist/auth/token-resolver.js +57 -37
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/comments.js +17 -28
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/init/index.js +2 -0
- package/dist/commands/init/oauth.d.ts +6 -0
- package/dist/commands/init/oauth.js +8 -2
- package/dist/commands/init/token.d.ts +0 -8
- package/dist/commands/init/token.js +13 -1
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.d.ts +17 -0
- package/dist/commands/issues/branch.js +40 -0
- package/dist/commands/issues/description.d.ts +89 -0
- package/dist/commands/issues/description.js +183 -0
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +55 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +97 -497
- package/dist/commands/labels.js +20 -28
- package/dist/commands/profile/migrate-legacy.js +10 -33
- package/dist/commands/profile.js +1 -10
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +5 -1
- package/dist/commands/projects.js +202 -102
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +6 -1
- package/dist/commands/refs.js +10 -1
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +21 -30
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +128 -7
- package/dist/commands/users.js +3 -1
- package/dist/config/config.js +31 -9
- package/dist/config/issue-validation.js +1 -1
- package/dist/config/paths.d.ts +11 -0
- package/dist/config/paths.js +44 -6
- package/dist/config/resolver.js +2 -3
- package/dist/config/term-enforcer.js +1 -1
- package/dist/main.js +37 -2
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +49 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/documents-types.d.ts +61 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +57 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +416 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +2 -0
- package/dist/queries/issues.js +22 -0
- package/dist/queries/labels-types.d.ts +64 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +91 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/projects-types.d.ts +75 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +84 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +86 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +61 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/queries/templates.d.ts +3 -0
- package/dist/queries/templates.js +43 -0
- package/dist/types/linear.d.ts +8 -2
- package/dist/utils/auth.js +7 -9
- package/dist/utils/auto-link-references.js +44 -25
- package/dist/utils/disk-cache.js +2 -1
- package/dist/utils/formatters/summary.d.ts +62 -0
- package/dist/utils/formatters/summary.js +755 -0
- package/dist/utils/graphql-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +118 -5
- package/dist/utils/graphql-issues-service.js +202 -209
- package/dist/utils/issue-reference-extractor.d.ts +9 -1
- package/dist/utils/issue-reference-extractor.js +16 -9
- package/dist/utils/issue-reference-wrapper.js +1 -54
- package/dist/utils/linear-service.d.ts +7 -3
- package/dist/utils/linear-service.js +27 -5
- package/dist/utils/markdown-prosemirror.js +17 -1
- package/dist/utils/mention-resolver.js +17 -5
- package/dist/utils/output.d.ts +7 -1
- package/dist/utils/output.js +38 -1
- package/dist/utils/protected-ranges.d.ts +33 -0
- package/dist/utils/protected-ranges.js +73 -0
- package/dist/utils/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +46 -24
- package/dist/utils/validators.d.ts +9 -1
- package/dist/utils/validators.js +10 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +35 -5
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
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";
|
|
2
|
+
import { ARCHIVE_ISSUE_MUTATION, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_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
3
|
import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
|
|
4
4
|
import { toISOStringOrNow } from "./date-format.js";
|
|
5
5
|
import { extractEmbeds } from "./embed-parser.js";
|
|
6
6
|
import { notFoundError } from "./error-messages.js";
|
|
7
7
|
import { parseIssueIdentifier, tryParseIssueIdentifier, } from "./identifier-parser.js";
|
|
8
|
+
import { logger } from "./logger.js";
|
|
8
9
|
import { isUuid } from "./uuid.js";
|
|
9
10
|
const TEAM_KEY_REGEX = /^[A-Z0-9]+$/i;
|
|
10
11
|
function extractSummaryText(summary) {
|
|
@@ -13,7 +14,7 @@ function extractSummaryText(summary) {
|
|
|
13
14
|
}
|
|
14
15
|
const parts = [];
|
|
15
16
|
const walk = (node) => {
|
|
16
|
-
if (node.text) {
|
|
17
|
+
if (typeof node.text === "string") {
|
|
17
18
|
parts.push(node.text);
|
|
18
19
|
}
|
|
19
20
|
if (Array.isArray(node.content)) {
|
|
@@ -33,15 +34,12 @@ export class GraphQLIssuesService {
|
|
|
33
34
|
this.linearService = linearService;
|
|
34
35
|
}
|
|
35
36
|
async getIssues(limit = 25) {
|
|
36
|
-
const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
const issues = result.issues;
|
|
41
|
-
if (!issues?.nodes) {
|
|
37
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, { first: limit, orderBy: "updatedAt" });
|
|
38
|
+
const nodes = result.issues?.nodes;
|
|
39
|
+
if (!nodes?.length) {
|
|
42
40
|
return [];
|
|
43
41
|
}
|
|
44
|
-
return
|
|
42
|
+
return nodes.map((issue) => this.transformIssueData(issue));
|
|
45
43
|
}
|
|
46
44
|
async getIssueById(issueId) {
|
|
47
45
|
let issueData;
|
|
@@ -54,12 +52,8 @@ export class GraphQLIssuesService {
|
|
|
54
52
|
}
|
|
55
53
|
else {
|
|
56
54
|
const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
|
|
57
|
-
const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, {
|
|
58
|
-
|
|
59
|
-
number: issueNumber,
|
|
60
|
-
});
|
|
61
|
-
const issues = result.issues;
|
|
62
|
-
const nodes = issues?.nodes;
|
|
55
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, { teamKey, number: issueNumber });
|
|
56
|
+
const nodes = result.issues?.nodes;
|
|
63
57
|
if (!nodes?.length) {
|
|
64
58
|
throw notFoundError("Issue", issueId);
|
|
65
59
|
}
|
|
@@ -76,15 +70,18 @@ export class GraphQLIssuesService {
|
|
|
76
70
|
const finalLabelIds = await this.resolveLabelsWithMode(args.labelIds, resolveResult, labelMode, currentIssueLabels, teamIdForLabels, args.teamId);
|
|
77
71
|
const finalProjectId = args.projectId
|
|
78
72
|
? this.resolveProjectId(args.projectId, resolveResult)
|
|
79
|
-
:
|
|
73
|
+
: undefined;
|
|
80
74
|
const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(args, resolveResult);
|
|
81
75
|
const finalMilestoneId = this.resolveMilestoneId(args.milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes);
|
|
82
76
|
const finalCycleId = await this.resolveCycleIdForUpdate(args, resolveResult, resolvedIssueId);
|
|
83
77
|
const resolvedStatusId = await this.resolveStatusIdForUpdate(args, resolvedIssueId);
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
// Resolve assignee in place — `buildUpdateInput` reads it
|
|
79
|
+
// directly. Match the createIssue pattern.
|
|
80
|
+
let assigneeId = args.assigneeId;
|
|
81
|
+
if (assigneeId && !isUuid(assigneeId)) {
|
|
82
|
+
assigneeId = await this.linearService.resolveUserId(assigneeId);
|
|
86
83
|
}
|
|
87
|
-
const updateInput = this.buildUpdateInput(args, {
|
|
84
|
+
const updateInput = this.buildUpdateInput({ ...args, assigneeId }, {
|
|
88
85
|
statusId: resolvedStatusId,
|
|
89
86
|
projectId: finalProjectId,
|
|
90
87
|
labelIds: finalLabelIds,
|
|
@@ -93,15 +90,40 @@ export class GraphQLIssuesService {
|
|
|
93
90
|
});
|
|
94
91
|
return this.executeUpdateMutation(resolvedIssueId, args.id, updateInput);
|
|
95
92
|
}
|
|
93
|
+
async archiveIssue(issueId) {
|
|
94
|
+
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
95
|
+
const result = await this.graphQLService.rawRequest(ARCHIVE_ISSUE_MUTATION, { id: resolvedIssueId });
|
|
96
|
+
const payload = result.issueArchive;
|
|
97
|
+
if (!payload.success) {
|
|
98
|
+
throw new Error(`Failed to archive issue ${issueId}`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
id: resolvedIssueId,
|
|
102
|
+
entity: payload.entity ?? undefined,
|
|
103
|
+
lastSyncId: payload.lastSyncId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async deleteIssue(issueId, options = {}) {
|
|
107
|
+
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
108
|
+
const result = await this.graphQLService.rawRequest(DELETE_ISSUE_MUTATION, {
|
|
109
|
+
id: resolvedIssueId,
|
|
110
|
+
permanentlyDelete: Boolean(options.permanentlyDelete),
|
|
111
|
+
});
|
|
112
|
+
const payload = result.issueDelete;
|
|
113
|
+
if (!payload.success) {
|
|
114
|
+
throw new Error(`Failed to delete issue ${issueId}`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: resolvedIssueId,
|
|
118
|
+
entity: payload.entity ?? undefined,
|
|
119
|
+
lastSyncId: payload.lastSyncId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
96
122
|
extractMilestoneNodes(args, resolveResult) {
|
|
97
|
-
const projectNodes = resolveResult.projects?.nodes;
|
|
98
123
|
const projectMilestoneNodes = args.projectId
|
|
99
|
-
?
|
|
124
|
+
? resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes
|
|
100
125
|
: undefined;
|
|
101
|
-
const
|
|
102
|
-
?.nodes;
|
|
103
|
-
const issueProjectMilestoneNodes = issueNodes?.[0]?.project
|
|
104
|
-
?.projectMilestones?.nodes;
|
|
126
|
+
const issueProjectMilestoneNodes = resolveResult.issues?.nodes?.[0]?.project?.projectMilestones.nodes;
|
|
105
127
|
return { projectMilestoneNodes, issueProjectMilestoneNodes };
|
|
106
128
|
}
|
|
107
129
|
async executeUpdateMutation(resolvedIssueId, originalId, updateInput) {
|
|
@@ -136,49 +158,51 @@ export class GraphQLIssuesService {
|
|
|
136
158
|
let resolveResult = {};
|
|
137
159
|
if (Object.keys(resolveVariables).length > 0) {
|
|
138
160
|
const { __labelNames, ...queryVars } = resolveVariables;
|
|
139
|
-
const
|
|
140
|
-
this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars),
|
|
141
|
-
];
|
|
161
|
+
const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars);
|
|
142
162
|
if (__labelNames) {
|
|
143
163
|
const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
|
|
144
|
-
|
|
164
|
+
const [batch, labels] = await Promise.all([
|
|
165
|
+
batchPromise,
|
|
166
|
+
this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
|
|
167
|
+
]);
|
|
168
|
+
resolveResult = { ...batch, labels: labels.labels };
|
|
145
169
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (results[1]) {
|
|
149
|
-
resolveResult.labels = results[1].labels;
|
|
170
|
+
else {
|
|
171
|
+
resolveResult = await batchPromise;
|
|
150
172
|
}
|
|
151
173
|
}
|
|
152
174
|
const resolved = await this.resolveCreateFields(args, resolveResult);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
// Mutate-the-input feels gross but matches the existing pattern
|
|
176
|
+
// — `resolved` doesn't carry assigneeId, so the buildCreateInput
|
|
177
|
+
// step reads `args.assigneeId` directly. Resolve in place.
|
|
178
|
+
let assigneeId = args.assigneeId;
|
|
179
|
+
if (assigneeId && !isUuid(assigneeId)) {
|
|
180
|
+
assigneeId = await this.linearService.resolveUserId(assigneeId);
|
|
181
|
+
}
|
|
182
|
+
const createInput = this.buildCreateInput({ ...args, assigneeId }, resolved);
|
|
157
183
|
return this.executeCreateMutation(createInput);
|
|
158
184
|
}
|
|
159
185
|
async resolveCreateFields(args, resolveResult) {
|
|
160
186
|
let teamId = args.teamId
|
|
161
187
|
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
162
|
-
:
|
|
188
|
+
: undefined;
|
|
163
189
|
const projectId = args.projectId
|
|
164
190
|
? this.resolveProjectId(args.projectId, resolveResult)
|
|
165
|
-
:
|
|
191
|
+
: undefined;
|
|
166
192
|
// Validate team-project compatibility and auto-correct when possible
|
|
167
|
-
if (projectId && teamId) {
|
|
193
|
+
if (projectId && teamId && args.projectId) {
|
|
168
194
|
teamId = this.validateProjectTeam(teamId, args.projectId, resolveResult);
|
|
169
195
|
}
|
|
170
196
|
const labelIds = await this.resolveLabels(args.labelIds, resolveResult, teamId, args.teamInput);
|
|
171
197
|
const parentId = this.resolveParentId(args.parentId, resolveResult);
|
|
172
198
|
const milestoneId = this.resolveMilestoneIdForCreate(args.milestoneId, resolveResult, projectId);
|
|
173
199
|
let cycleId = args.cycleId;
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
!isUuid(args.cycleId)) {
|
|
177
|
-
cycleId = await this.linearService.resolveCycleId(args.cycleId, teamId);
|
|
200
|
+
if (cycleId && !isUuid(cycleId)) {
|
|
201
|
+
cycleId = await this.linearService.resolveCycleId(cycleId, teamId);
|
|
178
202
|
}
|
|
179
203
|
let statusId = args.statusId;
|
|
180
|
-
if (
|
|
181
|
-
statusId = await this.linearService.resolveStatusId(
|
|
204
|
+
if (statusId && !isUuid(statusId)) {
|
|
205
|
+
statusId = await this.linearService.resolveStatusId(statusId, teamId);
|
|
182
206
|
}
|
|
183
207
|
return {
|
|
184
208
|
teamId,
|
|
@@ -225,25 +249,27 @@ export class GraphQLIssuesService {
|
|
|
225
249
|
}
|
|
226
250
|
let resolveResult = {};
|
|
227
251
|
if (Object.keys(resolveVariables).length > 0) {
|
|
228
|
-
resolveResult =
|
|
252
|
+
resolveResult =
|
|
253
|
+
await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
|
|
229
254
|
}
|
|
230
255
|
const finalTeamId = args.teamId
|
|
231
256
|
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
232
|
-
:
|
|
257
|
+
: undefined;
|
|
233
258
|
const finalProjectId = args.projectId
|
|
234
259
|
? this.resolveProjectId(args.projectId, resolveResult)
|
|
235
|
-
:
|
|
260
|
+
: undefined;
|
|
236
261
|
const finalAssigneeId = this.resolveAssigneeId(args.assigneeId, resolveResult);
|
|
262
|
+
const limit = args.limit ?? 10;
|
|
237
263
|
if (args.query) {
|
|
238
264
|
const searchResult = await this.graphQLService.rawRequest(SEARCH_ISSUES_QUERY, {
|
|
239
265
|
term: args.query,
|
|
240
|
-
first:
|
|
266
|
+
first: limit,
|
|
241
267
|
});
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
268
|
+
const nodes = searchResult.searchIssues?.nodes;
|
|
269
|
+
if (!nodes?.length) {
|
|
244
270
|
return [];
|
|
245
271
|
}
|
|
246
|
-
const results =
|
|
272
|
+
const results = nodes.map((issue) => this.transformIssueData(issue));
|
|
247
273
|
return this.applySearchFilters(results, {
|
|
248
274
|
teamId: finalTeamId,
|
|
249
275
|
assigneeId: finalAssigneeId,
|
|
@@ -263,9 +289,9 @@ export class GraphQLIssuesService {
|
|
|
263
289
|
priority: args.priority,
|
|
264
290
|
});
|
|
265
291
|
const searchResult = await this.graphQLService.rawRequest(FILTERED_SEARCH_ISSUES_QUERY, {
|
|
266
|
-
first:
|
|
292
|
+
first: limit,
|
|
267
293
|
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
268
|
-
orderBy: args.orderBy
|
|
294
|
+
orderBy: args.orderBy ?? "updatedAt",
|
|
269
295
|
});
|
|
270
296
|
const filteredIssues = searchResult.issues;
|
|
271
297
|
if (!filteredIssues?.nodes) {
|
|
@@ -278,8 +304,7 @@ export class GraphQLIssuesService {
|
|
|
278
304
|
if (isUuid(teamId)) {
|
|
279
305
|
return teamId;
|
|
280
306
|
}
|
|
281
|
-
const
|
|
282
|
-
const teamNodes = teams?.nodes;
|
|
307
|
+
const teamNodes = resolveResult.teams?.nodes;
|
|
283
308
|
const resolvedTeam = teamNodes?.[0];
|
|
284
309
|
if (resolvedTeam &&
|
|
285
310
|
(resolvedTeam.key.toUpperCase() === teamId.toUpperCase() ||
|
|
@@ -293,8 +318,7 @@ export class GraphQLIssuesService {
|
|
|
293
318
|
if (isUuid(projectId)) {
|
|
294
319
|
return projectId;
|
|
295
320
|
}
|
|
296
|
-
const
|
|
297
|
-
const projectNodes = projects?.nodes;
|
|
321
|
+
const projectNodes = resolveResult.projects?.nodes;
|
|
298
322
|
if (!projectNodes?.length) {
|
|
299
323
|
throw notFoundError("Project", projectId);
|
|
300
324
|
}
|
|
@@ -305,13 +329,11 @@ export class GraphQLIssuesService {
|
|
|
305
329
|
* If not, auto-switch when the project belongs to exactly one team.
|
|
306
330
|
*/
|
|
307
331
|
validateProjectTeam(teamId, projectInput, resolveResult) {
|
|
308
|
-
const
|
|
309
|
-
const projectNode = projects?.nodes?.[0];
|
|
332
|
+
const projectNode = resolveResult.projects?.nodes?.[0];
|
|
310
333
|
if (!projectNode) {
|
|
311
334
|
return teamId;
|
|
312
335
|
}
|
|
313
|
-
const
|
|
314
|
-
const teamNodes = teamsConn?.nodes;
|
|
336
|
+
const teamNodes = projectNode.teams?.nodes;
|
|
315
337
|
if (!teamNodes?.length) {
|
|
316
338
|
return teamId;
|
|
317
339
|
}
|
|
@@ -324,7 +346,7 @@ export class GraphQLIssuesService {
|
|
|
324
346
|
if (teamNodes.length === 1) {
|
|
325
347
|
const correctTeamId = teamNodes[0].id;
|
|
326
348
|
const correctKey = teamKeys[0];
|
|
327
|
-
|
|
349
|
+
logger.error(`Auto-switched team to ${correctKey} (the only team associated with project "${projectName}").`);
|
|
328
350
|
return correctTeamId;
|
|
329
351
|
}
|
|
330
352
|
throw new Error(`Project "${projectName}" is not associated with the specified team.\n` +
|
|
@@ -339,8 +361,7 @@ export class GraphQLIssuesService {
|
|
|
339
361
|
return labelIds;
|
|
340
362
|
}
|
|
341
363
|
const resolvedLabels = [];
|
|
342
|
-
const
|
|
343
|
-
const labelNodes = labels?.nodes;
|
|
364
|
+
const labelNodes = resolveResult.labels?.nodes;
|
|
344
365
|
for (const labelIdOrName of labelIds) {
|
|
345
366
|
if (isUuid(labelIdOrName)) {
|
|
346
367
|
resolvedLabels.push(labelIdOrName);
|
|
@@ -350,10 +371,7 @@ export class GraphQLIssuesService {
|
|
|
350
371
|
const candidates = labelNodes?.filter((l) => l.name.toLowerCase() === lowerName);
|
|
351
372
|
let label;
|
|
352
373
|
if (candidates && candidates.length > 1 && teamId) {
|
|
353
|
-
label = candidates.find((l) =>
|
|
354
|
-
const team = l.team;
|
|
355
|
-
return team?.id === teamId;
|
|
356
|
-
});
|
|
374
|
+
label = candidates.find((l) => l.team?.id === teamId);
|
|
357
375
|
}
|
|
358
376
|
if (!label) {
|
|
359
377
|
label = candidates?.[0];
|
|
@@ -395,12 +413,10 @@ export class GraphQLIssuesService {
|
|
|
395
413
|
}
|
|
396
414
|
const result = await this.graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
|
|
397
415
|
const created = result.issueLabelCreate;
|
|
398
|
-
if (created
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
process.stderr.write(`Auto-created label "${name}" on team ${teamKey}\n`);
|
|
403
|
-
return labelData.id;
|
|
416
|
+
if (created.success && created.issueLabel) {
|
|
417
|
+
const teamKey = created.issueLabel.team?.key ?? "";
|
|
418
|
+
logger.error(`Auto-created label "${name}" on team ${teamKey}`);
|
|
419
|
+
return created.issueLabel.id;
|
|
404
420
|
}
|
|
405
421
|
return null;
|
|
406
422
|
}
|
|
@@ -428,37 +444,27 @@ export class GraphQLIssuesService {
|
|
|
428
444
|
// Only fall back to first team if there's exactly one (unambiguous).
|
|
429
445
|
// With multiple teams, returning an arbitrary one produces misleading error hints.
|
|
430
446
|
const topLevelNodes = resolveResult.teams?.nodes;
|
|
431
|
-
return topLevelNodes?.length === 1
|
|
432
|
-
? topLevelNodes[0].key
|
|
433
|
-
: undefined;
|
|
447
|
+
return topLevelNodes?.length === 1 ? topLevelNodes[0].key : undefined;
|
|
434
448
|
}
|
|
435
449
|
collectTeamNodes(resolveResult) {
|
|
436
450
|
const nodes = [];
|
|
437
451
|
const seen = new Set();
|
|
438
|
-
const
|
|
439
|
-
const topNodes = topTeams?.nodes;
|
|
452
|
+
const topNodes = resolveResult.teams?.nodes;
|
|
440
453
|
if (topNodes) {
|
|
441
454
|
for (const t of topNodes) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
seen.add(id);
|
|
455
|
+
if (!seen.has(t.id)) {
|
|
456
|
+
seen.add(t.id);
|
|
445
457
|
nodes.push(t);
|
|
446
458
|
}
|
|
447
459
|
}
|
|
448
460
|
}
|
|
449
461
|
// Also check project teams (which are always fetched when --project is used)
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
const projectTeamNodes = resolveResult.projects?.nodes?.[0]?.teams?.nodes;
|
|
463
|
+
if (projectTeamNodes) {
|
|
464
|
+
for (const t of projectTeamNodes) {
|
|
465
|
+
if (!seen.has(t.id)) {
|
|
466
|
+
seen.add(t.id);
|
|
467
|
+
nodes.push(t);
|
|
462
468
|
}
|
|
463
469
|
}
|
|
464
470
|
}
|
|
@@ -481,8 +487,7 @@ export class GraphQLIssuesService {
|
|
|
481
487
|
resolved = issueMilestone.id;
|
|
482
488
|
}
|
|
483
489
|
}
|
|
484
|
-
const
|
|
485
|
-
const milestoneNodes = milestones?.nodes;
|
|
490
|
+
const milestoneNodes = resolveResult.milestones?.nodes;
|
|
486
491
|
if (!isUuid(resolved) && milestoneNodes?.length) {
|
|
487
492
|
resolved = milestoneNodes[0].id;
|
|
488
493
|
}
|
|
@@ -496,19 +501,14 @@ export class GraphQLIssuesService {
|
|
|
496
501
|
return milestoneId;
|
|
497
502
|
}
|
|
498
503
|
let resolved;
|
|
499
|
-
const
|
|
500
|
-
const createProjectNodes = createProjects?.nodes;
|
|
501
|
-
const createProjectMilestones = createProjectNodes?.[0]
|
|
502
|
-
?.projectMilestones;
|
|
503
|
-
const createProjectMilestoneNodes = createProjectMilestones?.nodes;
|
|
504
|
+
const createProjectMilestoneNodes = resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes;
|
|
504
505
|
if (createProjectMilestoneNodes) {
|
|
505
506
|
const projectMilestone = createProjectMilestoneNodes.find((m) => m.name === milestoneId);
|
|
506
507
|
if (projectMilestone) {
|
|
507
508
|
resolved = projectMilestone.id;
|
|
508
509
|
}
|
|
509
510
|
}
|
|
510
|
-
const
|
|
511
|
-
const createMilestoneNodes = createMilestones?.nodes;
|
|
511
|
+
const createMilestoneNodes = resolveResult.milestones?.nodes;
|
|
512
512
|
if (!resolved && createMilestoneNodes?.length) {
|
|
513
513
|
resolved = createMilestoneNodes[0].id;
|
|
514
514
|
}
|
|
@@ -521,15 +521,18 @@ export class GraphQLIssuesService {
|
|
|
521
521
|
return resolved;
|
|
522
522
|
}
|
|
523
523
|
async fetchIssueTeamId(issueId) {
|
|
524
|
-
const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, {
|
|
525
|
-
|
|
526
|
-
});
|
|
527
|
-
const fetchedIssue = result.issue;
|
|
528
|
-
const fetchedTeam = fetchedIssue?.team;
|
|
529
|
-
return fetchedTeam?.id;
|
|
524
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, { issueId });
|
|
525
|
+
return result.issue?.team?.id;
|
|
530
526
|
}
|
|
531
527
|
buildCreateInput(args, resolved) {
|
|
532
|
-
const input = {
|
|
528
|
+
const input = {};
|
|
529
|
+
// Title is required by Linear's API unless --from-template is set,
|
|
530
|
+
// in which case Linear copies the template's title. We only include
|
|
531
|
+
// the field when explicitly set so a missing title with templateId
|
|
532
|
+
// works as the API documents.
|
|
533
|
+
if (args.title) {
|
|
534
|
+
input.title = args.title;
|
|
535
|
+
}
|
|
533
536
|
if (resolved.teamId) {
|
|
534
537
|
input.teamId = resolved.teamId;
|
|
535
538
|
}
|
|
@@ -548,9 +551,7 @@ export class GraphQLIssuesService {
|
|
|
548
551
|
if (resolved.statusId) {
|
|
549
552
|
input.stateId = resolved.statusId;
|
|
550
553
|
}
|
|
551
|
-
if (resolved.labelIds &&
|
|
552
|
-
Array.isArray(resolved.labelIds) &&
|
|
553
|
-
resolved.labelIds.length > 0) {
|
|
554
|
+
if (resolved.labelIds && resolved.labelIds.length > 0) {
|
|
554
555
|
input.labelIds = resolved.labelIds;
|
|
555
556
|
}
|
|
556
557
|
if (args.estimate !== undefined) {
|
|
@@ -565,14 +566,18 @@ export class GraphQLIssuesService {
|
|
|
565
566
|
if (resolved.cycleId) {
|
|
566
567
|
input.cycleId = resolved.cycleId;
|
|
567
568
|
}
|
|
568
|
-
if (args.subscriberIds &&
|
|
569
|
-
Array.isArray(args.subscriberIds) &&
|
|
570
|
-
args.subscriberIds.length > 0) {
|
|
569
|
+
if (args.subscriberIds && args.subscriberIds.length > 0) {
|
|
571
570
|
input.subscriberIds = args.subscriberIds;
|
|
572
571
|
}
|
|
573
572
|
if (args.dueDate !== undefined) {
|
|
574
573
|
input.dueDate = args.dueDate;
|
|
575
574
|
}
|
|
575
|
+
if (args.templateId !== undefined && args.templateId !== null) {
|
|
576
|
+
// Server-side template instantiation: Linear copies the
|
|
577
|
+
// template's title/description/labels/priority onto the
|
|
578
|
+
// new issue. Any explicit field above wins by override.
|
|
579
|
+
input.templateId = args.templateId;
|
|
580
|
+
}
|
|
576
581
|
return input;
|
|
577
582
|
}
|
|
578
583
|
buildUpdateInput(args, resolved) {
|
|
@@ -688,8 +693,7 @@ export class GraphQLIssuesService {
|
|
|
688
693
|
if (!assigneeId || isUuid(assigneeId) || !assigneeId.includes("@")) {
|
|
689
694
|
return assigneeId;
|
|
690
695
|
}
|
|
691
|
-
const
|
|
692
|
-
const userNodes = users?.nodes;
|
|
696
|
+
const userNodes = resolveResult.users?.nodes;
|
|
693
697
|
if (!userNodes?.length) {
|
|
694
698
|
throw notFoundError("User", assigneeId);
|
|
695
699
|
}
|
|
@@ -705,7 +709,7 @@ export class GraphQLIssuesService {
|
|
|
705
709
|
resolveVariables.teamKey = teamKey;
|
|
706
710
|
resolveVariables.issueNumber = issueNumber;
|
|
707
711
|
}
|
|
708
|
-
if (args.labelIds &&
|
|
712
|
+
if (args.labelIds && args.labelIds.length > 0) {
|
|
709
713
|
const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
|
|
710
714
|
if (nonUuidLabels.length > 0) {
|
|
711
715
|
resolveVariables.__labelNames = nonUuidLabels;
|
|
@@ -714,35 +718,31 @@ export class GraphQLIssuesService {
|
|
|
714
718
|
if (args.projectId && !isUuid(args.projectId)) {
|
|
715
719
|
resolveVariables.projectName = args.projectId;
|
|
716
720
|
}
|
|
717
|
-
if (args.milestoneId &&
|
|
718
|
-
typeof args.milestoneId === "string" &&
|
|
719
|
-
!isUuid(args.milestoneId)) {
|
|
721
|
+
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
720
722
|
resolveVariables.milestoneName = args.milestoneId;
|
|
721
723
|
}
|
|
722
724
|
const { __labelNames, ...updateQueryVars } = resolveVariables;
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
];
|
|
725
|
+
const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars);
|
|
726
|
+
let resolveResult;
|
|
726
727
|
if (__labelNames) {
|
|
727
728
|
const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
|
|
728
|
-
|
|
729
|
+
const [batch, labels] = await Promise.all([
|
|
730
|
+
batchPromise,
|
|
731
|
+
this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
|
|
732
|
+
]);
|
|
733
|
+
resolveResult = { ...batch, labels: labels.labels };
|
|
729
734
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (results[1]) {
|
|
733
|
-
resolveResult.labels = results[1].labels;
|
|
735
|
+
else {
|
|
736
|
+
resolveResult = await batchPromise;
|
|
734
737
|
}
|
|
735
738
|
if (!isUuid(args.id)) {
|
|
736
|
-
const
|
|
737
|
-
const resolvedIssueNodes = resolvedIssues.nodes;
|
|
739
|
+
const resolvedIssueNodes = resolveResult.issues?.nodes ?? [];
|
|
738
740
|
if (!resolvedIssueNodes.length) {
|
|
739
741
|
throw notFoundError("Issue", args.id);
|
|
740
742
|
}
|
|
741
743
|
resolvedIssueId = resolvedIssueNodes[0].id;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const issueLabels = resolvedIssueNodes[0].labels;
|
|
745
|
-
currentIssueLabels = issueLabels.nodes.map((l) => l.id);
|
|
744
|
+
issueTeamId = resolvedIssueNodes[0].team?.id;
|
|
745
|
+
currentIssueLabels = resolvedIssueNodes[0].labels.nodes.map((l) => l.id);
|
|
746
746
|
}
|
|
747
747
|
return { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult };
|
|
748
748
|
}
|
|
@@ -750,7 +750,7 @@ export class GraphQLIssuesService {
|
|
|
750
750
|
if (args.cycleId === undefined || args.cycleId === null) {
|
|
751
751
|
return args.cycleId;
|
|
752
752
|
}
|
|
753
|
-
if (
|
|
753
|
+
if (isUuid(args.cycleId)) {
|
|
754
754
|
return args.cycleId;
|
|
755
755
|
}
|
|
756
756
|
let teamIdForCycle = resolveResult.issues?.nodes?.[0]?.team?.id;
|
|
@@ -780,7 +780,7 @@ export class GraphQLIssuesService {
|
|
|
780
780
|
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
781
781
|
resolveVariables.milestoneName = args.milestoneId;
|
|
782
782
|
}
|
|
783
|
-
if (args.labelIds &&
|
|
783
|
+
if (args.labelIds && args.labelIds.length > 0) {
|
|
784
784
|
const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
|
|
785
785
|
if (nonUuidLabels.length > 0) {
|
|
786
786
|
resolveVariables.__labelNames = nonUuidLabels;
|
|
@@ -799,30 +799,32 @@ export class GraphQLIssuesService {
|
|
|
799
799
|
if (!parentId || isUuid(parentId)) {
|
|
800
800
|
return parentId;
|
|
801
801
|
}
|
|
802
|
-
const
|
|
803
|
-
const parentNodes = parentIssues?.nodes;
|
|
802
|
+
const parentNodes = resolveResult.parentIssues?.nodes;
|
|
804
803
|
if (!parentNodes?.length) {
|
|
805
804
|
throw notFoundError("Parent issue", parentId);
|
|
806
805
|
}
|
|
807
806
|
return parentNodes[0].id;
|
|
808
807
|
}
|
|
809
808
|
transformIssueData(issue) {
|
|
810
|
-
|
|
809
|
+
// `labels`/`children`/`comments` are connection fields that the
|
|
810
|
+
// fragment always selects, so production responses populate them.
|
|
811
|
+
// Test mocks pass partial objects, so read defensively to keep
|
|
812
|
+
// those test fixtures small.
|
|
811
813
|
return {
|
|
812
814
|
...this.transformIssueCoreFields(issue),
|
|
813
815
|
...this.transformIssueRelations(issue),
|
|
814
|
-
summary: issue.summary
|
|
815
|
-
? extractSummaryText(issue.summary)
|
|
816
|
-
: undefined,
|
|
816
|
+
summary: issue.summary ? extractSummaryText(issue.summary) : undefined,
|
|
817
817
|
priority: issue.priority,
|
|
818
|
-
estimate: issue.estimate
|
|
819
|
-
dueDate: issue.dueDate
|
|
820
|
-
labels: labels
|
|
818
|
+
estimate: issue.estimate ?? undefined,
|
|
819
|
+
dueDate: issue.dueDate ?? undefined,
|
|
820
|
+
labels: (issue.labels?.nodes ?? []).map((label) => ({
|
|
821
821
|
id: label.id,
|
|
822
822
|
name: label.name,
|
|
823
823
|
})),
|
|
824
824
|
...this.transformIssueHierarchy(issue),
|
|
825
|
-
comments:
|
|
825
|
+
comments: "comments" in issue && issue.comments
|
|
826
|
+
? this.transformIssueComments(issue)
|
|
827
|
+
: [],
|
|
826
828
|
createdAt: toISOStringOrNow(issue.createdAt),
|
|
827
829
|
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
828
830
|
};
|
|
@@ -833,93 +835,84 @@ export class GraphQLIssuesService {
|
|
|
833
835
|
identifier: issue.identifier,
|
|
834
836
|
url: issue.url,
|
|
835
837
|
title: issue.title,
|
|
836
|
-
description: issue.description
|
|
838
|
+
description: issue.description ?? undefined,
|
|
837
839
|
branchName: issue.branchName || undefined,
|
|
838
|
-
embeds: issue.description
|
|
839
|
-
? extractEmbeds(issue.description)
|
|
840
|
-
: undefined,
|
|
840
|
+
embeds: issue.description ? extractEmbeds(issue.description) : undefined,
|
|
841
841
|
};
|
|
842
842
|
}
|
|
843
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
844
|
return {
|
|
851
|
-
state: state
|
|
852
|
-
? { id: state.id, name: state.name }
|
|
845
|
+
state: issue.state
|
|
846
|
+
? { id: issue.state.id, name: issue.state.name }
|
|
853
847
|
: undefined,
|
|
854
|
-
assignee: assignee
|
|
848
|
+
assignee: issue.assignee
|
|
855
849
|
? {
|
|
856
|
-
id: assignee.id,
|
|
857
|
-
name: resolveUserDisplayName(assignee.id, assignee.name),
|
|
858
|
-
url: assignee.url
|
|
850
|
+
id: issue.assignee.id,
|
|
851
|
+
name: resolveUserDisplayName(issue.assignee.id, issue.assignee.name),
|
|
852
|
+
url: issue.assignee.url ?? undefined,
|
|
859
853
|
}
|
|
860
854
|
: undefined,
|
|
861
|
-
team: team
|
|
855
|
+
team: issue.team
|
|
862
856
|
? {
|
|
863
|
-
id: team.id,
|
|
864
|
-
key: team.key,
|
|
865
|
-
name: team.name,
|
|
857
|
+
id: issue.team.id,
|
|
858
|
+
key: issue.team.key,
|
|
859
|
+
name: issue.team.name,
|
|
866
860
|
}
|
|
867
861
|
: undefined,
|
|
868
|
-
project: project
|
|
869
|
-
? { id: project.id, name: project.name }
|
|
862
|
+
project: issue.project
|
|
863
|
+
? { id: issue.project.id, name: issue.project.name }
|
|
870
864
|
: undefined,
|
|
871
|
-
cycle: cycle
|
|
865
|
+
cycle: issue.cycle
|
|
872
866
|
? {
|
|
873
|
-
id: cycle.id,
|
|
874
|
-
name: cycle.name,
|
|
875
|
-
number: cycle.number,
|
|
867
|
+
id: issue.cycle.id,
|
|
868
|
+
name: issue.cycle.name ?? "",
|
|
869
|
+
number: issue.cycle.number,
|
|
876
870
|
}
|
|
877
871
|
: undefined,
|
|
878
|
-
projectMilestone:
|
|
872
|
+
projectMilestone: issue.projectMilestone
|
|
879
873
|
? {
|
|
880
|
-
id:
|
|
881
|
-
name:
|
|
882
|
-
targetDate:
|
|
874
|
+
id: issue.projectMilestone.id,
|
|
875
|
+
name: issue.projectMilestone.name,
|
|
876
|
+
targetDate: issue.projectMilestone.targetDate ?? undefined,
|
|
883
877
|
}
|
|
884
878
|
: undefined,
|
|
885
879
|
};
|
|
886
880
|
}
|
|
887
881
|
transformIssueHierarchy(issue) {
|
|
888
|
-
|
|
889
|
-
|
|
882
|
+
// children is a connection field — always present in production
|
|
883
|
+
// responses. Defensive read keeps test mocks small.
|
|
884
|
+
const childNodes = issue.children?.nodes ?? [];
|
|
890
885
|
return {
|
|
891
|
-
parentIssue: parent
|
|
886
|
+
parentIssue: issue.parent
|
|
892
887
|
? {
|
|
893
|
-
id: parent.id,
|
|
894
|
-
identifier: parent.identifier,
|
|
895
|
-
title: parent.title,
|
|
888
|
+
id: issue.parent.id,
|
|
889
|
+
identifier: issue.parent.identifier,
|
|
890
|
+
title: issue.parent.title,
|
|
896
891
|
}
|
|
897
892
|
: undefined,
|
|
898
|
-
subIssues:
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
893
|
+
subIssues: childNodes.length > 0
|
|
894
|
+
? childNodes.map((child) => ({
|
|
895
|
+
id: child.id,
|
|
896
|
+
identifier: child.identifier,
|
|
897
|
+
title: child.title,
|
|
898
|
+
}))
|
|
899
|
+
: undefined,
|
|
903
900
|
};
|
|
904
901
|
}
|
|
905
902
|
transformIssueComments(issue) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
createdAt: toISOStringOrNow(comment.createdAt),
|
|
921
|
-
updatedAt: toISOStringOrNow(comment.updatedAt),
|
|
922
|
-
};
|
|
923
|
-
}) || []);
|
|
903
|
+
return issue.comments.nodes.map((comment) => ({
|
|
904
|
+
id: comment.id,
|
|
905
|
+
body: comment.body,
|
|
906
|
+
embeds: extractEmbeds(comment.body),
|
|
907
|
+
user: comment.user
|
|
908
|
+
? {
|
|
909
|
+
id: comment.user.id,
|
|
910
|
+
name: resolveUserDisplayName(comment.user.id, comment.user.name),
|
|
911
|
+
url: comment.user.url ?? undefined,
|
|
912
|
+
}
|
|
913
|
+
: undefined,
|
|
914
|
+
createdAt: toISOStringOrNow(comment.createdAt),
|
|
915
|
+
updatedAt: toISOStringOrNow(comment.updatedAt),
|
|
916
|
+
}));
|
|
924
917
|
}
|
|
925
918
|
}
|