@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.
Files changed (135) hide show
  1. package/README.md +139 -10
  2. package/claude-skills/linear-operations/SKILL.md +41 -1
  3. package/dist/auth/linear-credential.d.ts +27 -0
  4. package/dist/auth/linear-credential.js +1 -0
  5. package/dist/auth/oauth-app-config.d.ts +4 -3
  6. package/dist/auth/oauth-app-config.js +13 -2
  7. package/dist/auth/oauth-callback.d.ts +2 -3
  8. package/dist/auth/oauth-callback.js +2 -2
  9. package/dist/auth/oauth-client.d.ts +8 -2
  10. package/dist/auth/oauth-client.js +26 -0
  11. package/dist/auth/oauth-fs.d.ts +2 -1
  12. package/dist/auth/oauth-headless.d.ts +2 -1
  13. package/dist/auth/oauth-storage.d.ts +5 -1
  14. package/dist/auth/oauth-storage.js +1 -1
  15. package/dist/auth/oauth-token.d.ts +4 -3
  16. package/dist/auth/oauth-token.js +16 -4
  17. package/dist/auth/token-resolver.d.ts +14 -5
  18. package/dist/auth/token-resolver.js +6 -1
  19. package/dist/commands/attachments.js +2 -1
  20. package/dist/commands/batch.js +18 -21
  21. package/dist/commands/comments.js +22 -33
  22. package/dist/commands/config.js +178 -5
  23. package/dist/commands/cycles.js +2 -1
  24. package/dist/commands/documents.js +2 -1
  25. package/dist/commands/graphql.js +4 -6
  26. package/dist/commands/init/aliases.js +1 -1
  27. package/dist/commands/init/defaults.d.ts +2 -1
  28. package/dist/commands/init/index.js +45 -35
  29. package/dist/commands/init/oauth.d.ts +4 -1
  30. package/dist/commands/init/oauth.js +22 -4
  31. package/dist/commands/init/shared.d.ts +24 -2
  32. package/dist/commands/init/shared.js +35 -4
  33. package/dist/commands/init/token.d.ts +3 -3
  34. package/dist/commands/init/token.js +5 -24
  35. package/dist/commands/init/workspace.d.ts +2 -1
  36. package/dist/commands/init/workspace.js +1 -1
  37. package/dist/commands/introspect.d.ts +27 -0
  38. package/dist/commands/introspect.js +178 -0
  39. package/dist/commands/issue-id.js +1 -3
  40. package/dist/commands/issues/branch.js +9 -1
  41. package/dist/commands/issues/description.js +2 -6
  42. package/dist/commands/issues/link-references.d.ts +21 -0
  43. package/dist/commands/issues/link-references.js +171 -0
  44. package/dist/commands/issues/relations.d.ts +44 -0
  45. package/dist/commands/issues/relations.js +132 -0
  46. package/dist/commands/issues.js +269 -309
  47. package/dist/commands/labels.js +15 -24
  48. package/dist/commands/profile.js +1 -0
  49. package/dist/commands/project-milestones.js +13 -20
  50. package/dist/commands/projects.d.ts +2 -0
  51. package/dist/commands/projects.js +157 -44
  52. package/dist/commands/read-shortcut.d.ts +1 -1
  53. package/dist/commands/read-shortcut.js +28 -8
  54. package/dist/commands/refs.js +75 -8
  55. package/dist/commands/releases.js +26 -30
  56. package/dist/commands/search.js +49 -33
  57. package/dist/commands/teams.js +2 -1
  58. package/dist/commands/templates.js +9 -14
  59. package/dist/commands/users.js +5 -2
  60. package/dist/config/config.d.ts +99 -1
  61. package/dist/config/config.js +264 -52
  62. package/dist/config/error-enrichment.d.ts +62 -0
  63. package/dist/config/error-enrichment.js +417 -0
  64. package/dist/config/issue-validation.d.ts +37 -0
  65. package/dist/config/issue-validation.js +63 -1
  66. package/dist/config/paths.d.ts +2 -8
  67. package/dist/config/paths.js +4 -2
  68. package/dist/config/resolver.d.ts +8 -1
  69. package/dist/config/resolver.js +11 -5
  70. package/dist/main.js +13 -1
  71. package/dist/queries/attachments-types.d.ts +30 -0
  72. package/dist/queries/attachments-types.js +5 -0
  73. package/dist/queries/comments-types.d.ts +55 -0
  74. package/dist/queries/comments-types.js +5 -0
  75. package/dist/queries/common.d.ts +2 -2
  76. package/dist/queries/common.js +8 -0
  77. package/dist/queries/documents-types.d.ts +62 -0
  78. package/dist/queries/documents-types.js +9 -0
  79. package/dist/queries/introspect-types.d.ts +58 -0
  80. package/dist/queries/introspect-types.js +10 -0
  81. package/dist/queries/issues-types.d.ts +481 -0
  82. package/dist/queries/issues-types.js +23 -0
  83. package/dist/queries/issues.d.ts +51 -10
  84. package/dist/queries/issues.js +147 -5
  85. package/dist/queries/labels-types.d.ts +65 -0
  86. package/dist/queries/labels-types.js +5 -0
  87. package/dist/queries/project-milestones-types.d.ts +92 -0
  88. package/dist/queries/project-milestones-types.js +10 -0
  89. package/dist/queries/project-milestones.d.ts +1 -1
  90. package/dist/queries/projects-types.d.ts +76 -0
  91. package/dist/queries/projects-types.js +5 -0
  92. package/dist/queries/projects.d.ts +2 -0
  93. package/dist/queries/projects.js +22 -0
  94. package/dist/queries/releases-types.d.ts +85 -0
  95. package/dist/queries/releases-types.js +5 -0
  96. package/dist/queries/search-types.d.ts +102 -0
  97. package/dist/queries/search-types.js +6 -0
  98. package/dist/queries/templates-types.d.ts +62 -0
  99. package/dist/queries/templates-types.js +9 -0
  100. package/dist/types/linear.d.ts +21 -3
  101. package/dist/utils/auto-link-references.d.ts +3 -3
  102. package/dist/utils/auto-link-references.js +30 -34
  103. package/dist/utils/extract-field.d.ts +19 -0
  104. package/dist/utils/extract-field.js +99 -0
  105. package/dist/utils/file-service.d.ts +6 -13
  106. package/dist/utils/file-service.js +0 -2
  107. package/dist/utils/formatters/summary.js +6 -1
  108. package/dist/utils/graphql-attachments-service.js +6 -9
  109. package/dist/utils/graphql-documents-service.js +19 -25
  110. package/dist/utils/graphql-issues-service.d.ts +112 -46
  111. package/dist/utils/graphql-issues-service.js +398 -206
  112. package/dist/utils/graphql-service.d.ts +10 -12
  113. package/dist/utils/graphql-service.js +0 -3
  114. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  115. package/dist/utils/issue-reference-extractor.js +5 -3
  116. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  117. package/dist/utils/issues-service-bootstrap.js +27 -0
  118. package/dist/utils/linear-service.d.ts +21 -14
  119. package/dist/utils/linear-service.js +73 -11
  120. package/dist/utils/markdown-prosemirror.js +12 -12
  121. package/dist/utils/mention-resolver.js +1 -1
  122. package/dist/utils/output.d.ts +82 -2
  123. package/dist/utils/output.js +76 -11
  124. package/dist/utils/project-slug.d.ts +21 -0
  125. package/dist/utils/project-slug.js +45 -0
  126. package/dist/utils/protected-ranges.d.ts +14 -0
  127. package/dist/utils/protected-ranges.js +88 -2
  128. package/dist/utils/sanitize-for-log.d.ts +24 -0
  129. package/dist/utils/sanitize-for-log.js +38 -0
  130. package/dist/utils/table-formatter.js +24 -0
  131. package/dist/utils/validators.d.ts +7 -2
  132. package/dist/utils/validators.js +6 -0
  133. package/dist/utils/workspace-url.d.ts +5 -1
  134. package/dist/utils/workspace-url.js +53 -7
  135. 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
- first: limit,
39
- orderBy: "updatedAt",
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 issues.nodes.map((issue) => this.transformIssueData(issue));
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
- teamKey,
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
- const { resolvedIssueId, issueTeamId, currentIssueLabels, resolveResult } = await this.resolveUpdateContext(args);
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 && args.labelIds && resolvedIssueId) {
110
+ if (!teamIdForLabels && normalizedArgs.labelIds && resolvedIssueId) {
75
111
  teamIdForLabels = await this.fetchIssueTeamId(resolvedIssueId);
76
112
  }
77
- const finalLabelIds = await this.resolveLabelsWithMode(args.labelIds, resolveResult, labelMode, currentIssueLabels, teamIdForLabels, args.teamId);
78
- const finalProjectId = args.projectId
79
- ? this.resolveProjectId(args.projectId, resolveResult)
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(args, resolveResult);
82
- const finalMilestoneId = this.resolveMilestoneId(args.milestoneId, resolveResult, projectMilestoneNodes, issueProjectMilestoneNodes);
83
- const finalCycleId = await this.resolveCycleIdForUpdate(args, resolveResult, resolvedIssueId);
84
- const resolvedStatusId = await this.resolveStatusIdForUpdate(args, resolvedIssueId);
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 = args.assigneeId;
123
+ let assigneeId = normalizedArgs.assigneeId;
88
124
  if (assigneeId && !isUuid(assigneeId)) {
89
125
  assigneeId = await this.linearService.resolveUserId(assigneeId);
90
126
  }
91
- const updateInput = this.buildUpdateInput({ ...args, assigneeId }, {
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, args.id, updateInput);
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
- ? projectNodes?.[0]?.projectMilestones?.nodes
171
+ ? resolveResult.projects?.nodes?.[0]?.projectMilestones?.nodes
104
172
  : undefined;
105
- const issueNodes = resolveResult.issues
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
- const resolveVariables = this.buildCreateResolveVariables(args);
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 requests = [
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
- requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
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
- const results = await Promise.all(requests);
151
- resolveResult = results[0];
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(args, resolveResult);
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 = args.assigneeId;
233
+ let assigneeId = normalizedArgs.assigneeId;
161
234
  if (assigneeId && !isUuid(assigneeId)) {
162
235
  assigneeId = await this.linearService.resolveUserId(assigneeId);
163
236
  }
164
- const createInput = this.buildCreateInput({ ...args, assigneeId }, resolved);
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
- if (args.projectId && !isUuid(args.projectId)) {
225
- resolveVariables.projectName = args.projectId;
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 = await this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_SEARCH_QUERY, resolveVariables);
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 = args.projectId
240
- ? this.resolveProjectId(args.projectId, resolveResult)
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 searchIssues = searchResult.searchIssues;
250
- if (!searchIssues?.nodes) {
376
+ const nodes = searchResult.searchIssues?.nodes;
377
+ if (!nodes?.length) {
251
378
  return [];
252
379
  }
253
- const results = searchIssues.nodes.map((issue) => this.transformIssueData(issue));
380
+ const results = nodes.map((issue) => this.transformIssueData(issue));
254
381
  return this.applySearchFilters(results, {
255
382
  teamId: finalTeamId,
256
383
  assigneeId: finalAssigneeId,
257
- projectId: finalProjectId,
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
- projectId: finalProjectId,
267
- noProject: args.noProject,
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 teams = resolveResult.teams;
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 projects = resolveResult.projects;
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 projects = resolveResult.projects;
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 teamsConn = projectNode.teams;
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 labels = resolveResult.labels;
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?.success && created.issueLabel) {
406
- const labelData = created.issueLabel;
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 labelData.id;
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 topTeams = resolveResult.teams;
446
- const topNodes = topTeams?.nodes;
573
+ const topNodes = resolveResult.teams?.nodes;
447
574
  if (topNodes) {
448
575
  for (const t of topNodes) {
449
- const id = t.id;
450
- if (!seen.has(id)) {
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 projects = resolveResult.projects;
458
- const projectNode = projects?.nodes?.[0];
459
- if (projectNode) {
460
- const projectTeams = projectNode.teams;
461
- const projectTeamNodes = projectTeams?.nodes;
462
- if (projectTeamNodes) {
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 milestones = resolveResult.milestones;
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 createProjects = resolveResult.projects;
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 createMilestones = resolveResult.milestones;
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
- issueId,
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.projectId) {
650
- filtered = filtered.filter((issue) => issue.project?.id === filters.projectId);
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.noProject) {
676
- filter.project = { null: true };
677
- }
678
- else if (filters.projectId) {
679
- filter.project = { id: { eq: filters.projectId } };
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 users = resolveResult.users;
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 && !isUuid(args.projectId)) {
731
- resolveVariables.projectName = args.projectId;
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 requests = [
738
- this.graphQLService.rawRequest(BATCH_RESOLVE_FOR_UPDATE_QUERY, updateQueryVars),
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
- requests.push(this.graphQLService.rawRequest(labelQuery.query, labelQuery.variables));
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
- const results = await Promise.all(requests);
745
- const resolveResult = results[0];
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 resolvedIssues = resolveResult.issues;
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
- const team = resolvedIssueNodes[0].team;
757
- issueTeamId = team?.id;
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 && !isUuid(args.projectId)) {
792
- resolveVariables.projectName = args.projectId;
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 parentIssues = resolveResult.parentIssues;
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
- const labels = issue.labels;
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 || undefined,
833
- dueDate: issue.dueDate || undefined,
834
- labels: labels.nodes.map((label) => ({
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: this.transformIssueComments(issue),
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 || undefined,
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 || undefined,
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
- team: team
1061
+ delegate: issue.delegate
876
1062
  ? {
877
- id: team.id,
878
- key: team.key,
879
- name: team.name,
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
- project: project
883
- ? { id: project.id, name: project.name }
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: milestone
1085
+ projectMilestone: issue.projectMilestone
893
1086
  ? {
894
- id: milestone.id,
895
- name: milestone.name,
896
- targetDate: milestone.targetDate || undefined,
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
- const parent = issue.parent;
903
- const children = issue.children;
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: children?.nodes?.map((child) => ({
913
- id: child.id,
914
- identifier: child.identifier,
915
- title: child.title,
916
- })) || undefined,
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
- const comments = issue.comments;
921
- return (comments?.nodes?.map((comment) => {
922
- const commentUser = comment.user;
923
- return {
924
- id: comment.id,
925
- body: comment.body,
926
- embeds: extractEmbeds(comment.body),
927
- user: commentUser
928
- ? {
929
- id: commentUser.id,
930
- name: resolveUserDisplayName(commentUser.id, commentUser.name),
931
- url: commentUser.url || undefined,
932
- }
933
- : undefined,
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
  }