@enrichlayer/el-linear 1.9.0 → 1.15.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 +139 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +22 -33
- package/dist/commands/config.js +178 -5
- 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/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/description.js +2 -6
- 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 +44 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +269 -309
- package/dist/commands/labels.js +15 -24
- package/dist/commands/profile.js +1 -0
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +157 -44
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +75 -8
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +49 -33
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +5 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +11 -5
- package/dist/main.js +13 -1
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +55 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +62 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +58 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +481 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +51 -10
- package/dist/queries/issues.js +147 -5
- package/dist/queries/labels-types.d.ts +65 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +92 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +76 -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 +85 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +102 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +62 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +21 -3
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +30 -34
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- 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 +112 -46
- package/dist/utils/graphql-issues-service.js +398 -206
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +82 -2
- package/dist/utils/output.js +76 -11
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +53 -7
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
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_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_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";
|
|
@@ -14,7 +14,7 @@ function extractSummaryText(summary) {
|
|
|
14
14
|
}
|
|
15
15
|
const parts = [];
|
|
16
16
|
const walk = (node) => {
|
|
17
|
-
if (node.text) {
|
|
17
|
+
if (typeof node.text === "string") {
|
|
18
18
|
parts.push(node.text);
|
|
19
19
|
}
|
|
20
20
|
if (Array.isArray(node.content)) {
|
|
@@ -34,15 +34,12 @@ export class GraphQLIssuesService {
|
|
|
34
34
|
this.linearService = linearService;
|
|
35
35
|
}
|
|
36
36
|
async getIssues(limit = 25) {
|
|
37
|
-
const result = await this.graphQLService.rawRequest(GET_ISSUES_QUERY, {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
const issues = result.issues;
|
|
42
|
-
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) {
|
|
43
40
|
return [];
|
|
44
41
|
}
|
|
45
|
-
return
|
|
42
|
+
return nodes.map((issue) => this.transformIssueData(issue));
|
|
46
43
|
}
|
|
47
44
|
async getIssueById(issueId) {
|
|
48
45
|
let issueData;
|
|
@@ -55,12 +52,8 @@ export class GraphQLIssuesService {
|
|
|
55
52
|
}
|
|
56
53
|
else {
|
|
57
54
|
const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
|
|
58
|
-
const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, {
|
|
59
|
-
|
|
60
|
-
number: issueNumber,
|
|
61
|
-
});
|
|
62
|
-
const issues = result.issues;
|
|
63
|
-
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;
|
|
64
57
|
if (!nodes?.length) {
|
|
65
58
|
throw notFoundError("Issue", issueId);
|
|
66
59
|
}
|
|
@@ -68,44 +61,116 @@ export class GraphQLIssuesService {
|
|
|
68
61
|
}
|
|
69
62
|
return this.transformIssueData(issueData);
|
|
70
63
|
}
|
|
64
|
+
async startIssue(issueId) {
|
|
65
|
+
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
66
|
+
const context = await this.graphQLService.rawRequest(GET_ISSUE_START_CONTEXT_QUERY, { id: resolvedIssueId });
|
|
67
|
+
const issue = context.issue;
|
|
68
|
+
if (!issue) {
|
|
69
|
+
throw notFoundError("Issue", issueId);
|
|
70
|
+
}
|
|
71
|
+
const previousState = issue.state
|
|
72
|
+
? {
|
|
73
|
+
id: issue.state.id,
|
|
74
|
+
name: issue.state.name,
|
|
75
|
+
type: issue.state.type,
|
|
76
|
+
}
|
|
77
|
+
: undefined;
|
|
78
|
+
if (previousState &&
|
|
79
|
+
["started", "completed", "canceled"].includes(previousState.type)) {
|
|
80
|
+
return {
|
|
81
|
+
issue: await this.getIssueById(resolvedIssueId),
|
|
82
|
+
previousState,
|
|
83
|
+
started: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!issue.team?.id) {
|
|
87
|
+
throw new Error(`Issue ${issue.identifier} has no team; cannot start it.`);
|
|
88
|
+
}
|
|
89
|
+
const statuses = await this.graphQLService.rawRequest(TEAM_STARTED_STATUSES_QUERY, { teamId: issue.team.id });
|
|
90
|
+
const startedStatus = statuses.team?.states.nodes
|
|
91
|
+
.slice()
|
|
92
|
+
.sort((a, b) => a.position - b.position)[0];
|
|
93
|
+
if (!startedStatus) {
|
|
94
|
+
throw new Error(`Team ${issue.team.key} has no workflow status of type "started".`);
|
|
95
|
+
}
|
|
96
|
+
const updated = await this.updateIssue({ id: resolvedIssueId, statusId: startedStatus.id }, "adding");
|
|
97
|
+
return {
|
|
98
|
+
issue: updated,
|
|
99
|
+
previousState,
|
|
100
|
+
started: true,
|
|
101
|
+
targetState: { id: startedStatus.id, name: startedStatus.name },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
71
104
|
async updateIssue(args, labelMode = "overwriting") {
|
|
72
|
-
|
|
105
|
+
// Normalize URL/slug-id --project inputs to UUIDs before the batch
|
|
106
|
+
// resolver runs; see comment on `withNormalizedProjectId`.
|
|
107
|
+
const normalizedArgs = await this.withNormalizedProjectId(args);
|
|
108
|
+
const { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult } = await this.resolveUpdateContext(normalizedArgs);
|
|
73
109
|
let teamIdForLabels = issueTeamId;
|
|
74
|
-
if (!teamIdForLabels &&
|
|
110
|
+
if (!teamIdForLabels && normalizedArgs.labelIds && resolvedIssueId) {
|
|
75
111
|
teamIdForLabels = await this.fetchIssueTeamId(resolvedIssueId);
|
|
76
112
|
}
|
|
77
|
-
const finalLabelIds = await this.resolveLabelsWithMode(
|
|
78
|
-
const finalProjectId =
|
|
79
|
-
? this.resolveProjectId(
|
|
113
|
+
const finalLabelIds = await this.resolveLabelsWithMode(normalizedArgs.labelIds, resolveResult, labelMode, currentIssueLabels, teamIdForLabels, normalizedArgs.teamId);
|
|
114
|
+
const finalProjectId = normalizedArgs.projectId
|
|
115
|
+
? this.resolveProjectId(normalizedArgs.projectId, resolveResult)
|
|
80
116
|
: undefined;
|
|
81
|
-
const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(
|
|
82
|
-
const finalMilestoneId = this.resolveMilestoneId(
|
|
83
|
-
const finalCycleId = await this.resolveCycleIdForUpdate(
|
|
84
|
-
const resolvedStatusId = await this.resolveStatusIdForUpdate(
|
|
117
|
+
const { projectMilestoneNodes, issueProjectMilestoneNodes } = this.extractMilestoneNodes(normalizedArgs, resolveResult);
|
|
118
|
+
const finalMilestoneId = this.resolveMilestoneId(normalizedArgs.milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes);
|
|
119
|
+
const finalCycleId = await this.resolveCycleIdForUpdate(normalizedArgs, resolveResult, resolvedIssueId);
|
|
120
|
+
const resolvedStatusId = await this.resolveStatusIdForUpdate(normalizedArgs, resolvedIssueId);
|
|
85
121
|
// Resolve assignee in place — `buildUpdateInput` reads it
|
|
86
122
|
// directly. Match the createIssue pattern.
|
|
87
|
-
let assigneeId =
|
|
123
|
+
let assigneeId = normalizedArgs.assigneeId;
|
|
88
124
|
if (assigneeId && !isUuid(assigneeId)) {
|
|
89
125
|
assigneeId = await this.linearService.resolveUserId(assigneeId);
|
|
90
126
|
}
|
|
91
|
-
|
|
127
|
+
let delegateId = normalizedArgs.delegateId;
|
|
128
|
+
if (delegateId && !isUuid(delegateId)) {
|
|
129
|
+
delegateId = await this.linearService.resolveUserId(delegateId);
|
|
130
|
+
}
|
|
131
|
+
const updateInput = this.buildUpdateInput({ ...normalizedArgs, assigneeId, delegateId }, {
|
|
92
132
|
statusId: resolvedStatusId,
|
|
93
133
|
projectId: finalProjectId,
|
|
94
134
|
labelIds: finalLabelIds,
|
|
95
135
|
milestoneId: finalMilestoneId,
|
|
96
136
|
cycleId: finalCycleId,
|
|
97
137
|
});
|
|
98
|
-
return this.executeUpdateMutation(resolvedIssueId,
|
|
138
|
+
return this.executeUpdateMutation(resolvedIssueId, normalizedArgs.id, updateInput);
|
|
139
|
+
}
|
|
140
|
+
async archiveIssue(issueId) {
|
|
141
|
+
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
142
|
+
const result = await this.graphQLService.rawRequest(ARCHIVE_ISSUE_MUTATION, { id: resolvedIssueId });
|
|
143
|
+
const payload = result.issueArchive;
|
|
144
|
+
if (!payload.success) {
|
|
145
|
+
throw new Error(`Failed to archive issue ${issueId}`);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: resolvedIssueId,
|
|
149
|
+
entity: payload.entity ?? undefined,
|
|
150
|
+
lastSyncId: payload.lastSyncId,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async deleteIssue(issueId, options = {}) {
|
|
154
|
+
const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
|
|
155
|
+
const result = await this.graphQLService.rawRequest(DELETE_ISSUE_MUTATION, {
|
|
156
|
+
id: resolvedIssueId,
|
|
157
|
+
permanentlyDelete: Boolean(options.permanentlyDelete),
|
|
158
|
+
});
|
|
159
|
+
const payload = result.issueDelete;
|
|
160
|
+
if (!payload.success) {
|
|
161
|
+
throw new Error(`Failed to delete issue ${issueId}`);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id: resolvedIssueId,
|
|
165
|
+
entity: payload.entity ?? undefined,
|
|
166
|
+
lastSyncId: payload.lastSyncId,
|
|
167
|
+
};
|
|
99
168
|
}
|
|
100
169
|
extractMilestoneNodes(args, resolveResult) {
|
|
101
|
-
const projectNodes = resolveResult.projects?.nodes;
|
|
102
170
|
const projectMilestoneNodes = args.projectId
|
|
103
|
-
?
|
|
171
|
+
? resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes
|
|
104
172
|
: undefined;
|
|
105
|
-
const
|
|
106
|
-
?.nodes;
|
|
107
|
-
const issueProjectMilestoneNodes = issueNodes?.[0]?.project
|
|
108
|
-
?.projectMilestones?.nodes;
|
|
173
|
+
const issueProjectMilestoneNodes = resolveResult.issues?.nodes?.[0]?.project?.projectMilestones.nodes;
|
|
109
174
|
return { projectMilestoneNodes, issueProjectMilestoneNodes };
|
|
110
175
|
}
|
|
111
176
|
async executeUpdateMutation(resolvedIssueId, originalId, updateInput) {
|
|
@@ -136,34 +201,72 @@ export class GraphQLIssuesService {
|
|
|
136
201
|
return this.transformIssueData(issueUpdate.issue);
|
|
137
202
|
}
|
|
138
203
|
async createIssue(args) {
|
|
139
|
-
|
|
204
|
+
// Pre-resolve URL/slug-id forms of --project to a UUID so the batch
|
|
205
|
+
// resolver below (which uses a `name eqIgnoreCase` filter) can skip
|
|
206
|
+
// the project lookup entirely. Plain name inputs flow through unchanged
|
|
207
|
+
// and get resolved by the batch query in a single round-trip.
|
|
208
|
+
const normalizedArgs = await this.withNormalizedProjectId(args);
|
|
209
|
+
const resolveVariables = this.buildCreateResolveVariables(normalizedArgs);
|
|
140
210
|
let resolveResult = {};
|
|
141
211
|
if (Object.keys(resolveVariables).length > 0) {
|
|
142
212
|
const { __labelNames, ...queryVars } = resolveVariables;
|
|
143
|
-
const
|
|
144
|
-
this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars),
|
|
145
|
-
];
|
|
213
|
+
const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_CREATE_QUERY, queryVars);
|
|
146
214
|
if (__labelNames) {
|
|
147
215
|
const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
|
|
148
|
-
|
|
216
|
+
const [batch, labels] = await Promise.all([
|
|
217
|
+
batchPromise,
|
|
218
|
+
this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
|
|
219
|
+
]);
|
|
220
|
+
resolveResult = {
|
|
221
|
+
...this.foldCreateBatchResult(batch),
|
|
222
|
+
labels: labels.labels,
|
|
223
|
+
};
|
|
149
224
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (results[1]) {
|
|
153
|
-
resolveResult.labels = results[1].labels;
|
|
225
|
+
else {
|
|
226
|
+
resolveResult = this.foldCreateBatchResult(await batchPromise);
|
|
154
227
|
}
|
|
155
228
|
}
|
|
156
|
-
const resolved = await this.resolveCreateFields(
|
|
229
|
+
const resolved = await this.resolveCreateFields(normalizedArgs, resolveResult);
|
|
157
230
|
// Mutate-the-input feels gross but matches the existing pattern
|
|
158
231
|
// — `resolved` doesn't carry assigneeId, so the buildCreateInput
|
|
159
232
|
// step reads `args.assigneeId` directly. Resolve in place.
|
|
160
|
-
let assigneeId =
|
|
233
|
+
let assigneeId = normalizedArgs.assigneeId;
|
|
161
234
|
if (assigneeId && !isUuid(assigneeId)) {
|
|
162
235
|
assigneeId = await this.linearService.resolveUserId(assigneeId);
|
|
163
236
|
}
|
|
164
|
-
|
|
237
|
+
let delegateId = normalizedArgs.delegateId;
|
|
238
|
+
if (delegateId && !isUuid(delegateId)) {
|
|
239
|
+
delegateId = await this.linearService.resolveUserId(delegateId);
|
|
240
|
+
}
|
|
241
|
+
const createInput = this.buildCreateInput({ ...normalizedArgs, assigneeId, delegateId }, resolved);
|
|
165
242
|
return this.executeCreateMutation(createInput);
|
|
166
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Folds the `@include`-gated `projectsByName` / `projectsById` aliases
|
|
246
|
+
* from a create batch response into the single `projects` field the
|
|
247
|
+
* field-resolution helpers expect. At most one project alias is ever
|
|
248
|
+
* present, since a `--project` input is either a name or a UUID.
|
|
249
|
+
*/
|
|
250
|
+
foldCreateBatchResult(batch) {
|
|
251
|
+
return {
|
|
252
|
+
teams: batch.teams,
|
|
253
|
+
projects: batch.projectsByName ?? batch.projectsById,
|
|
254
|
+
milestones: batch.milestones,
|
|
255
|
+
parentIssues: batch.parentIssues,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Folds the `@include`-gated `projectsByName` / `projectsById` aliases
|
|
260
|
+
* from an update batch response into the single `projects` field the
|
|
261
|
+
* field-resolution helpers expect. See `foldCreateBatchResult`.
|
|
262
|
+
*/
|
|
263
|
+
foldUpdateBatchResult(batch) {
|
|
264
|
+
return {
|
|
265
|
+
projects: batch.projectsByName ?? batch.projectsById,
|
|
266
|
+
milestones: batch.milestones,
|
|
267
|
+
issues: batch.issues,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
167
270
|
async resolveCreateFields(args, resolveResult) {
|
|
168
271
|
let teamId = args.teamId
|
|
169
272
|
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
@@ -221,40 +324,65 @@ export class GraphQLIssuesService {
|
|
|
221
324
|
if (args.teamId && !isUuid(args.teamId)) {
|
|
222
325
|
Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
|
|
223
326
|
}
|
|
224
|
-
|
|
225
|
-
|
|
327
|
+
// Only the `id` arm carries a name/UUID for resolution; `none` and
|
|
328
|
+
// `undefined` skip the resolve.
|
|
329
|
+
const projectIdArg = args.project?.kind === "id" ? args.project.id : undefined;
|
|
330
|
+
if (projectIdArg && !isUuid(projectIdArg)) {
|
|
331
|
+
resolveVariables.projectName = projectIdArg;
|
|
332
|
+
resolveVariables.hasProjectName = true;
|
|
226
333
|
}
|
|
227
334
|
if (args.assigneeId &&
|
|
228
335
|
!isUuid(args.assigneeId) &&
|
|
229
336
|
args.assigneeId.includes("@")) {
|
|
230
337
|
resolveVariables.assigneeEmail = args.assigneeId;
|
|
231
338
|
}
|
|
339
|
+
if (args.delegateId &&
|
|
340
|
+
!isUuid(args.delegateId) &&
|
|
341
|
+
args.delegateId.includes("@")) {
|
|
342
|
+
resolveVariables.delegateEmail = args.delegateId;
|
|
343
|
+
}
|
|
232
344
|
let resolveResult = {};
|
|
233
345
|
if (Object.keys(resolveVariables).length > 0) {
|
|
234
|
-
resolveResult =
|
|
346
|
+
resolveResult =
|
|
347
|
+
await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
|
|
235
348
|
}
|
|
236
349
|
const finalTeamId = args.teamId
|
|
237
350
|
? await this.resolveTeamId(args.teamId, resolveResult)
|
|
238
351
|
: undefined;
|
|
239
|
-
const finalProjectId =
|
|
240
|
-
? this.resolveProjectId(
|
|
352
|
+
const finalProjectId = projectIdArg
|
|
353
|
+
? this.resolveProjectId(projectIdArg, resolveResult)
|
|
241
354
|
: undefined;
|
|
242
355
|
const finalAssigneeId = this.resolveAssigneeId(args.assigneeId, resolveResult);
|
|
356
|
+
const finalDelegateId = await this.resolveDelegateId(args.delegateId, resolveResult);
|
|
357
|
+
// Map the public discriminant back to the internal filter shape:
|
|
358
|
+
// `{ kind: "id", id }` → `{ kind: "id", id: <resolvedUuid> }`
|
|
359
|
+
// `{ kind: "none" }` → pass through
|
|
360
|
+
// undefined → undefined (filter off)
|
|
361
|
+
// Reused by both the full-text and structured search paths so
|
|
362
|
+
// `--no-project` (`{ kind: "none" }`) participates in both — pre-fix,
|
|
363
|
+
// the full-text branch silently dropped `noProject` because only
|
|
364
|
+
// the resolved `projectId` was forwarded.
|
|
365
|
+
const projectFilter = args.project?.kind === "id" && finalProjectId
|
|
366
|
+
? { kind: "id", id: finalProjectId }
|
|
367
|
+
: args.project?.kind === "none"
|
|
368
|
+
? { kind: "none" }
|
|
369
|
+
: undefined;
|
|
243
370
|
const limit = args.limit ?? 10;
|
|
244
371
|
if (args.query) {
|
|
245
372
|
const searchResult = await this.graphQLService.rawRequest(SEARCH_ISSUES_QUERY, {
|
|
246
373
|
term: args.query,
|
|
247
374
|
first: limit,
|
|
248
375
|
});
|
|
249
|
-
const
|
|
250
|
-
if (!
|
|
376
|
+
const nodes = searchResult.searchIssues?.nodes;
|
|
377
|
+
if (!nodes?.length) {
|
|
251
378
|
return [];
|
|
252
379
|
}
|
|
253
|
-
const results =
|
|
380
|
+
const results = nodes.map((issue) => this.transformIssueData(issue));
|
|
254
381
|
return this.applySearchFilters(results, {
|
|
255
382
|
teamId: finalTeamId,
|
|
256
383
|
assigneeId: finalAssigneeId,
|
|
257
|
-
|
|
384
|
+
delegateId: finalDelegateId,
|
|
385
|
+
project: projectFilter,
|
|
258
386
|
status: args.status,
|
|
259
387
|
labelNames: args.labelNames,
|
|
260
388
|
priority: args.priority,
|
|
@@ -263,8 +391,8 @@ export class GraphQLIssuesService {
|
|
|
263
391
|
const filter = this.buildSearchFilter({
|
|
264
392
|
teamId: finalTeamId,
|
|
265
393
|
assigneeId: finalAssigneeId,
|
|
266
|
-
|
|
267
|
-
|
|
394
|
+
delegateId: finalDelegateId,
|
|
395
|
+
project: projectFilter,
|
|
268
396
|
status: args.status,
|
|
269
397
|
labelNames: args.labelNames,
|
|
270
398
|
priority: args.priority,
|
|
@@ -285,8 +413,7 @@ export class GraphQLIssuesService {
|
|
|
285
413
|
if (isUuid(teamId)) {
|
|
286
414
|
return teamId;
|
|
287
415
|
}
|
|
288
|
-
const
|
|
289
|
-
const teamNodes = teams?.nodes;
|
|
416
|
+
const teamNodes = resolveResult.teams?.nodes;
|
|
290
417
|
const resolvedTeam = teamNodes?.[0];
|
|
291
418
|
if (resolvedTeam &&
|
|
292
419
|
(resolvedTeam.key.toUpperCase() === teamId.toUpperCase() ||
|
|
@@ -298,10 +425,21 @@ export class GraphQLIssuesService {
|
|
|
298
425
|
}
|
|
299
426
|
resolveProjectId(projectId, resolveResult) {
|
|
300
427
|
if (isUuid(projectId)) {
|
|
428
|
+
// The create/update batch queries fetch a `projectsById` block for a
|
|
429
|
+
// UUID `--project` (folded into `resolveResult.projects`). An empty
|
|
430
|
+
// `nodes` array means the block ran but matched nothing — i.e. the
|
|
431
|
+
// UUID doesn't exist. Fail clearly here instead of passing a dead
|
|
432
|
+
// UUID to the mutation, which would surface a vague server error.
|
|
433
|
+
// When no block was fetched (`projects` undefined — e.g. the search
|
|
434
|
+
// path has no `projectsById` arm) there's nothing to validate, so
|
|
435
|
+
// the UUID passes through unchanged.
|
|
436
|
+
const fetched = resolveResult.projects?.nodes;
|
|
437
|
+
if (fetched && fetched.length === 0) {
|
|
438
|
+
throw notFoundError("Project", projectId);
|
|
439
|
+
}
|
|
301
440
|
return projectId;
|
|
302
441
|
}
|
|
303
|
-
const
|
|
304
|
-
const projectNodes = projects?.nodes;
|
|
442
|
+
const projectNodes = resolveResult.projects?.nodes;
|
|
305
443
|
if (!projectNodes?.length) {
|
|
306
444
|
throw notFoundError("Project", projectId);
|
|
307
445
|
}
|
|
@@ -312,13 +450,11 @@ export class GraphQLIssuesService {
|
|
|
312
450
|
* If not, auto-switch when the project belongs to exactly one team.
|
|
313
451
|
*/
|
|
314
452
|
validateProjectTeam(teamId, projectInput, resolveResult) {
|
|
315
|
-
const
|
|
316
|
-
const projectNode = projects?.nodes?.[0];
|
|
453
|
+
const projectNode = resolveResult.projects?.nodes?.[0];
|
|
317
454
|
if (!projectNode) {
|
|
318
455
|
return teamId;
|
|
319
456
|
}
|
|
320
|
-
const
|
|
321
|
-
const teamNodes = teamsConn?.nodes;
|
|
457
|
+
const teamNodes = projectNode.teams?.nodes;
|
|
322
458
|
if (!teamNodes?.length) {
|
|
323
459
|
return teamId;
|
|
324
460
|
}
|
|
@@ -346,8 +482,7 @@ export class GraphQLIssuesService {
|
|
|
346
482
|
return labelIds;
|
|
347
483
|
}
|
|
348
484
|
const resolvedLabels = [];
|
|
349
|
-
const
|
|
350
|
-
const labelNodes = labels?.nodes;
|
|
485
|
+
const labelNodes = resolveResult.labels?.nodes;
|
|
351
486
|
for (const labelIdOrName of labelIds) {
|
|
352
487
|
if (isUuid(labelIdOrName)) {
|
|
353
488
|
resolvedLabels.push(labelIdOrName);
|
|
@@ -357,10 +492,7 @@ export class GraphQLIssuesService {
|
|
|
357
492
|
const candidates = labelNodes?.filter((l) => l.name.toLowerCase() === lowerName);
|
|
358
493
|
let label;
|
|
359
494
|
if (candidates && candidates.length > 1 && teamId) {
|
|
360
|
-
label = candidates.find((l) =>
|
|
361
|
-
const team = l.team;
|
|
362
|
-
return team?.id === teamId;
|
|
363
|
-
});
|
|
495
|
+
label = candidates.find((l) => l.team?.id === teamId);
|
|
364
496
|
}
|
|
365
497
|
if (!label) {
|
|
366
498
|
label = candidates?.[0];
|
|
@@ -402,12 +534,10 @@ export class GraphQLIssuesService {
|
|
|
402
534
|
}
|
|
403
535
|
const result = await this.graphQLService.rawRequest(CREATE_LABEL_MUTATION, { input });
|
|
404
536
|
const created = result.issueLabelCreate;
|
|
405
|
-
if (created
|
|
406
|
-
const
|
|
407
|
-
const teamInfo = labelData.team;
|
|
408
|
-
const teamKey = teamInfo?.key ?? "";
|
|
537
|
+
if (created.success && created.issueLabel) {
|
|
538
|
+
const teamKey = created.issueLabel.team?.key ?? "";
|
|
409
539
|
logger.error(`Auto-created label "${name}" on team ${teamKey}`);
|
|
410
|
-
return
|
|
540
|
+
return created.issueLabel.id;
|
|
411
541
|
}
|
|
412
542
|
return null;
|
|
413
543
|
}
|
|
@@ -435,37 +565,27 @@ export class GraphQLIssuesService {
|
|
|
435
565
|
// Only fall back to first team if there's exactly one (unambiguous).
|
|
436
566
|
// With multiple teams, returning an arbitrary one produces misleading error hints.
|
|
437
567
|
const topLevelNodes = resolveResult.teams?.nodes;
|
|
438
|
-
return topLevelNodes?.length === 1
|
|
439
|
-
? topLevelNodes[0].key
|
|
440
|
-
: undefined;
|
|
568
|
+
return topLevelNodes?.length === 1 ? topLevelNodes[0].key : undefined;
|
|
441
569
|
}
|
|
442
570
|
collectTeamNodes(resolveResult) {
|
|
443
571
|
const nodes = [];
|
|
444
572
|
const seen = new Set();
|
|
445
|
-
const
|
|
446
|
-
const topNodes = topTeams?.nodes;
|
|
573
|
+
const topNodes = resolveResult.teams?.nodes;
|
|
447
574
|
if (topNodes) {
|
|
448
575
|
for (const t of topNodes) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
seen.add(id);
|
|
576
|
+
if (!seen.has(t.id)) {
|
|
577
|
+
seen.add(t.id);
|
|
452
578
|
nodes.push(t);
|
|
453
579
|
}
|
|
454
580
|
}
|
|
455
581
|
}
|
|
456
582
|
// Also check project teams (which are always fetched when --project is used)
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
for (const t of projectTeamNodes) {
|
|
464
|
-
const id = t.id;
|
|
465
|
-
if (!seen.has(id)) {
|
|
466
|
-
seen.add(id);
|
|
467
|
-
nodes.push(t);
|
|
468
|
-
}
|
|
583
|
+
const projectTeamNodes = resolveResult.projects?.nodes?.[0]?.teams?.nodes;
|
|
584
|
+
if (projectTeamNodes) {
|
|
585
|
+
for (const t of projectTeamNodes) {
|
|
586
|
+
if (!seen.has(t.id)) {
|
|
587
|
+
seen.add(t.id);
|
|
588
|
+
nodes.push(t);
|
|
469
589
|
}
|
|
470
590
|
}
|
|
471
591
|
}
|
|
@@ -488,8 +608,7 @@ export class GraphQLIssuesService {
|
|
|
488
608
|
resolved = issueMilestone.id;
|
|
489
609
|
}
|
|
490
610
|
}
|
|
491
|
-
const
|
|
492
|
-
const milestoneNodes = milestones?.nodes;
|
|
611
|
+
const milestoneNodes = resolveResult.milestones?.nodes;
|
|
493
612
|
if (!isUuid(resolved) && milestoneNodes?.length) {
|
|
494
613
|
resolved = milestoneNodes[0].id;
|
|
495
614
|
}
|
|
@@ -503,19 +622,14 @@ export class GraphQLIssuesService {
|
|
|
503
622
|
return milestoneId;
|
|
504
623
|
}
|
|
505
624
|
let resolved;
|
|
506
|
-
const
|
|
507
|
-
const createProjectNodes = createProjects?.nodes;
|
|
508
|
-
const createProjectMilestones = createProjectNodes?.[0]
|
|
509
|
-
?.projectMilestones;
|
|
510
|
-
const createProjectMilestoneNodes = createProjectMilestones?.nodes;
|
|
625
|
+
const createProjectMilestoneNodes = resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes;
|
|
511
626
|
if (createProjectMilestoneNodes) {
|
|
512
627
|
const projectMilestone = createProjectMilestoneNodes.find((m) => m.name === milestoneId);
|
|
513
628
|
if (projectMilestone) {
|
|
514
629
|
resolved = projectMilestone.id;
|
|
515
630
|
}
|
|
516
631
|
}
|
|
517
|
-
const
|
|
518
|
-
const createMilestoneNodes = createMilestones?.nodes;
|
|
632
|
+
const createMilestoneNodes = resolveResult.milestones?.nodes;
|
|
519
633
|
if (!resolved && createMilestoneNodes?.length) {
|
|
520
634
|
resolved = createMilestoneNodes[0].id;
|
|
521
635
|
}
|
|
@@ -528,12 +642,8 @@ export class GraphQLIssuesService {
|
|
|
528
642
|
return resolved;
|
|
529
643
|
}
|
|
530
644
|
async fetchIssueTeamId(issueId) {
|
|
531
|
-
const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, {
|
|
532
|
-
|
|
533
|
-
});
|
|
534
|
-
const fetchedIssue = result.issue;
|
|
535
|
-
const fetchedTeam = fetchedIssue?.team;
|
|
536
|
-
return fetchedTeam?.id;
|
|
645
|
+
const result = await this.graphQLService.rawRequest(GET_ISSUE_TEAM_QUERY, { issueId });
|
|
646
|
+
return result.issue?.team?.id;
|
|
537
647
|
}
|
|
538
648
|
buildCreateInput(args, resolved) {
|
|
539
649
|
const input = {};
|
|
@@ -553,6 +663,9 @@ export class GraphQLIssuesService {
|
|
|
553
663
|
if (args.assigneeId) {
|
|
554
664
|
input.assigneeId = args.assigneeId;
|
|
555
665
|
}
|
|
666
|
+
if (args.delegateId) {
|
|
667
|
+
input.delegateId = args.delegateId;
|
|
668
|
+
}
|
|
556
669
|
if (args.priority !== undefined) {
|
|
557
670
|
input.priority = args.priority;
|
|
558
671
|
}
|
|
@@ -608,6 +721,9 @@ export class GraphQLIssuesService {
|
|
|
608
721
|
if (args.assigneeId !== undefined) {
|
|
609
722
|
input.assigneeId = args.assigneeId;
|
|
610
723
|
}
|
|
724
|
+
if (args.delegateId !== undefined) {
|
|
725
|
+
input.delegateId = args.delegateId;
|
|
726
|
+
}
|
|
611
727
|
if (resolved.projectId !== undefined) {
|
|
612
728
|
input.projectId = resolved.projectId;
|
|
613
729
|
}
|
|
@@ -646,8 +762,22 @@ export class GraphQLIssuesService {
|
|
|
646
762
|
if (filters.assigneeId) {
|
|
647
763
|
filtered = filtered.filter((issue) => issue.assignee?.id === filters.assigneeId);
|
|
648
764
|
}
|
|
649
|
-
if (filters.
|
|
650
|
-
filtered = filtered.filter((issue) => issue.
|
|
765
|
+
if (filters.delegateId) {
|
|
766
|
+
filtered = filtered.filter((issue) => issue.delegate?.id === filters.delegateId);
|
|
767
|
+
}
|
|
768
|
+
if (filters.project) {
|
|
769
|
+
switch (filters.project.kind) {
|
|
770
|
+
case "id": {
|
|
771
|
+
const id = filters.project.id;
|
|
772
|
+
filtered = filtered.filter((issue) => issue.project?.id === id);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case "none":
|
|
776
|
+
filtered = filtered.filter((issue) => !issue.project);
|
|
777
|
+
break;
|
|
778
|
+
default:
|
|
779
|
+
return filters.project;
|
|
780
|
+
}
|
|
651
781
|
}
|
|
652
782
|
if (filters.status && filters.status.length > 0) {
|
|
653
783
|
filtered = filtered.filter((issue) => filters.status.includes(issue.state?.name ?? ""));
|
|
@@ -672,11 +802,20 @@ export class GraphQLIssuesService {
|
|
|
672
802
|
if (filters.assigneeId) {
|
|
673
803
|
filter.assignee = { id: { eq: filters.assigneeId } };
|
|
674
804
|
}
|
|
675
|
-
if (filters.
|
|
676
|
-
filter.
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
805
|
+
if (filters.delegateId) {
|
|
806
|
+
filter.delegate = { id: { eq: filters.delegateId } };
|
|
807
|
+
}
|
|
808
|
+
if (filters.project) {
|
|
809
|
+
switch (filters.project.kind) {
|
|
810
|
+
case "id":
|
|
811
|
+
filter.project = { id: { eq: filters.project.id } };
|
|
812
|
+
break;
|
|
813
|
+
case "none":
|
|
814
|
+
filter.project = { null: true };
|
|
815
|
+
break;
|
|
816
|
+
default:
|
|
817
|
+
return filters.project;
|
|
818
|
+
}
|
|
680
819
|
}
|
|
681
820
|
if (filters.status && filters.status.length > 0) {
|
|
682
821
|
filter.state = { name: { in: filters.status } };
|
|
@@ -704,13 +843,25 @@ export class GraphQLIssuesService {
|
|
|
704
843
|
if (!assigneeId || isUuid(assigneeId) || !assigneeId.includes("@")) {
|
|
705
844
|
return assigneeId;
|
|
706
845
|
}
|
|
707
|
-
const
|
|
708
|
-
const userNodes = users?.nodes;
|
|
846
|
+
const userNodes = resolveResult.users?.nodes;
|
|
709
847
|
if (!userNodes?.length) {
|
|
710
848
|
throw notFoundError("User", assigneeId);
|
|
711
849
|
}
|
|
712
850
|
return userNodes[0].id;
|
|
713
851
|
}
|
|
852
|
+
async resolveDelegateId(delegateId, resolveResult) {
|
|
853
|
+
if (!delegateId || isUuid(delegateId)) {
|
|
854
|
+
return delegateId;
|
|
855
|
+
}
|
|
856
|
+
if (!delegateId.includes("@")) {
|
|
857
|
+
return this.linearService.resolveUserId(delegateId);
|
|
858
|
+
}
|
|
859
|
+
const userNodes = resolveResult.delegates?.nodes;
|
|
860
|
+
if (!userNodes?.length) {
|
|
861
|
+
throw notFoundError("Delegate", delegateId);
|
|
862
|
+
}
|
|
863
|
+
return userNodes[0].id;
|
|
864
|
+
}
|
|
714
865
|
async resolveUpdateContext(args) {
|
|
715
866
|
let resolvedIssueId = args.id;
|
|
716
867
|
let issueTeamId;
|
|
@@ -727,36 +878,49 @@ export class GraphQLIssuesService {
|
|
|
727
878
|
resolveVariables.__labelNames = nonUuidLabels;
|
|
728
879
|
}
|
|
729
880
|
}
|
|
730
|
-
if (args.projectId
|
|
731
|
-
|
|
881
|
+
if (args.projectId) {
|
|
882
|
+
// A UUID resolves by `id` so the project's milestones are still
|
|
883
|
+
// fetched for `--project-milestone` name resolution; a name
|
|
884
|
+
// resolves by `name`. The `has*` flags gate the `@include`d
|
|
885
|
+
// blocks so an unset input never triggers a null no-op filter.
|
|
886
|
+
if (isUuid(args.projectId)) {
|
|
887
|
+
resolveVariables.projectId = args.projectId;
|
|
888
|
+
resolveVariables.hasProjectId = true;
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
resolveVariables.projectName = args.projectId;
|
|
892
|
+
resolveVariables.hasProjectName = true;
|
|
893
|
+
}
|
|
732
894
|
}
|
|
733
895
|
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
734
896
|
resolveVariables.milestoneName = args.milestoneId;
|
|
897
|
+
resolveVariables.hasMilestoneName = true;
|
|
735
898
|
}
|
|
736
899
|
const { __labelNames, ...updateQueryVars } = resolveVariables;
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
];
|
|
900
|
+
const batchPromise = this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars);
|
|
901
|
+
let resolveResult;
|
|
740
902
|
if (__labelNames) {
|
|
741
903
|
const labelQuery = buildResolveLabelsByNameQuery(__labelNames);
|
|
742
|
-
|
|
904
|
+
const [batch, labels] = await Promise.all([
|
|
905
|
+
batchPromise,
|
|
906
|
+
this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables),
|
|
907
|
+
]);
|
|
908
|
+
resolveResult = {
|
|
909
|
+
...this.foldUpdateBatchResult(batch),
|
|
910
|
+
labels: labels.labels,
|
|
911
|
+
};
|
|
743
912
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (results[1]) {
|
|
747
|
-
resolveResult.labels = results[1].labels;
|
|
913
|
+
else {
|
|
914
|
+
resolveResult = this.foldUpdateBatchResult(await batchPromise);
|
|
748
915
|
}
|
|
749
916
|
if (!isUuid(args.id)) {
|
|
750
|
-
const
|
|
751
|
-
const resolvedIssueNodes = resolvedIssues.nodes;
|
|
917
|
+
const resolvedIssueNodes = resolveResult.issues?.nodes ?? [];
|
|
752
918
|
if (!resolvedIssueNodes.length) {
|
|
753
919
|
throw notFoundError("Issue", args.id);
|
|
754
920
|
}
|
|
755
921
|
resolvedIssueId = resolvedIssueNodes[0].id;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const issueLabels = resolvedIssueNodes[0].labels;
|
|
759
|
-
currentIssueLabels = issueLabels.nodes.map((l) => l.id);
|
|
922
|
+
issueTeamId = resolvedIssueNodes[0].team?.id;
|
|
923
|
+
currentIssueLabels = resolvedIssueNodes[0].labels.nodes.map((l) => l.id);
|
|
760
924
|
}
|
|
761
925
|
return { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult };
|
|
762
926
|
}
|
|
@@ -783,16 +947,44 @@ export class GraphQLIssuesService {
|
|
|
783
947
|
}
|
|
784
948
|
return this.linearService.resolveStatusId(args.statusId, teamId);
|
|
785
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* Pre-resolve URL/slug-id project inputs to UUIDs before the batch
|
|
952
|
+
* resolver runs. Plain names and UUIDs pass through unchanged; only
|
|
953
|
+
* URL/slug-id forms incur a separate slug→UUID round-trip (the batch
|
|
954
|
+
* query's `name eqIgnoreCase` filter can't match a URL string).
|
|
955
|
+
*/
|
|
956
|
+
async withNormalizedProjectId(args) {
|
|
957
|
+
if (!args.projectId) {
|
|
958
|
+
return args;
|
|
959
|
+
}
|
|
960
|
+
const normalized = await this.linearService.normalizeProjectInput(args.projectId);
|
|
961
|
+
if (normalized === args.projectId) {
|
|
962
|
+
return args;
|
|
963
|
+
}
|
|
964
|
+
return { ...args, projectId: normalized };
|
|
965
|
+
}
|
|
786
966
|
buildCreateResolveVariables(args) {
|
|
787
967
|
const resolveVariables = {};
|
|
788
968
|
if (args.teamId && !isUuid(args.teamId)) {
|
|
789
969
|
Object.assign(resolveVariables, this.buildResolveVariablesForTeam(args.teamId));
|
|
790
970
|
}
|
|
791
|
-
if (args.projectId
|
|
792
|
-
|
|
971
|
+
if (args.projectId) {
|
|
972
|
+
// A UUID is resolved by `id` so the project's team associations
|
|
973
|
+
// are still fetched for team-project validation; a name is
|
|
974
|
+
// resolved by `name`. The `has*` flags gate the `@include`d
|
|
975
|
+
// blocks so an unset input never triggers a null no-op filter.
|
|
976
|
+
if (isUuid(args.projectId)) {
|
|
977
|
+
resolveVariables.projectId = args.projectId;
|
|
978
|
+
resolveVariables.hasProjectId = true;
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
resolveVariables.projectName = args.projectId;
|
|
982
|
+
resolveVariables.hasProjectName = true;
|
|
983
|
+
}
|
|
793
984
|
}
|
|
794
985
|
if (args.milestoneId && !isUuid(args.milestoneId)) {
|
|
795
986
|
resolveVariables.milestoneName = args.milestoneId;
|
|
987
|
+
resolveVariables.hasMilestoneName = true;
|
|
796
988
|
}
|
|
797
989
|
if (args.labelIds && args.labelIds.length > 0) {
|
|
798
990
|
const nonUuidLabels = args.labelIds.filter((id) => !isUuid(id));
|
|
@@ -813,30 +1005,32 @@ export class GraphQLIssuesService {
|
|
|
813
1005
|
if (!parentId || isUuid(parentId)) {
|
|
814
1006
|
return parentId;
|
|
815
1007
|
}
|
|
816
|
-
const
|
|
817
|
-
const parentNodes = parentIssues?.nodes;
|
|
1008
|
+
const parentNodes = resolveResult.parentIssues?.nodes;
|
|
818
1009
|
if (!parentNodes?.length) {
|
|
819
1010
|
throw notFoundError("Parent issue", parentId);
|
|
820
1011
|
}
|
|
821
1012
|
return parentNodes[0].id;
|
|
822
1013
|
}
|
|
823
1014
|
transformIssueData(issue) {
|
|
824
|
-
|
|
1015
|
+
// `labels`/`children`/`comments` are connection fields that the
|
|
1016
|
+
// fragment always selects, so production responses populate them.
|
|
1017
|
+
// Test mocks pass partial objects, so read defensively to keep
|
|
1018
|
+
// those test fixtures small.
|
|
825
1019
|
return {
|
|
826
1020
|
...this.transformIssueCoreFields(issue),
|
|
827
1021
|
...this.transformIssueRelations(issue),
|
|
828
|
-
summary: issue.summary
|
|
829
|
-
? extractSummaryText(issue.summary)
|
|
830
|
-
: undefined,
|
|
1022
|
+
summary: issue.summary ? extractSummaryText(issue.summary) : undefined,
|
|
831
1023
|
priority: issue.priority,
|
|
832
|
-
estimate: issue.estimate
|
|
833
|
-
dueDate: issue.dueDate
|
|
834
|
-
labels: labels
|
|
1024
|
+
estimate: issue.estimate ?? undefined,
|
|
1025
|
+
dueDate: issue.dueDate ?? undefined,
|
|
1026
|
+
labels: (issue.labels?.nodes ?? []).map((label) => ({
|
|
835
1027
|
id: label.id,
|
|
836
1028
|
name: label.name,
|
|
837
1029
|
})),
|
|
838
1030
|
...this.transformIssueHierarchy(issue),
|
|
839
|
-
comments:
|
|
1031
|
+
comments: "comments" in issue && issue.comments
|
|
1032
|
+
? this.transformIssueComments(issue)
|
|
1033
|
+
: [],
|
|
840
1034
|
createdAt: toISOStringOrNow(issue.createdAt),
|
|
841
1035
|
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
842
1036
|
};
|
|
@@ -847,93 +1041,91 @@ export class GraphQLIssuesService {
|
|
|
847
1041
|
identifier: issue.identifier,
|
|
848
1042
|
url: issue.url,
|
|
849
1043
|
title: issue.title,
|
|
850
|
-
description: issue.description
|
|
1044
|
+
description: issue.description ?? undefined,
|
|
851
1045
|
branchName: issue.branchName || undefined,
|
|
852
|
-
embeds: issue.description
|
|
853
|
-
? extractEmbeds(issue.description)
|
|
854
|
-
: undefined,
|
|
1046
|
+
embeds: issue.description ? extractEmbeds(issue.description) : undefined,
|
|
855
1047
|
};
|
|
856
1048
|
}
|
|
857
1049
|
transformIssueRelations(issue) {
|
|
858
|
-
const state = issue.state;
|
|
859
|
-
const assignee = issue.assignee;
|
|
860
|
-
const team = issue.team;
|
|
861
|
-
const project = issue.project;
|
|
862
|
-
const cycle = issue.cycle;
|
|
863
|
-
const milestone = issue.projectMilestone;
|
|
864
1050
|
return {
|
|
865
|
-
state: state
|
|
866
|
-
? { id: state.id, name: state.name }
|
|
1051
|
+
state: issue.state
|
|
1052
|
+
? { id: issue.state.id, name: issue.state.name }
|
|
867
1053
|
: undefined,
|
|
868
|
-
assignee: assignee
|
|
1054
|
+
assignee: issue.assignee
|
|
869
1055
|
? {
|
|
870
|
-
id: assignee.id,
|
|
871
|
-
name: resolveUserDisplayName(assignee.id, assignee.name),
|
|
872
|
-
url: assignee.url
|
|
1056
|
+
id: issue.assignee.id,
|
|
1057
|
+
name: resolveUserDisplayName(issue.assignee.id, issue.assignee.name),
|
|
1058
|
+
url: issue.assignee.url ?? undefined,
|
|
873
1059
|
}
|
|
874
1060
|
: undefined,
|
|
875
|
-
|
|
1061
|
+
delegate: issue.delegate
|
|
876
1062
|
? {
|
|
877
|
-
id:
|
|
878
|
-
|
|
879
|
-
|
|
1063
|
+
id: issue.delegate.id,
|
|
1064
|
+
name: resolveUserDisplayName(issue.delegate.id, issue.delegate.name),
|
|
1065
|
+
url: issue.delegate.url ?? undefined,
|
|
880
1066
|
}
|
|
881
1067
|
: undefined,
|
|
882
|
-
|
|
883
|
-
? {
|
|
1068
|
+
team: issue.team
|
|
1069
|
+
? {
|
|
1070
|
+
id: issue.team.id,
|
|
1071
|
+
key: issue.team.key,
|
|
1072
|
+
name: issue.team.name,
|
|
1073
|
+
}
|
|
1074
|
+
: undefined,
|
|
1075
|
+
project: issue.project
|
|
1076
|
+
? { id: issue.project.id, name: issue.project.name }
|
|
884
1077
|
: undefined,
|
|
885
|
-
cycle: cycle
|
|
1078
|
+
cycle: issue.cycle
|
|
886
1079
|
? {
|
|
887
|
-
id: cycle.id,
|
|
888
|
-
name: cycle.name,
|
|
889
|
-
number: cycle.number,
|
|
1080
|
+
id: issue.cycle.id,
|
|
1081
|
+
name: issue.cycle.name ?? "",
|
|
1082
|
+
number: issue.cycle.number,
|
|
890
1083
|
}
|
|
891
1084
|
: undefined,
|
|
892
|
-
projectMilestone:
|
|
1085
|
+
projectMilestone: issue.projectMilestone
|
|
893
1086
|
? {
|
|
894
|
-
id:
|
|
895
|
-
name:
|
|
896
|
-
targetDate:
|
|
1087
|
+
id: issue.projectMilestone.id,
|
|
1088
|
+
name: issue.projectMilestone.name,
|
|
1089
|
+
targetDate: issue.projectMilestone.targetDate ?? undefined,
|
|
897
1090
|
}
|
|
898
1091
|
: undefined,
|
|
899
1092
|
};
|
|
900
1093
|
}
|
|
901
1094
|
transformIssueHierarchy(issue) {
|
|
902
|
-
|
|
903
|
-
|
|
1095
|
+
// children is a connection field — always present in production
|
|
1096
|
+
// responses. Defensive read keeps test mocks small.
|
|
1097
|
+
const childNodes = issue.children?.nodes ?? [];
|
|
904
1098
|
return {
|
|
905
|
-
parentIssue: parent
|
|
1099
|
+
parentIssue: issue.parent
|
|
906
1100
|
? {
|
|
907
|
-
id: parent.id,
|
|
908
|
-
identifier: parent.identifier,
|
|
909
|
-
title: parent.title,
|
|
1101
|
+
id: issue.parent.id,
|
|
1102
|
+
identifier: issue.parent.identifier,
|
|
1103
|
+
title: issue.parent.title,
|
|
910
1104
|
}
|
|
911
1105
|
: undefined,
|
|
912
|
-
subIssues:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1106
|
+
subIssues: childNodes.length > 0
|
|
1107
|
+
? childNodes.map((child) => ({
|
|
1108
|
+
id: child.id,
|
|
1109
|
+
identifier: child.identifier,
|
|
1110
|
+
title: child.title,
|
|
1111
|
+
}))
|
|
1112
|
+
: undefined,
|
|
917
1113
|
};
|
|
918
1114
|
}
|
|
919
1115
|
transformIssueComments(issue) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
createdAt: toISOStringOrNow(comment.createdAt),
|
|
935
|
-
updatedAt: toISOStringOrNow(comment.updatedAt),
|
|
936
|
-
};
|
|
937
|
-
}) || []);
|
|
1116
|
+
return issue.comments.nodes.map((comment) => ({
|
|
1117
|
+
id: comment.id,
|
|
1118
|
+
body: comment.body,
|
|
1119
|
+
embeds: extractEmbeds(comment.body),
|
|
1120
|
+
user: comment.user
|
|
1121
|
+
? {
|
|
1122
|
+
id: comment.user.id,
|
|
1123
|
+
name: resolveUserDisplayName(comment.user.id, comment.user.name),
|
|
1124
|
+
url: comment.user.url ?? undefined,
|
|
1125
|
+
}
|
|
1126
|
+
: undefined,
|
|
1127
|
+
createdAt: toISOStringOrNow(comment.createdAt),
|
|
1128
|
+
updatedAt: toISOStringOrNow(comment.updatedAt),
|
|
1129
|
+
}));
|
|
938
1130
|
}
|
|
939
1131
|
}
|