@fink-andreas/pi-linear-tools 0.4.2 → 0.5.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/CHANGELOG.md +33 -0
- package/README.md +131 -3
- package/extensions/pi-linear-tools.js +227 -89
- package/index.js +1 -1019
- package/package.json +2 -2
- package/src/cli.js +700 -6
- package/src/handlers.js +466 -3
- package/src/linear.js +2792 -782
- package/src/sync-doc.js +1208 -0
package/src/linear.js
CHANGED
|
@@ -90,830 +90,2589 @@ const ISSUES_WITH_RELATIONS_QUERY = `
|
|
|
90
90
|
}
|
|
91
91
|
`;
|
|
92
92
|
|
|
93
|
+
const PROJECT_DETAILS_QUERY = `
|
|
94
|
+
query ProjectDetails($id: String!, $milestoneLimit: Int!) {
|
|
95
|
+
project(id: $id) {
|
|
96
|
+
id
|
|
97
|
+
name
|
|
98
|
+
description
|
|
99
|
+
content
|
|
100
|
+
color
|
|
101
|
+
icon
|
|
102
|
+
priority
|
|
103
|
+
progress
|
|
104
|
+
health
|
|
105
|
+
startDate
|
|
106
|
+
targetDate
|
|
107
|
+
slugId
|
|
108
|
+
url
|
|
109
|
+
archivedAt
|
|
110
|
+
completedAt
|
|
111
|
+
canceledAt
|
|
112
|
+
status {
|
|
113
|
+
id
|
|
114
|
+
name
|
|
115
|
+
type
|
|
116
|
+
color
|
|
117
|
+
}
|
|
118
|
+
lead {
|
|
119
|
+
id
|
|
120
|
+
name
|
|
121
|
+
displayName
|
|
122
|
+
}
|
|
123
|
+
teams {
|
|
124
|
+
nodes {
|
|
125
|
+
id
|
|
126
|
+
key
|
|
127
|
+
name
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
projectMilestones(first: $milestoneLimit) {
|
|
131
|
+
nodes {
|
|
132
|
+
id
|
|
133
|
+
name
|
|
134
|
+
status
|
|
135
|
+
progress
|
|
136
|
+
targetDate
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const PROJECTS_LOOKUP_QUERY = `
|
|
144
|
+
query ProjectsLookup($includeArchived: Boolean!) {
|
|
145
|
+
projects(first: 250, includeArchived: $includeArchived) {
|
|
146
|
+
nodes {
|
|
147
|
+
id
|
|
148
|
+
name
|
|
149
|
+
slugId
|
|
150
|
+
archivedAt
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const PROJECT_MINIMAL_QUERY = `
|
|
157
|
+
query ProjectMinimal($id: String!) {
|
|
158
|
+
project(id: $id) {
|
|
159
|
+
id
|
|
160
|
+
name
|
|
161
|
+
slugId
|
|
162
|
+
archivedAt
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const PROJECT_MILESTONES_QUERY = `
|
|
168
|
+
query ProjectMilestones($id: String!, $first: Int!) {
|
|
169
|
+
project(id: $id) {
|
|
170
|
+
id
|
|
171
|
+
name
|
|
172
|
+
projectMilestones(first: $first) {
|
|
173
|
+
nodes {
|
|
174
|
+
id
|
|
175
|
+
name
|
|
176
|
+
description
|
|
177
|
+
progress
|
|
178
|
+
sortOrder
|
|
179
|
+
targetDate
|
|
180
|
+
status
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
const TEAM_MINIMAL_QUERY = `
|
|
188
|
+
query TeamMinimal($id: String!) {
|
|
189
|
+
team(id: $id) {
|
|
190
|
+
id
|
|
191
|
+
key
|
|
192
|
+
name
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
const TEAM_STATES_QUERY = `
|
|
198
|
+
query TeamStates($id: String!, $first: Int!) {
|
|
199
|
+
team(id: $id) {
|
|
200
|
+
id
|
|
201
|
+
key
|
|
202
|
+
name
|
|
203
|
+
states(first: $first) {
|
|
204
|
+
nodes {
|
|
205
|
+
id
|
|
206
|
+
name
|
|
207
|
+
type
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
const PROJECT_CREATE_MUTATION = `
|
|
215
|
+
mutation ProjectCreate($input: ProjectCreateInput!) {
|
|
216
|
+
projectCreate(input: $input) {
|
|
217
|
+
success
|
|
218
|
+
project {
|
|
219
|
+
id
|
|
220
|
+
name
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const PROJECT_UPDATE_MUTATION = `
|
|
227
|
+
mutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
|
|
228
|
+
projectUpdate(id: $id, input: $input) {
|
|
229
|
+
success
|
|
230
|
+
project {
|
|
231
|
+
id
|
|
232
|
+
name
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
const PROJECT_DELETE_MUTATION = `
|
|
239
|
+
mutation ProjectDelete($id: String!) {
|
|
240
|
+
projectDelete(id: $id) {
|
|
241
|
+
success
|
|
242
|
+
entity {
|
|
243
|
+
id
|
|
244
|
+
name
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
const PROJECT_ARCHIVE_MUTATION = `
|
|
251
|
+
mutation ProjectArchive($id: String!) {
|
|
252
|
+
projectArchiveResult: projectDelete(id: $id) {
|
|
253
|
+
success
|
|
254
|
+
entity {
|
|
255
|
+
id
|
|
256
|
+
name
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
const PROJECT_UNARCHIVE_MUTATION = `
|
|
263
|
+
mutation ProjectUnarchive($id: String!) {
|
|
264
|
+
projectUnarchive(id: $id) {
|
|
265
|
+
success
|
|
266
|
+
entity {
|
|
267
|
+
id
|
|
268
|
+
name
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
const PROJECT_UPDATES_BY_PROJECT_QUERY = `
|
|
275
|
+
query ProjectUpdatesByProject($id: String!, $first: Int!, $includeArchived: Boolean!) {
|
|
276
|
+
project(id: $id) {
|
|
277
|
+
id
|
|
278
|
+
name
|
|
279
|
+
projectUpdates(first: $first, includeArchived: $includeArchived) {
|
|
280
|
+
nodes {
|
|
281
|
+
id
|
|
282
|
+
body
|
|
283
|
+
health
|
|
284
|
+
createdAt
|
|
285
|
+
updatedAt
|
|
286
|
+
archivedAt
|
|
287
|
+
url
|
|
288
|
+
slugId
|
|
289
|
+
isDiffHidden
|
|
290
|
+
isStale
|
|
291
|
+
user {
|
|
292
|
+
id
|
|
293
|
+
name
|
|
294
|
+
displayName
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
const PROJECT_UPDATE_DETAILS_QUERY = `
|
|
303
|
+
query ProjectUpdateDetails($id: String!) {
|
|
304
|
+
projectUpdate(id: $id) {
|
|
305
|
+
id
|
|
306
|
+
body
|
|
307
|
+
health
|
|
308
|
+
createdAt
|
|
309
|
+
updatedAt
|
|
310
|
+
archivedAt
|
|
311
|
+
editedAt
|
|
312
|
+
url
|
|
313
|
+
slugId
|
|
314
|
+
isDiffHidden
|
|
315
|
+
isStale
|
|
316
|
+
project {
|
|
317
|
+
id
|
|
318
|
+
name
|
|
319
|
+
}
|
|
320
|
+
user {
|
|
321
|
+
id
|
|
322
|
+
name
|
|
323
|
+
displayName
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
const PROJECT_UPDATE_CREATE_MUTATION = `
|
|
330
|
+
mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {
|
|
331
|
+
projectUpdateCreate(input: $input) {
|
|
332
|
+
success
|
|
333
|
+
projectUpdate {
|
|
334
|
+
id
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const PROJECT_UPDATE_UPDATE_MUTATION = `
|
|
341
|
+
mutation ProjectUpdateUpdate($id: String!, $input: ProjectUpdateUpdateInput!) {
|
|
342
|
+
projectUpdateUpdate(id: $id, input: $input) {
|
|
343
|
+
success
|
|
344
|
+
projectUpdate {
|
|
345
|
+
id
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
`;
|
|
350
|
+
|
|
351
|
+
const PROJECT_UPDATE_ARCHIVE_MUTATION = `
|
|
352
|
+
mutation ProjectUpdateArchive($id: String!) {
|
|
353
|
+
projectUpdateArchive(id: $id) {
|
|
354
|
+
success
|
|
355
|
+
entity {
|
|
356
|
+
id
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
const PROJECT_UPDATE_UNARCHIVE_MUTATION = `
|
|
363
|
+
mutation ProjectUpdateUnarchive($id: String!) {
|
|
364
|
+
projectUpdateUnarchive(id: $id) {
|
|
365
|
+
success
|
|
366
|
+
entity {
|
|
367
|
+
id
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
const DOCUMENT_DETAILS_QUERY = `
|
|
374
|
+
query DocumentDetails($id: String!) {
|
|
375
|
+
document(id: $id) {
|
|
376
|
+
id
|
|
377
|
+
title
|
|
378
|
+
content
|
|
379
|
+
icon
|
|
380
|
+
color
|
|
381
|
+
slugId
|
|
382
|
+
url
|
|
383
|
+
archivedAt
|
|
384
|
+
createdAt
|
|
385
|
+
updatedAt
|
|
386
|
+
project {
|
|
387
|
+
id
|
|
388
|
+
name
|
|
389
|
+
}
|
|
390
|
+
issue {
|
|
391
|
+
id
|
|
392
|
+
identifier
|
|
393
|
+
title
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
`;
|
|
398
|
+
|
|
399
|
+
const DOCUMENT_CREATE_MUTATION = `
|
|
400
|
+
mutation DocumentCreate($input: DocumentCreateInput!) {
|
|
401
|
+
documentCreate(input: $input) {
|
|
402
|
+
success
|
|
403
|
+
document {
|
|
404
|
+
id
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
const DOCUMENT_UPDATE_MUTATION = `
|
|
411
|
+
mutation DocumentUpdate($id: String!, $input: DocumentUpdateInput!) {
|
|
412
|
+
documentUpdate(id: $id, input: $input) {
|
|
413
|
+
success
|
|
414
|
+
document {
|
|
415
|
+
id
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
const ISSUE_MINIMAL_QUERY = `
|
|
422
|
+
query IssueMinimal($id: String!) {
|
|
423
|
+
issue(id: $id) {
|
|
424
|
+
id
|
|
425
|
+
identifier
|
|
426
|
+
title
|
|
427
|
+
description
|
|
428
|
+
url
|
|
429
|
+
branchName
|
|
430
|
+
priority
|
|
431
|
+
estimate
|
|
432
|
+
createdAt
|
|
433
|
+
updatedAt
|
|
434
|
+
state {
|
|
435
|
+
id
|
|
436
|
+
name
|
|
437
|
+
type
|
|
438
|
+
}
|
|
439
|
+
team {
|
|
440
|
+
id
|
|
441
|
+
key
|
|
442
|
+
name
|
|
443
|
+
}
|
|
444
|
+
project {
|
|
445
|
+
id
|
|
446
|
+
name
|
|
447
|
+
}
|
|
448
|
+
projectMilestone {
|
|
449
|
+
id
|
|
450
|
+
name
|
|
451
|
+
}
|
|
452
|
+
assignee {
|
|
453
|
+
id
|
|
454
|
+
name
|
|
455
|
+
displayName
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
`;
|
|
460
|
+
|
|
461
|
+
const ISSUE_MINIMAL_BY_TEAM_AND_NUMBER_QUERY = `
|
|
462
|
+
query IssueMinimalByTeamAndNumber($teamKey: String!, $number: Float!) {
|
|
463
|
+
issues(first: 1, filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }) {
|
|
464
|
+
nodes {
|
|
465
|
+
id
|
|
466
|
+
identifier
|
|
467
|
+
title
|
|
468
|
+
description
|
|
469
|
+
url
|
|
470
|
+
branchName
|
|
471
|
+
priority
|
|
472
|
+
estimate
|
|
473
|
+
createdAt
|
|
474
|
+
updatedAt
|
|
475
|
+
state {
|
|
476
|
+
id
|
|
477
|
+
name
|
|
478
|
+
type
|
|
479
|
+
}
|
|
480
|
+
team {
|
|
481
|
+
id
|
|
482
|
+
key
|
|
483
|
+
name
|
|
484
|
+
}
|
|
485
|
+
project {
|
|
486
|
+
id
|
|
487
|
+
name
|
|
488
|
+
}
|
|
489
|
+
projectMilestone {
|
|
490
|
+
id
|
|
491
|
+
name
|
|
492
|
+
}
|
|
493
|
+
assignee {
|
|
494
|
+
id
|
|
495
|
+
name
|
|
496
|
+
displayName
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
`;
|
|
502
|
+
|
|
503
|
+
const ISSUE_DETAILS_QUERY = `
|
|
504
|
+
query IssueDetails($id: String!) {
|
|
505
|
+
issue(id: $id) {
|
|
506
|
+
id
|
|
507
|
+
identifier
|
|
508
|
+
title
|
|
509
|
+
description
|
|
510
|
+
url
|
|
511
|
+
branchName
|
|
512
|
+
priority
|
|
513
|
+
estimate
|
|
514
|
+
createdAt
|
|
515
|
+
updatedAt
|
|
516
|
+
state {
|
|
517
|
+
id
|
|
518
|
+
name
|
|
519
|
+
color
|
|
520
|
+
type
|
|
521
|
+
}
|
|
522
|
+
team {
|
|
523
|
+
id
|
|
524
|
+
key
|
|
525
|
+
name
|
|
526
|
+
}
|
|
527
|
+
project {
|
|
528
|
+
id
|
|
529
|
+
name
|
|
530
|
+
}
|
|
531
|
+
projectMilestone {
|
|
532
|
+
id
|
|
533
|
+
name
|
|
534
|
+
}
|
|
535
|
+
assignee {
|
|
536
|
+
id
|
|
537
|
+
name
|
|
538
|
+
displayName
|
|
539
|
+
}
|
|
540
|
+
creator {
|
|
541
|
+
id
|
|
542
|
+
name
|
|
543
|
+
displayName
|
|
544
|
+
}
|
|
545
|
+
labels(first: 50) {
|
|
546
|
+
nodes {
|
|
547
|
+
id
|
|
548
|
+
name
|
|
549
|
+
color
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
parent {
|
|
553
|
+
id
|
|
554
|
+
identifier
|
|
555
|
+
title
|
|
556
|
+
state {
|
|
557
|
+
id
|
|
558
|
+
name
|
|
559
|
+
color
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
children(first: 50) {
|
|
563
|
+
nodes {
|
|
564
|
+
id
|
|
565
|
+
identifier
|
|
566
|
+
title
|
|
567
|
+
state {
|
|
568
|
+
id
|
|
569
|
+
name
|
|
570
|
+
color
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
attachments(first: 50) {
|
|
575
|
+
nodes {
|
|
576
|
+
id
|
|
577
|
+
title
|
|
578
|
+
url
|
|
579
|
+
subtitle
|
|
580
|
+
sourceType
|
|
581
|
+
createdAt
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
const ISSUE_DETAILS_WITH_COMMENTS_QUERY = `
|
|
589
|
+
query IssueDetailsWithComments($id: String!) {
|
|
590
|
+
issue(id: $id) {
|
|
591
|
+
id
|
|
592
|
+
identifier
|
|
593
|
+
title
|
|
594
|
+
description
|
|
595
|
+
url
|
|
596
|
+
branchName
|
|
597
|
+
priority
|
|
598
|
+
estimate
|
|
599
|
+
createdAt
|
|
600
|
+
updatedAt
|
|
601
|
+
state {
|
|
602
|
+
id
|
|
603
|
+
name
|
|
604
|
+
color
|
|
605
|
+
type
|
|
606
|
+
}
|
|
607
|
+
team {
|
|
608
|
+
id
|
|
609
|
+
key
|
|
610
|
+
name
|
|
611
|
+
}
|
|
612
|
+
project {
|
|
613
|
+
id
|
|
614
|
+
name
|
|
615
|
+
}
|
|
616
|
+
projectMilestone {
|
|
617
|
+
id
|
|
618
|
+
name
|
|
619
|
+
}
|
|
620
|
+
assignee {
|
|
621
|
+
id
|
|
622
|
+
name
|
|
623
|
+
displayName
|
|
624
|
+
}
|
|
625
|
+
creator {
|
|
626
|
+
id
|
|
627
|
+
name
|
|
628
|
+
displayName
|
|
629
|
+
}
|
|
630
|
+
labels(first: 50) {
|
|
631
|
+
nodes {
|
|
632
|
+
id
|
|
633
|
+
name
|
|
634
|
+
color
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
parent {
|
|
638
|
+
id
|
|
639
|
+
identifier
|
|
640
|
+
title
|
|
641
|
+
state {
|
|
642
|
+
id
|
|
643
|
+
name
|
|
644
|
+
color
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
children(first: 50) {
|
|
648
|
+
nodes {
|
|
649
|
+
id
|
|
650
|
+
identifier
|
|
651
|
+
title
|
|
652
|
+
state {
|
|
653
|
+
id
|
|
654
|
+
name
|
|
655
|
+
color
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
comments(first: 100) {
|
|
660
|
+
nodes {
|
|
661
|
+
id
|
|
662
|
+
body
|
|
663
|
+
createdAt
|
|
664
|
+
updatedAt
|
|
665
|
+
user {
|
|
666
|
+
id
|
|
667
|
+
name
|
|
668
|
+
displayName
|
|
669
|
+
}
|
|
670
|
+
externalUser {
|
|
671
|
+
id
|
|
672
|
+
name
|
|
673
|
+
displayName
|
|
674
|
+
}
|
|
675
|
+
parent {
|
|
676
|
+
id
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
attachments(first: 50) {
|
|
681
|
+
nodes {
|
|
682
|
+
id
|
|
683
|
+
title
|
|
684
|
+
url
|
|
685
|
+
subtitle
|
|
686
|
+
sourceType
|
|
687
|
+
createdAt
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
`;
|
|
693
|
+
|
|
694
|
+
const ISSUE_CREATE_MUTATION = `
|
|
695
|
+
mutation IssueCreate($input: IssueCreateInput!) {
|
|
696
|
+
issueCreate(input: $input) {
|
|
697
|
+
success
|
|
698
|
+
issue {
|
|
699
|
+
id
|
|
700
|
+
identifier
|
|
701
|
+
title
|
|
702
|
+
description
|
|
703
|
+
url
|
|
704
|
+
branchName
|
|
705
|
+
priority
|
|
706
|
+
estimate
|
|
707
|
+
createdAt
|
|
708
|
+
updatedAt
|
|
709
|
+
state {
|
|
710
|
+
id
|
|
711
|
+
name
|
|
712
|
+
type
|
|
713
|
+
}
|
|
714
|
+
team {
|
|
715
|
+
id
|
|
716
|
+
key
|
|
717
|
+
name
|
|
718
|
+
}
|
|
719
|
+
project {
|
|
720
|
+
id
|
|
721
|
+
name
|
|
722
|
+
}
|
|
723
|
+
projectMilestone {
|
|
724
|
+
id
|
|
725
|
+
name
|
|
726
|
+
}
|
|
727
|
+
assignee {
|
|
728
|
+
id
|
|
729
|
+
name
|
|
730
|
+
displayName
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
`;
|
|
736
|
+
|
|
737
|
+
const ISSUE_UPDATE_MUTATION = `
|
|
738
|
+
mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
739
|
+
issueUpdate(id: $id, input: $input) {
|
|
740
|
+
success
|
|
741
|
+
issue {
|
|
742
|
+
id
|
|
743
|
+
identifier
|
|
744
|
+
title
|
|
745
|
+
description
|
|
746
|
+
url
|
|
747
|
+
branchName
|
|
748
|
+
priority
|
|
749
|
+
estimate
|
|
750
|
+
createdAt
|
|
751
|
+
updatedAt
|
|
752
|
+
state {
|
|
753
|
+
id
|
|
754
|
+
name
|
|
755
|
+
type
|
|
756
|
+
}
|
|
757
|
+
team {
|
|
758
|
+
id
|
|
759
|
+
key
|
|
760
|
+
name
|
|
761
|
+
}
|
|
762
|
+
project {
|
|
763
|
+
id
|
|
764
|
+
name
|
|
765
|
+
}
|
|
766
|
+
projectMilestone {
|
|
767
|
+
id
|
|
768
|
+
name
|
|
769
|
+
}
|
|
770
|
+
assignee {
|
|
771
|
+
id
|
|
772
|
+
name
|
|
773
|
+
displayName
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
`;
|
|
779
|
+
|
|
780
|
+
const ISSUE_DELETE_MUTATION = `
|
|
781
|
+
mutation IssueDelete($id: String!) {
|
|
782
|
+
issueDelete(id: $id) {
|
|
783
|
+
success
|
|
784
|
+
entity {
|
|
785
|
+
id
|
|
786
|
+
identifier
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
`;
|
|
791
|
+
|
|
792
|
+
const ISSUE_ACTIVITY_QUERY = `
|
|
793
|
+
query IssueActivity($id: String!, $first: Int!, $includeArchived: Boolean!) {
|
|
794
|
+
issue(id: $id) {
|
|
795
|
+
id
|
|
796
|
+
identifier
|
|
797
|
+
title
|
|
798
|
+
url
|
|
799
|
+
history(first: $first, includeArchived: $includeArchived) {
|
|
800
|
+
nodes {
|
|
801
|
+
id
|
|
802
|
+
createdAt
|
|
803
|
+
updatedAt
|
|
804
|
+
archived
|
|
805
|
+
archivedAt
|
|
806
|
+
autoArchived
|
|
807
|
+
autoClosed
|
|
808
|
+
trashed
|
|
809
|
+
updatedDescription
|
|
810
|
+
fromTitle
|
|
811
|
+
toTitle
|
|
812
|
+
fromPriority
|
|
813
|
+
toPriority
|
|
814
|
+
fromState {
|
|
815
|
+
id
|
|
816
|
+
name
|
|
817
|
+
}
|
|
818
|
+
toState {
|
|
819
|
+
id
|
|
820
|
+
name
|
|
821
|
+
}
|
|
822
|
+
fromAssignee {
|
|
823
|
+
id
|
|
824
|
+
name
|
|
825
|
+
displayName
|
|
826
|
+
}
|
|
827
|
+
toAssignee {
|
|
828
|
+
id
|
|
829
|
+
name
|
|
830
|
+
displayName
|
|
831
|
+
}
|
|
832
|
+
fromProject {
|
|
833
|
+
id
|
|
834
|
+
name
|
|
835
|
+
}
|
|
836
|
+
toProject {
|
|
837
|
+
id
|
|
838
|
+
name
|
|
839
|
+
}
|
|
840
|
+
fromProjectMilestone {
|
|
841
|
+
id
|
|
842
|
+
name
|
|
843
|
+
}
|
|
844
|
+
toProjectMilestone {
|
|
845
|
+
id
|
|
846
|
+
name
|
|
847
|
+
}
|
|
848
|
+
addedLabels {
|
|
849
|
+
id
|
|
850
|
+
name
|
|
851
|
+
}
|
|
852
|
+
removedLabels {
|
|
853
|
+
id
|
|
854
|
+
name
|
|
855
|
+
}
|
|
856
|
+
relationChanges {
|
|
857
|
+
identifier
|
|
858
|
+
type
|
|
859
|
+
}
|
|
860
|
+
attachment {
|
|
861
|
+
id
|
|
862
|
+
title
|
|
863
|
+
url
|
|
864
|
+
}
|
|
865
|
+
actor {
|
|
866
|
+
id
|
|
867
|
+
name
|
|
868
|
+
displayName
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
`;
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Execute an optimized GraphQL query using rawRequest
|
|
878
|
+
* Falls back to SDK method if rawRequest is not available (e.g., in tests)
|
|
879
|
+
* @param {LinearClient} client - Linear SDK client
|
|
880
|
+
* @param {string} query - GraphQL query string
|
|
881
|
+
* @param {Object} variables - Query variables
|
|
882
|
+
* @returns {Promise<{data: Object, headers: Headers}>}
|
|
883
|
+
*/
|
|
884
|
+
async function executeOptimizedQuery(client, query, variables) {
|
|
885
|
+
// Try rawRequest first (preferred - more efficient)
|
|
886
|
+
if (typeof client.rawRequest === 'function') {
|
|
887
|
+
const response = await client.rawRequest(query, variables);
|
|
888
|
+
return {
|
|
889
|
+
data: response.data,
|
|
890
|
+
headers: response.headers,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Fallback to SDK method for testing/compatibility
|
|
895
|
+
if (typeof client.client?.rawRequest === 'function') {
|
|
896
|
+
const response = await client.client.rawRequest(query, variables);
|
|
897
|
+
return {
|
|
898
|
+
data: response.data,
|
|
899
|
+
headers: response.headers,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Fallback: use SDK's issues() method (less efficient but always available)
|
|
904
|
+
warn('executeOptimizedQuery: rawRequest not available, falling back to SDK method');
|
|
905
|
+
const filter = variables.filter || {};
|
|
906
|
+
const result = await client.issues({
|
|
907
|
+
first: variables.first,
|
|
908
|
+
filter,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
data: {
|
|
913
|
+
issues: {
|
|
914
|
+
nodes: result.nodes || [],
|
|
915
|
+
pageInfo: result.pageInfo || { hasNextPage: false },
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
headers: new Headers(),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function getRawRequest(client) {
|
|
923
|
+
return (
|
|
924
|
+
(typeof client.client?.rawRequest === 'function' ? client.client.rawRequest.bind(client.client) : null) ||
|
|
925
|
+
(typeof client.rawRequest === 'function' ? client.rawRequest.bind(client) : null)
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function executeGraphQL(client, query, variables = {}) {
|
|
930
|
+
const rawRequest = getRawRequest(client);
|
|
931
|
+
|
|
932
|
+
if (!rawRequest) {
|
|
933
|
+
throw new Error('GraphQL rawRequest is unavailable on this Linear client');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const response = await rawRequest(query, variables);
|
|
937
|
+
updateRateLimitState(response);
|
|
938
|
+
checkRateLimitWarning();
|
|
939
|
+
return response.data;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Transform raw GraphQL issue data to plain object format
|
|
944
|
+
* Used by optimized queries to avoid SDK lazy loading
|
|
945
|
+
*/
|
|
946
|
+
function transformRawIssue(rawIssue) {
|
|
947
|
+
if (!rawIssue) return null;
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
id: rawIssue.id,
|
|
951
|
+
identifier: rawIssue.identifier,
|
|
952
|
+
title: rawIssue.title,
|
|
953
|
+
description: rawIssue.description,
|
|
954
|
+
url: rawIssue.url,
|
|
955
|
+
branchName: rawIssue.branchName,
|
|
956
|
+
priority: rawIssue.priority,
|
|
957
|
+
estimate: rawIssue.estimate ?? null,
|
|
958
|
+
createdAt: rawIssue.createdAt ?? null,
|
|
959
|
+
updatedAt: rawIssue.updatedAt ?? null,
|
|
960
|
+
state: rawIssue.state ? { id: rawIssue.state.id, name: rawIssue.state.name, type: rawIssue.state.type } : null,
|
|
961
|
+
team: rawIssue.team ? { id: rawIssue.team.id, key: rawIssue.team.key, name: rawIssue.team.name } : null,
|
|
962
|
+
project: rawIssue.project ? { id: rawIssue.project.id, name: rawIssue.project.name } : null,
|
|
963
|
+
projectMilestone: rawIssue.projectMilestone ? { id: rawIssue.projectMilestone.id, name: rawIssue.projectMilestone.name } : null,
|
|
964
|
+
assignee: rawIssue.assignee ? { id: rawIssue.assignee.id, name: rawIssue.assignee.name, displayName: rawIssue.assignee.displayName } : null,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function parseIssueIdentifierLookup(lookup) {
|
|
969
|
+
const match = String(lookup || '').trim().match(/^([A-Za-z0-9]+)-(\d+)$/);
|
|
970
|
+
if (!match) {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
teamKey: match[1].toUpperCase(),
|
|
976
|
+
number: Number.parseInt(match[2], 10),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function fetchIssueMinimalById(client, issueId) {
|
|
981
|
+
if (getRawRequest(client)) {
|
|
982
|
+
try {
|
|
983
|
+
const data = await executeGraphQL(client, ISSUE_MINIMAL_QUERY, { id: issueId });
|
|
984
|
+
const transformed = transformRawIssue(data?.issue ?? null);
|
|
985
|
+
if (transformed) {
|
|
986
|
+
return transformed;
|
|
987
|
+
}
|
|
988
|
+
} catch {
|
|
989
|
+
// Fall back to SDK read below.
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const sdkIssue = await client.issue?.(issueId);
|
|
994
|
+
return sdkIssue ? transformIssue(sdkIssue) : null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function fetchIssueMinimalByIdentifier(client, identifier) {
|
|
998
|
+
if (getRawRequest(client)) {
|
|
999
|
+
const parsed = parseIssueIdentifierLookup(identifier);
|
|
1000
|
+
if (parsed) {
|
|
1001
|
+
try {
|
|
1002
|
+
const data = await executeGraphQL(client, ISSUE_MINIMAL_BY_TEAM_AND_NUMBER_QUERY, parsed);
|
|
1003
|
+
const transformed = transformRawIssue(data?.issues?.nodes?.[0] ?? null);
|
|
1004
|
+
if (transformed) {
|
|
1005
|
+
return transformed;
|
|
1006
|
+
}
|
|
1007
|
+
} catch {
|
|
1008
|
+
// Fall back to SDK read below.
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const sdkIssue = await client.issue?.(identifier);
|
|
1014
|
+
return sdkIssue ? transformIssue(sdkIssue) : null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function fetchIssueMinimal(client, lookup) {
|
|
1018
|
+
return isLinearId(lookup)
|
|
1019
|
+
? fetchIssueMinimalById(client, lookup)
|
|
1020
|
+
: fetchIssueMinimalByIdentifier(client, lookup);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function performIssueUpdate(client, issueId, updateInput) {
|
|
1024
|
+
if (getRawRequest(client)) {
|
|
1025
|
+
const payload = await executeGraphQL(client, ISSUE_UPDATE_MUTATION, {
|
|
1026
|
+
id: issueId,
|
|
1027
|
+
input: updateInput,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
if (!payload?.issueUpdate?.success) {
|
|
1031
|
+
throw new Error('Failed to update issue');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return transformRawIssue(payload.issueUpdate.issue ?? null);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const sdkIssue = await client.issue(issueId);
|
|
1038
|
+
if (!sdkIssue) {
|
|
1039
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const result = await sdkIssue.update(updateInput);
|
|
1043
|
+
if (!result.success) {
|
|
1044
|
+
throw new Error('Failed to update issue');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return transformIssue(result.issue);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function transformRawIssueDetails(rawIssue, options = {}) {
|
|
1051
|
+
const { includeComments = true } = options;
|
|
1052
|
+
if (!rawIssue) return null;
|
|
1053
|
+
|
|
1054
|
+
return {
|
|
1055
|
+
identifier: rawIssue.identifier,
|
|
1056
|
+
title: rawIssue.title,
|
|
1057
|
+
description: rawIssue.description ?? null,
|
|
1058
|
+
url: rawIssue.url ?? null,
|
|
1059
|
+
branchName: rawIssue.branchName ?? null,
|
|
1060
|
+
priority: rawIssue.priority ?? null,
|
|
1061
|
+
estimate: rawIssue.estimate ?? null,
|
|
1062
|
+
createdAt: rawIssue.createdAt ?? null,
|
|
1063
|
+
updatedAt: rawIssue.updatedAt ?? null,
|
|
1064
|
+
state: rawIssue.state ? {
|
|
1065
|
+
name: rawIssue.state.name,
|
|
1066
|
+
color: rawIssue.state.color ?? null,
|
|
1067
|
+
type: rawIssue.state.type ?? null,
|
|
1068
|
+
} : null,
|
|
1069
|
+
team: rawIssue.team ? { id: rawIssue.team.id, key: rawIssue.team.key, name: rawIssue.team.name } : null,
|
|
1070
|
+
project: rawIssue.project ? { id: rawIssue.project.id, name: rawIssue.project.name } : null,
|
|
1071
|
+
projectMilestone: rawIssue.projectMilestone ? { id: rawIssue.projectMilestone.id, name: rawIssue.projectMilestone.name } : null,
|
|
1072
|
+
assignee: rawIssue.assignee ? { id: rawIssue.assignee.id, name: rawIssue.assignee.name, displayName: rawIssue.assignee.displayName } : null,
|
|
1073
|
+
creator: rawIssue.creator ? { id: rawIssue.creator.id, name: rawIssue.creator.name, displayName: rawIssue.creator.displayName } : null,
|
|
1074
|
+
labels: (rawIssue.labels?.nodes || []).map((label) => ({
|
|
1075
|
+
id: label.id,
|
|
1076
|
+
name: label.name,
|
|
1077
|
+
color: label.color ?? null,
|
|
1078
|
+
})),
|
|
1079
|
+
parent: rawIssue.parent ? {
|
|
1080
|
+
identifier: rawIssue.parent.identifier,
|
|
1081
|
+
title: rawIssue.parent.title,
|
|
1082
|
+
state: rawIssue.parent.state ? {
|
|
1083
|
+
name: rawIssue.parent.state.name,
|
|
1084
|
+
color: rawIssue.parent.state.color ?? null,
|
|
1085
|
+
} : null,
|
|
1086
|
+
} : null,
|
|
1087
|
+
children: (rawIssue.children?.nodes || []).map((child) => ({
|
|
1088
|
+
identifier: child.identifier,
|
|
1089
|
+
title: child.title,
|
|
1090
|
+
state: child.state ? {
|
|
1091
|
+
name: child.state.name,
|
|
1092
|
+
color: child.state.color ?? null,
|
|
1093
|
+
} : null,
|
|
1094
|
+
})),
|
|
1095
|
+
comments: includeComments
|
|
1096
|
+
? (rawIssue.comments?.nodes || []).map((comment) => ({
|
|
1097
|
+
id: comment.id,
|
|
1098
|
+
body: comment.body,
|
|
1099
|
+
createdAt: comment.createdAt,
|
|
1100
|
+
updatedAt: comment.updatedAt,
|
|
1101
|
+
user: comment.user ? {
|
|
1102
|
+
name: comment.user.name,
|
|
1103
|
+
displayName: comment.user.displayName,
|
|
1104
|
+
} : null,
|
|
1105
|
+
externalUser: comment.externalUser ? {
|
|
1106
|
+
name: comment.externalUser.name,
|
|
1107
|
+
displayName: comment.externalUser.displayName,
|
|
1108
|
+
} : null,
|
|
1109
|
+
parent: comment.parent ? { id: comment.parent.id } : null,
|
|
1110
|
+
}))
|
|
1111
|
+
: [],
|
|
1112
|
+
attachments: (rawIssue.attachments?.nodes || []).map((attachment) => ({
|
|
1113
|
+
id: attachment.id,
|
|
1114
|
+
title: attachment.title,
|
|
1115
|
+
url: attachment.url,
|
|
1116
|
+
subtitle: attachment.subtitle,
|
|
1117
|
+
sourceType: attachment.sourceType,
|
|
1118
|
+
createdAt: attachment.createdAt,
|
|
1119
|
+
})),
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function transformRawProjectMinimal(rawProject) {
|
|
1124
|
+
if (!rawProject) return null;
|
|
1125
|
+
|
|
1126
|
+
return {
|
|
1127
|
+
id: rawProject.id,
|
|
1128
|
+
name: rawProject.name,
|
|
1129
|
+
slugId: rawProject.slugId ?? null,
|
|
1130
|
+
archivedAt: rawProject.archivedAt ?? null,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function transformRawMilestone(rawMilestone, project = null) {
|
|
1135
|
+
if (!rawMilestone) return null;
|
|
1136
|
+
|
|
1137
|
+
return {
|
|
1138
|
+
id: rawMilestone.id,
|
|
1139
|
+
name: rawMilestone.name,
|
|
1140
|
+
description: rawMilestone.description ?? null,
|
|
1141
|
+
progress: rawMilestone.progress ?? null,
|
|
1142
|
+
order: rawMilestone.sortOrder ?? null,
|
|
1143
|
+
targetDate: rawMilestone.targetDate ?? null,
|
|
1144
|
+
status: rawMilestone.status ?? null,
|
|
1145
|
+
project: project ? { id: project.id, name: project.name } : null,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async function fetchProjectMinimal(client, projectId) {
|
|
1150
|
+
// If input looks like a UUID but can't be resolved directly,
|
|
1151
|
+
// it might be a project name that accidentally matches the UUID pattern.
|
|
1152
|
+
// Return null to signal that resolveProjectRef should fall back to name search.
|
|
1153
|
+
if (isLinearId(projectId)) {
|
|
1154
|
+
if (!getRawRequest(client)) {
|
|
1155
|
+
const sdkProject = await client.project?.(projectId);
|
|
1156
|
+
if (sdkProject) {
|
|
1157
|
+
return transformRawProjectMinimal(sdkProject);
|
|
1158
|
+
}
|
|
1159
|
+
// SDK can't resolve it - return null so resolveProjectRef falls back to name lookup
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const data = await executeGraphQL(client, PROJECT_MINIMAL_QUERY, { id: projectId });
|
|
1164
|
+
if (data?.project) {
|
|
1165
|
+
return transformRawProjectMinimal(data.project);
|
|
1166
|
+
}
|
|
1167
|
+
// GraphQL can't resolve it - return null so resolveProjectRef falls back to name lookup
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// For non-UUID inputs (names), return null to let resolveProjectRef handle it directly
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function fetchTeamMinimal(client, teamId) {
|
|
1176
|
+
if (getRawRequest(client)) {
|
|
1177
|
+
try {
|
|
1178
|
+
const data = await executeGraphQL(client, TEAM_MINIMAL_QUERY, { id: teamId });
|
|
1179
|
+
const team = data?.team;
|
|
1180
|
+
if (team) {
|
|
1181
|
+
return { id: team.id, key: team.key, name: team.name };
|
|
1182
|
+
}
|
|
1183
|
+
} catch {
|
|
1184
|
+
// Fall back to SDK read below.
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const sdkTeam = await client.team?.(teamId);
|
|
1189
|
+
return sdkTeam ? { id: sdkTeam.id, key: sdkTeam.key, name: sdkTeam.name } : null;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function fetchTeamStatesByQuery(client, teamId, options = {}) {
|
|
1193
|
+
const { first = 50 } = options;
|
|
1194
|
+
|
|
1195
|
+
if (getRawRequest(client)) {
|
|
1196
|
+
try {
|
|
1197
|
+
const data = await executeGraphQL(client, TEAM_STATES_QUERY, { id: teamId, first });
|
|
1198
|
+
if (data?.team) {
|
|
1199
|
+
return (data.team.states?.nodes || []).map((state) => ({
|
|
1200
|
+
id: state.id,
|
|
1201
|
+
name: state.name,
|
|
1202
|
+
type: state.type,
|
|
1203
|
+
}));
|
|
1204
|
+
}
|
|
1205
|
+
} catch {
|
|
1206
|
+
// Fall back to SDK read below.
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const team = await client.team?.(teamId);
|
|
1211
|
+
if (!team) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const result = await team.states();
|
|
1216
|
+
return (result.nodes || []).map((state) => ({
|
|
1217
|
+
id: state.id,
|
|
1218
|
+
name: state.name,
|
|
1219
|
+
type: state.type,
|
|
1220
|
+
}));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function fetchProjectMilestonesByQuery(client, projectId, options = {}) {
|
|
1224
|
+
const { first = 250 } = options;
|
|
1225
|
+
|
|
1226
|
+
if (!getRawRequest(client)) {
|
|
1227
|
+
const project = await client.project?.(projectId);
|
|
1228
|
+
if (!project) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const result = await project.projectMilestones();
|
|
1233
|
+
const nodes = result.nodes || [];
|
|
1234
|
+
return Promise.all(nodes.map(transformMilestone));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const data = await executeGraphQL(client, PROJECT_MILESTONES_QUERY, {
|
|
1238
|
+
id: projectId,
|
|
1239
|
+
first,
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
if (!data?.project) {
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const project = { id: data.project.id, name: data.project.name };
|
|
1247
|
+
return (data.project.projectMilestones?.nodes || []).map((milestone) => transformRawMilestone(milestone, project));
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// ===== RATE LIMIT TRACKING =====
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Track rate limit status from API responses
|
|
1254
|
+
* Linear API returns headers: X-RateLimit-Requests-Remaining, X-RateLimit-Requests-Reset
|
|
1255
|
+
*/
|
|
1256
|
+
const rateLimitState = {
|
|
1257
|
+
remaining: null,
|
|
1258
|
+
resetAt: null,
|
|
1259
|
+
lastWarnAt: 0,
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Update rate limit state from response headers (internal)
|
|
1264
|
+
* @param {Response} response - Fetch response object
|
|
1265
|
+
*/
|
|
1266
|
+
function updateRateLimitState(response) {
|
|
1267
|
+
if (!response) return;
|
|
1268
|
+
|
|
1269
|
+
const headers = response.headers;
|
|
1270
|
+
if (headers) {
|
|
1271
|
+
const remaining = headers.get('X-RateLimit-Requests-Remaining');
|
|
1272
|
+
const resetAt = headers.get('X-RateLimit-Requests-Reset');
|
|
1273
|
+
|
|
1274
|
+
if (remaining !== null) {
|
|
1275
|
+
rateLimitState.remaining = parseInt(remaining, 10);
|
|
1276
|
+
}
|
|
1277
|
+
if (resetAt !== null) {
|
|
1278
|
+
rateLimitState.resetAt = parseInt(resetAt, 10);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Get current rate limit status
|
|
1285
|
+
* @returns {{remaining: number|null, resetAt: number|null, resetTime: string|null, usagePercent: number|null, shouldWarn: boolean}}
|
|
1286
|
+
*/
|
|
1287
|
+
export function getRateLimitStatus() {
|
|
1288
|
+
const result = {
|
|
1289
|
+
remaining: rateLimitState.remaining,
|
|
1290
|
+
resetAt: rateLimitState.resetAt,
|
|
1291
|
+
resetTime: null,
|
|
1292
|
+
usagePercent: null,
|
|
1293
|
+
shouldWarn: false,
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
if (rateLimitState.resetAt) {
|
|
1297
|
+
result.resetTime = new Date(rateLimitState.resetAt).toLocaleTimeString();
|
|
1298
|
+
const remaining = rateLimitState.remaining;
|
|
1299
|
+
if (remaining !== null && remaining <= 1000) {
|
|
1300
|
+
result.usagePercent = Math.round(((5000 - remaining) / 5000) * 100);
|
|
1301
|
+
result.shouldWarn = remaining <= 500;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return result;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Check and warn about low rate limit
|
|
1310
|
+
*/
|
|
1311
|
+
function checkRateLimitWarning() {
|
|
1312
|
+
const now = Date.now();
|
|
1313
|
+
// Only warn once per 30 seconds to avoid spam
|
|
1314
|
+
if (now - rateLimitState.lastWarnAt < 30000) return;
|
|
1315
|
+
|
|
1316
|
+
const status = getRateLimitStatus();
|
|
1317
|
+
if (status.shouldWarn && status.remaining !== null) {
|
|
1318
|
+
rateLimitState.lastWarnAt = now;
|
|
1319
|
+
warn(`Linear API rate limit running low: ${status.remaining} requests remaining (~${status.usagePercent}% used). Resets at ${status.resetTime}`, {
|
|
1320
|
+
remaining: status.remaining,
|
|
1321
|
+
resetAt: status.resetTime,
|
|
1322
|
+
usagePercent: status.usagePercent,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// ===== ERROR HANDLING =====
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Check if an error is a Linear SDK error type
|
|
1331
|
+
* @param {Error} error - The error to check
|
|
1332
|
+
* @returns {boolean}
|
|
1333
|
+
*/
|
|
1334
|
+
function isLinearError(error) {
|
|
1335
|
+
return error?.constructor?.name?.includes('LinearError') ||
|
|
1336
|
+
error?.name?.includes('LinearError') ||
|
|
1337
|
+
error?.type?.startsWith?.('Ratelimited') ||
|
|
1338
|
+
error?.type?.startsWith?.('Forbidden') ||
|
|
1339
|
+
error?.type?.startsWith?.('Authentication') ||
|
|
1340
|
+
error?.type === 'invalid_request' ||
|
|
1341
|
+
error?.type === 'NetworkError' ||
|
|
1342
|
+
error?.type === 'InternalError';
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Format a Linear API error into a user-friendly message
|
|
1347
|
+
* @param {Error} error - The original error
|
|
1348
|
+
* @returns {Error|unknown} Formatted error with user-friendly message, or original if unhandled
|
|
1349
|
+
*/
|
|
1350
|
+
function formatLinearError(error) {
|
|
1351
|
+
const message = String(error?.message || error || 'Unknown error');
|
|
1352
|
+
const errorType = error?.type || 'Unknown';
|
|
1353
|
+
|
|
1354
|
+
// Rate limit: provide reset time and reduce-frequency hint
|
|
1355
|
+
if (errorType === 'Ratelimited' || message.toLowerCase().includes('rate limit')) {
|
|
1356
|
+
const resetAt = error?.requestsResetAt
|
|
1357
|
+
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
1358
|
+
: '1 hour';
|
|
1359
|
+
|
|
1360
|
+
return new Error(
|
|
1361
|
+
`Linear API rate limit exceeded. Please wait before making more requests.\n` +
|
|
1362
|
+
`Rate limit resets at: ${resetAt}\n` +
|
|
1363
|
+
`Hint: Reduce request frequency or wait before retrying.`
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Auth/permission failures: prompt to check credentials
|
|
1368
|
+
if (errorType === 'Forbidden' || errorType === 'AuthenticationError' ||
|
|
1369
|
+
message.toLowerCase().includes('forbidden') || message.toLowerCase().includes('unauthorized')) {
|
|
1370
|
+
return new Error(
|
|
1371
|
+
`${message}\nHint: Check your Linear API key or OAuth token permissions.`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Network errors
|
|
1376
|
+
if (errorType === 'NetworkError' || message.toLowerCase().includes('network')) {
|
|
1377
|
+
return new Error(
|
|
1378
|
+
`Network error while communicating with Linear API.\nHint: Check your internet connection and try again.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Internal server errors
|
|
1383
|
+
if (errorType === 'InternalError' || (error?.status >= 500 && error?.status < 600)) {
|
|
1384
|
+
return new Error(
|
|
1385
|
+
`Linear API server error (${error?.status || 'unknown'}).\nHint: Linear may be experiencing issues. Try again later.`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Generic Linear API error
|
|
1390
|
+
if (isLinearError(error)) {
|
|
1391
|
+
return new Error(
|
|
1392
|
+
`Linear API error: ${message}`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Unknown error - wrap if not already an Error
|
|
1397
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Wrap an async function with Linear-specific error handling
|
|
1402
|
+
* @param {Function} fn - Async function to wrap
|
|
1403
|
+
* @param {string} operation - Description of the operation for error messages
|
|
1404
|
+
* @returns {Promise<*>} Result of the wrapped function
|
|
1405
|
+
*/
|
|
1406
|
+
async function withLinearErrorHandling(fn, operation = 'Linear API operation') {
|
|
1407
|
+
try {
|
|
1408
|
+
return await fn();
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
if (isLinearError(error)) {
|
|
1411
|
+
const formatted = formatLinearError(error);
|
|
1412
|
+
debug(`${operation} failed with Linear error`, {
|
|
1413
|
+
originalError: error?.message,
|
|
1414
|
+
errorType: error?.type,
|
|
1415
|
+
formattedMessage: formatted.message,
|
|
1416
|
+
});
|
|
1417
|
+
throw formatted;
|
|
1418
|
+
}
|
|
1419
|
+
throw error;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Wrap a handler function with comprehensive error handling for Linear API errors.
|
|
1425
|
+
* This provides user-friendly error messages for rate limits, auth issues, etc.
|
|
1426
|
+
* Use this in the execute() functions of tool handlers.
|
|
1427
|
+
*
|
|
1428
|
+
* @param {Function} fn - Async handler function to wrap
|
|
1429
|
+
* @param {string} operation - Description of the operation for error messages
|
|
1430
|
+
* @returns {Promise<*>} Result of the wrapped function
|
|
1431
|
+
* @example
|
|
1432
|
+
* ```js
|
|
1433
|
+
* export async function executeIssueList(client, params) {
|
|
1434
|
+
* return withHandlerErrorHandling(async () => {
|
|
1435
|
+
* // ... implementation
|
|
1436
|
+
* }, 'executeIssueList');
|
|
1437
|
+
* }
|
|
1438
|
+
* ```
|
|
1439
|
+
*/
|
|
1440
|
+
export async function withHandlerErrorHandling(fn, operation = 'Handler') {
|
|
1441
|
+
return withLinearErrorHandling(async () => {
|
|
1442
|
+
try {
|
|
1443
|
+
return await fn();
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
// Log additional context for unexpected errors
|
|
1446
|
+
debug(`${operation} failed unexpectedly`, {
|
|
1447
|
+
error: error?.message,
|
|
1448
|
+
stack: error?.stack,
|
|
1449
|
+
});
|
|
1450
|
+
throw error;
|
|
1451
|
+
}
|
|
1452
|
+
}, operation);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// ===== HELPERS =====
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Check if a value looks like a Linear UUID
|
|
1459
|
+
*/
|
|
1460
|
+
function isLinearId(value) {
|
|
1461
|
+
return typeof value === 'string' && /^[0-9a-fA-F-]{16,}$/.test(value);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Normalize issue lookup input
|
|
1466
|
+
*/
|
|
1467
|
+
function normalizeIssueLookupInput(issue) {
|
|
1468
|
+
const value = String(issue || '').trim();
|
|
1469
|
+
if (!value) throw new Error('Missing required issue identifier');
|
|
1470
|
+
|
|
1471
|
+
const issueUrlMatch = value.match(/\/issue\/([A-Za-z0-9]+-\d+)(?:[/?#]|$)/i);
|
|
1472
|
+
if (issueUrlMatch?.[1]) {
|
|
1473
|
+
return issueUrlMatch[1].toUpperCase();
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return value;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function extractProjectLookupValue(projectRef) {
|
|
1480
|
+
const ref = String(projectRef || '').trim();
|
|
1481
|
+
if (!ref) {
|
|
1482
|
+
return '';
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const projectUrlMatch = ref.match(/\/project\/([^/?#]+)/i);
|
|
1486
|
+
if (projectUrlMatch?.[1]) {
|
|
1487
|
+
return projectUrlMatch[1];
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
return ref;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function getProjectLookupCandidates(projectRef) {
|
|
1494
|
+
const lookupValue = extractProjectLookupValue(projectRef);
|
|
1495
|
+
const candidates = new Set([lookupValue]);
|
|
1496
|
+
|
|
1497
|
+
if (lookupValue.includes('-')) {
|
|
1498
|
+
const slugSuffix = lookupValue.split('-').pop();
|
|
1499
|
+
if (slugSuffix) {
|
|
1500
|
+
candidates.add(slugSuffix);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return Array.from(candidates).filter(Boolean);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const PROJECT_UPDATE_HEALTH_VALUES = ['onTrack', 'atRisk', 'offTrack'];
|
|
1508
|
+
|
|
1509
|
+
function normalizePositiveInteger(value, fieldName, defaultValue) {
|
|
1510
|
+
if (value === undefined || value === null) {
|
|
1511
|
+
return defaultValue;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
const parsed = typeof value === 'number' ? value : Number(String(value).trim());
|
|
1515
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1516
|
+
throw new Error(`${fieldName} must be a positive integer`);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return parsed;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function normalizeProjectUpdateHealth(value) {
|
|
1523
|
+
if (value === undefined) {
|
|
1524
|
+
return undefined;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
const normalized = String(value).trim();
|
|
1528
|
+
if (!normalized) {
|
|
1529
|
+
throw new Error(`health must be one of: ${PROJECT_UPDATE_HEALTH_VALUES.join(', ')}`);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (!PROJECT_UPDATE_HEALTH_VALUES.includes(normalized)) {
|
|
1533
|
+
throw new Error(`health must be one of: ${PROJECT_UPDATE_HEALTH_VALUES.join(', ')}`);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
return normalized;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function formatPriorityLabel(value) {
|
|
1540
|
+
if (value === undefined || value === null) {
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const numeric = Number(value);
|
|
1545
|
+
if (!Number.isFinite(numeric)) {
|
|
1546
|
+
return String(value);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const priorityNames = ['No priority', 'Urgent', 'High', 'Medium', 'Low'];
|
|
1550
|
+
return priorityNames[numeric] || `Priority ${numeric}`;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function getUserDisplayName(user) {
|
|
1554
|
+
return user?.displayName || user?.name || 'Unknown';
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function summarizeIssueHistoryEntry(entry) {
|
|
1558
|
+
if (entry.fromState?.name || entry.toState?.name) {
|
|
1559
|
+
if (entry.fromState?.name && entry.toState?.name) {
|
|
1560
|
+
return `moved state from ${entry.fromState.name} to ${entry.toState.name}`;
|
|
1561
|
+
}
|
|
1562
|
+
if (entry.toState?.name) {
|
|
1563
|
+
return `set state to ${entry.toState.name}`;
|
|
1564
|
+
}
|
|
1565
|
+
return `cleared state ${entry.fromState.name}`;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (entry.fromAssignee || entry.toAssignee) {
|
|
1569
|
+
const fromAssignee = entry.fromAssignee ? getUserDisplayName(entry.fromAssignee) : null;
|
|
1570
|
+
const toAssignee = entry.toAssignee ? getUserDisplayName(entry.toAssignee) : null;
|
|
1571
|
+
if (fromAssignee && toAssignee) {
|
|
1572
|
+
return `reassigned from ${fromAssignee} to ${toAssignee}`;
|
|
1573
|
+
}
|
|
1574
|
+
if (toAssignee) {
|
|
1575
|
+
return `assigned to ${toAssignee}`;
|
|
1576
|
+
}
|
|
1577
|
+
return `unassigned from ${fromAssignee}`;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (entry.fromTitle || entry.toTitle) {
|
|
1581
|
+
if (entry.fromTitle && entry.toTitle) {
|
|
1582
|
+
return `renamed issue from "${entry.fromTitle}" to "${entry.toTitle}"`;
|
|
1583
|
+
}
|
|
1584
|
+
if (entry.toTitle) {
|
|
1585
|
+
return `set title to "${entry.toTitle}"`;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (entry.fromPriority !== undefined || entry.toPriority !== undefined) {
|
|
1590
|
+
const fromPriority = formatPriorityLabel(entry.fromPriority);
|
|
1591
|
+
const toPriority = formatPriorityLabel(entry.toPriority);
|
|
1592
|
+
if (fromPriority && toPriority) {
|
|
1593
|
+
return `changed priority from ${fromPriority} to ${toPriority}`;
|
|
1594
|
+
}
|
|
1595
|
+
if (toPriority) {
|
|
1596
|
+
return `set priority to ${toPriority}`;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (entry.fromProject?.name || entry.toProject?.name) {
|
|
1601
|
+
if (entry.fromProject?.name && entry.toProject?.name) {
|
|
1602
|
+
return `moved project from ${entry.fromProject.name} to ${entry.toProject.name}`;
|
|
1603
|
+
}
|
|
1604
|
+
if (entry.toProject?.name) {
|
|
1605
|
+
return `added to project ${entry.toProject.name}`;
|
|
1606
|
+
}
|
|
1607
|
+
return `removed from project ${entry.fromProject.name}`;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (entry.fromProjectMilestone?.name || entry.toProjectMilestone?.name) {
|
|
1611
|
+
if (entry.fromProjectMilestone?.name && entry.toProjectMilestone?.name) {
|
|
1612
|
+
return `moved milestone from ${entry.fromProjectMilestone.name} to ${entry.toProjectMilestone.name}`;
|
|
1613
|
+
}
|
|
1614
|
+
if (entry.toProjectMilestone?.name) {
|
|
1615
|
+
return `set milestone to ${entry.toProjectMilestone.name}`;
|
|
1616
|
+
}
|
|
1617
|
+
return `cleared milestone ${entry.fromProjectMilestone.name}`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if ((entry.addedLabels?.length || 0) > 0 || (entry.removedLabels?.length || 0) > 0) {
|
|
1621
|
+
const labelChanges = [];
|
|
1622
|
+
if ((entry.addedLabels?.length || 0) > 0) {
|
|
1623
|
+
labelChanges.push(`added labels ${entry.addedLabels.map((label) => label.name).join(', ')}`);
|
|
1624
|
+
}
|
|
1625
|
+
if ((entry.removedLabels?.length || 0) > 0) {
|
|
1626
|
+
labelChanges.push(`removed labels ${entry.removedLabels.map((label) => label.name).join(', ')}`);
|
|
1627
|
+
}
|
|
1628
|
+
return labelChanges.join('; ');
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if ((entry.relationChanges?.length || 0) > 0) {
|
|
1632
|
+
const relationSummary = entry.relationChanges
|
|
1633
|
+
.map((relation) => `${relation.type} ${relation.identifier}`)
|
|
1634
|
+
.join(', ');
|
|
1635
|
+
return `updated relations: ${relationSummary}`;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (entry.updatedDescription) {
|
|
1639
|
+
return 'updated description';
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (entry.attachment?.title || entry.attachment?.url) {
|
|
1643
|
+
return `linked attachment ${entry.attachment.title ? `"${entry.attachment.title}"` : entry.attachment.url}`;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (entry.archivedAt || entry.archived === true) {
|
|
1647
|
+
return entry.autoArchived ? 'auto-archived issue' : 'archived issue';
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
if (entry.autoClosed) {
|
|
1651
|
+
return 'auto-closed issue';
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (entry.trashed === true) {
|
|
1655
|
+
return 'trashed issue';
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (entry.trashed === false) {
|
|
1659
|
+
return 'restored issue from trash';
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
return 'updated issue';
|
|
1663
|
+
}
|
|
1664
|
+
|
|
93
1665
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* @param {LinearClient} client - Linear SDK client
|
|
97
|
-
* @param {string} query - GraphQL query string
|
|
98
|
-
* @param {Object} variables - Query variables
|
|
99
|
-
* @returns {Promise<{data: Object, headers: Headers}>}
|
|
1666
|
+
* Safely resolve a lazy-loaded relation without triggering unnecessary API calls
|
|
1667
|
+
* Only fetches if the relation is already cached or if we have minimal data
|
|
100
1668
|
*/
|
|
101
|
-
async function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1669
|
+
async function safeResolveRelation(sdkIssue, relationKey) {
|
|
1670
|
+
try {
|
|
1671
|
+
const relation = sdkIssue[relationKey];
|
|
1672
|
+
if (!relation) return null;
|
|
1673
|
+
|
|
1674
|
+
// If it's a function (lazy loader), check if we can call it safely
|
|
1675
|
+
if (typeof relation === 'function') {
|
|
1676
|
+
const result = await relation().catch(() => null);
|
|
1677
|
+
return result || null;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// If it's already resolved, return it
|
|
1681
|
+
return relation;
|
|
1682
|
+
} catch {
|
|
1683
|
+
return null;
|
|
109
1684
|
}
|
|
1685
|
+
}
|
|
110
1686
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1687
|
+
/**
|
|
1688
|
+
* Transform SDK issue object to plain object for consumers
|
|
1689
|
+
* Optimized to minimize API calls by avoiding unnecessary lazy loads
|
|
1690
|
+
*/
|
|
1691
|
+
async function transformIssue(sdkIssue) {
|
|
1692
|
+
if (!sdkIssue) return null;
|
|
1693
|
+
|
|
1694
|
+
// If the SDK issue has _data, relations might already be available
|
|
1695
|
+
// Check if we can extract data without making extra calls
|
|
1696
|
+
const hasRelations = sdkIssue.state?.id || sdkIssue.assignee?.id || sdkIssue.project?.id;
|
|
1697
|
+
|
|
1698
|
+
// Only resolve relations if not already available
|
|
1699
|
+
let state = null;
|
|
1700
|
+
let team = null;
|
|
1701
|
+
let project = null;
|
|
1702
|
+
let assignee = null;
|
|
1703
|
+
let projectMilestone = null;
|
|
1704
|
+
|
|
1705
|
+
if (sdkIssue.state?.id) {
|
|
1706
|
+
state = sdkIssue.state;
|
|
1707
|
+
} else if (sdkIssue._data?.state) {
|
|
1708
|
+
state = { id: sdkIssue._data.state?.id, name: sdkIssue._data.state?.name, type: sdkIssue._data.state?.type };
|
|
118
1709
|
}
|
|
119
1710
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1711
|
+
if (sdkIssue.team?.id) {
|
|
1712
|
+
team = sdkIssue.team;
|
|
1713
|
+
} else if (sdkIssue._data?.team) {
|
|
1714
|
+
team = { id: sdkIssue._data.team?.id, key: sdkIssue._data.team?.key, name: sdkIssue._data.team?.name };
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if (sdkIssue.project?.id) {
|
|
1718
|
+
project = sdkIssue.project;
|
|
1719
|
+
} else if (sdkIssue._data?.project) {
|
|
1720
|
+
project = { id: sdkIssue._data.project?.id, name: sdkIssue._data.project?.name };
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if (sdkIssue.assignee?.id) {
|
|
1724
|
+
assignee = sdkIssue.assignee;
|
|
1725
|
+
} else if (sdkIssue._data?.assignee) {
|
|
1726
|
+
assignee = { id: sdkIssue._data.assignee?.id, name: sdkIssue._data.assignee?.name, displayName: sdkIssue._data.assignee?.displayName };
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (sdkIssue.projectMilestone?.id) {
|
|
1730
|
+
projectMilestone = sdkIssue.projectMilestone;
|
|
1731
|
+
} else if (sdkIssue._data?.projectMilestone) {
|
|
1732
|
+
projectMilestone = { id: sdkIssue._data.projectMilestone?.id, name: sdkIssue._data.projectMilestone?.name };
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Only trigger lazy loads if we don't have data from cache
|
|
1736
|
+
const needsLazyLoad = !state && !team && !project && !assignee && !projectMilestone;
|
|
1737
|
+
|
|
1738
|
+
if (needsLazyLoad) {
|
|
1739
|
+
// Use Promise.all with small timeout to avoid blocking
|
|
1740
|
+
const [resolvedState, resolvedTeam, resolvedProject, resolvedAssignee, resolvedMilestone] = await Promise.all([
|
|
1741
|
+
safeResolveRelation(sdkIssue, 'state'),
|
|
1742
|
+
safeResolveRelation(sdkIssue, 'team'),
|
|
1743
|
+
safeResolveRelation(sdkIssue, 'project'),
|
|
1744
|
+
safeResolveRelation(sdkIssue, 'assignee'),
|
|
1745
|
+
safeResolveRelation(sdkIssue, 'projectMilestone'),
|
|
1746
|
+
]);
|
|
1747
|
+
|
|
1748
|
+
state = state || resolvedState;
|
|
1749
|
+
team = team || resolvedTeam;
|
|
1750
|
+
project = project || resolvedProject;
|
|
1751
|
+
assignee = assignee || resolvedAssignee;
|
|
1752
|
+
projectMilestone = projectMilestone || resolvedMilestone;
|
|
1753
|
+
}
|
|
127
1754
|
|
|
128
1755
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1756
|
+
id: sdkIssue.id,
|
|
1757
|
+
identifier: sdkIssue.identifier,
|
|
1758
|
+
title: sdkIssue.title,
|
|
1759
|
+
description: sdkIssue.description,
|
|
1760
|
+
url: sdkIssue.url,
|
|
1761
|
+
branchName: sdkIssue.branchName,
|
|
1762
|
+
priority: sdkIssue.priority,
|
|
1763
|
+
state: state ? { id: state.id, name: state.name, type: state.type } : null,
|
|
1764
|
+
team: team ? { id: team.id, key: team.key, name: team.name } : null,
|
|
1765
|
+
project: project ? { id: project.id, name: project.name } : null,
|
|
1766
|
+
projectMilestone: projectMilestone ? { id: projectMilestone.id, name: projectMilestone.name } : null,
|
|
1767
|
+
assignee: assignee ? { id: assignee.id, name: assignee.name, displayName: assignee.displayName } : null,
|
|
136
1768
|
};
|
|
137
1769
|
}
|
|
138
1770
|
|
|
139
1771
|
/**
|
|
140
|
-
*
|
|
141
|
-
* Used by optimized queries to avoid SDK lazy loading
|
|
1772
|
+
* Resolve state ID from state input (ID, name, or type)
|
|
142
1773
|
*/
|
|
143
|
-
function
|
|
144
|
-
if (!
|
|
1774
|
+
function resolveStateIdFromInput(states, stateInput) {
|
|
1775
|
+
if (!stateInput) return null;
|
|
1776
|
+
const target = String(stateInput).trim();
|
|
1777
|
+
if (!target) return null;
|
|
145
1778
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
projectMilestone: rawIssue.projectMilestone ? { id: rawIssue.projectMilestone.id, name: rawIssue.projectMilestone.name } : null,
|
|
158
|
-
assignee: rawIssue.assignee ? { id: rawIssue.assignee.id, name: rawIssue.assignee.name, displayName: rawIssue.assignee.displayName } : null,
|
|
159
|
-
};
|
|
1779
|
+
const byId = states.find((s) => s.id === target);
|
|
1780
|
+
if (byId) return byId.id;
|
|
1781
|
+
|
|
1782
|
+
const lower = target.toLowerCase();
|
|
1783
|
+
const byName = states.find((s) => String(s.name || '').toLowerCase() === lower);
|
|
1784
|
+
if (byName) return byName.id;
|
|
1785
|
+
|
|
1786
|
+
const byType = states.find((s) => String(s.type || '').toLowerCase() === lower);
|
|
1787
|
+
if (byType) return byType.id;
|
|
1788
|
+
|
|
1789
|
+
throw new Error(`State not found in team workflow: ${target}`);
|
|
160
1790
|
}
|
|
161
1791
|
|
|
162
|
-
|
|
1792
|
+
function resolveProjectMilestoneIdFromInput(milestones, milestoneInput) {
|
|
1793
|
+
const target = String(milestoneInput || '').trim();
|
|
1794
|
+
if (!target) return null;
|
|
1795
|
+
|
|
1796
|
+
const byId = milestones.find((m) => m.id === target);
|
|
1797
|
+
if (byId) return byId.id;
|
|
1798
|
+
|
|
1799
|
+
const lower = target.toLowerCase();
|
|
1800
|
+
const byName = milestones.find((m) => String(m.name || '').toLowerCase() === lower);
|
|
1801
|
+
if (byName) return byName.id;
|
|
1802
|
+
|
|
1803
|
+
throw new Error(`Milestone not found in project: ${target}`);
|
|
1804
|
+
}
|
|
163
1805
|
|
|
164
1806
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
1807
|
+
* Resolve a milestone reference (name or ID) to a milestone object with id and name.
|
|
1808
|
+
* Requires project context to search for milestones by name.
|
|
1809
|
+
* @param {LinearClient} client - Linear SDK client
|
|
1810
|
+
* @param {string} milestoneRef - Milestone name or ID
|
|
1811
|
+
* @param {string} projectId - Project ID to search within
|
|
1812
|
+
* @returns {Promise<{id: string, name: string}>}
|
|
167
1813
|
*/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
1814
|
+
export async function resolveMilestoneRef(client, milestoneRef, projectId) {
|
|
1815
|
+
const ref = String(milestoneRef || '').trim();
|
|
1816
|
+
if (!ref) {
|
|
1817
|
+
throw new Error('Missing milestone reference');
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// If it's already a Linear ID (UUID format with 16+ hex chars), try to fetch it directly
|
|
1821
|
+
if (/^[0-9a-fA-F-]{16,}$/.test(ref)) {
|
|
1822
|
+
try {
|
|
1823
|
+
const milestone = await client.projectMilestone(ref);
|
|
1824
|
+
if (milestone) {
|
|
1825
|
+
return { id: milestone.id, name: milestone.name };
|
|
1826
|
+
}
|
|
1827
|
+
} catch {
|
|
1828
|
+
// fall through to name lookup
|
|
1829
|
+
}
|
|
1830
|
+
throw new Error(`Milestone not found: ${ref}`);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// Search by name in the project's milestones
|
|
1834
|
+
const milestones = await fetchProjectMilestones(client, projectId);
|
|
1835
|
+
|
|
1836
|
+
// Try exact name match first
|
|
1837
|
+
const exactMatch = milestones.find((m) => m.name === ref);
|
|
1838
|
+
if (exactMatch) {
|
|
1839
|
+
return { id: exactMatch.id, name: exactMatch.name };
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// Try case-insensitive match
|
|
1843
|
+
const lowerRef = ref.toLowerCase();
|
|
1844
|
+
const caseInsensitiveMatch = milestones.find((m) => m.name?.toLowerCase() === lowerRef);
|
|
1845
|
+
if (caseInsensitiveMatch) {
|
|
1846
|
+
return { id: caseInsensitiveMatch.id, name: caseInsensitiveMatch.name };
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
throw new Error(`Milestone not found: ${ref}. Available milestones: ${milestones.map((m) => m.name).join(', ')}`);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function normalizeIssueRefList(value) {
|
|
1853
|
+
if (value === undefined || value === null) return [];
|
|
1854
|
+
if (Array.isArray(value)) {
|
|
1855
|
+
return value.map((v) => String(v || '').trim()).filter(Boolean);
|
|
1856
|
+
}
|
|
1857
|
+
return String(value)
|
|
1858
|
+
.split(',')
|
|
1859
|
+
.map((v) => v.trim())
|
|
1860
|
+
.filter(Boolean);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// ===== QUERY FUNCTIONS =====
|
|
173
1864
|
|
|
174
1865
|
/**
|
|
175
|
-
*
|
|
176
|
-
* @param {
|
|
1866
|
+
* Fetch the current authenticated viewer
|
|
1867
|
+
* @param {LinearClient} client - Linear SDK client
|
|
1868
|
+
* @returns {Promise<{id: string, name: string}>}
|
|
177
1869
|
*/
|
|
178
|
-
function
|
|
179
|
-
|
|
1870
|
+
export async function fetchViewer(client) {
|
|
1871
|
+
return withLinearErrorHandling(async () => {
|
|
1872
|
+
const cacheKey = getClientCacheKey(client);
|
|
1873
|
+
const cached = getCache(viewerCache, cacheKey);
|
|
1874
|
+
if (cached) return cached;
|
|
180
1875
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
1876
|
+
const viewer = await client.viewer;
|
|
1877
|
+
const result = {
|
|
1878
|
+
id: viewer.id,
|
|
1879
|
+
name: viewer.name,
|
|
1880
|
+
displayName: viewer.displayName,
|
|
1881
|
+
};
|
|
185
1882
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (resetAt !== null) {
|
|
190
|
-
rateLimitState.resetAt = parseInt(resetAt, 10);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
1883
|
+
setCache(viewerCache, cacheKey, result, CACHE_TTL_MS.viewer);
|
|
1884
|
+
return result;
|
|
1885
|
+
}, 'fetchViewer');
|
|
193
1886
|
}
|
|
194
1887
|
|
|
1888
|
+
export const getViewer = fetchViewer;
|
|
1889
|
+
|
|
195
1890
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
1891
|
+
* Fetch issues in specific states, optionally filtered by assignee
|
|
1892
|
+
* OPTIMIZED: Uses rawRequest with custom GraphQL to fetch all relations in ONE request
|
|
1893
|
+
* @param {LinearClient} client - Linear SDK client
|
|
1894
|
+
* @param {string|null} assigneeId - Assignee ID to filter by (null = all assignees)
|
|
1895
|
+
* @param {Array<string>} openStates - List of state names to include
|
|
1896
|
+
* @param {number} limit - Maximum number of issues to fetch
|
|
1897
|
+
* @returns {Promise<{issues: Array, truncated: boolean}>}
|
|
198
1898
|
*/
|
|
199
|
-
export function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
usagePercent: null,
|
|
205
|
-
shouldWarn: false,
|
|
206
|
-
};
|
|
1899
|
+
export async function fetchIssues(client, assigneeId, openStates, limit) {
|
|
1900
|
+
return withLinearErrorHandling(async () => {
|
|
1901
|
+
const filter = {
|
|
1902
|
+
state: { name: { in: openStates } },
|
|
1903
|
+
};
|
|
207
1904
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const remaining = rateLimitState.remaining;
|
|
211
|
-
if (remaining !== null && remaining <= 1000) {
|
|
212
|
-
result.usagePercent = Math.round(((5000 - remaining) / 5000) * 100);
|
|
213
|
-
result.shouldWarn = remaining <= 500;
|
|
1905
|
+
if (assigneeId) {
|
|
1906
|
+
filter.assignee = { id: { eq: assigneeId } };
|
|
214
1907
|
}
|
|
215
|
-
}
|
|
216
1908
|
|
|
217
|
-
|
|
218
|
-
}
|
|
1909
|
+
// Use optimized rawRequest to fetch issues with ALL relations in ONE request
|
|
1910
|
+
const { data } = await executeOptimizedQuery(client, ISSUES_WITH_RELATIONS_QUERY, {
|
|
1911
|
+
first: limit,
|
|
1912
|
+
filter,
|
|
1913
|
+
});
|
|
219
1914
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
function checkRateLimitWarning() {
|
|
224
|
-
const now = Date.now();
|
|
225
|
-
// Only warn once per 30 seconds to avoid spam
|
|
226
|
-
if (now - rateLimitState.lastWarnAt < 30000) return;
|
|
1915
|
+
const nodes = data?.issues?.nodes || [];
|
|
1916
|
+
const pageInfo = data?.issues?.pageInfo;
|
|
1917
|
+
const hasNextPage = pageInfo?.hasNextPage ?? false;
|
|
227
1918
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
resetAt: status.resetTime,
|
|
234
|
-
usagePercent: status.usagePercent,
|
|
1919
|
+
// Transform raw GraphQL response directly - no lazy loading needed
|
|
1920
|
+
const issues = nodes.map(transformRawIssue);
|
|
1921
|
+
|
|
1922
|
+
debug('Fetched issues (optimized)', {
|
|
1923
|
+
issueCount: issues.length,
|
|
235
1924
|
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
1925
|
|
|
239
|
-
|
|
1926
|
+
const truncated = hasNextPage || nodes.length >= limit;
|
|
1927
|
+
if (truncated) {
|
|
1928
|
+
warn('Issues query may be truncated', {
|
|
1929
|
+
limit,
|
|
1930
|
+
returned: nodes.length,
|
|
1931
|
+
hasNextPage,
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
240
1934
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
function isLinearError(error) {
|
|
247
|
-
return error?.constructor?.name?.includes('LinearError') ||
|
|
248
|
-
error?.name?.includes('LinearError') ||
|
|
249
|
-
error?.type?.startsWith?.('Ratelimited') ||
|
|
250
|
-
error?.type?.startsWith?.('Forbidden') ||
|
|
251
|
-
error?.type?.startsWith?.('Authentication') ||
|
|
252
|
-
error?.type === 'invalid_request' ||
|
|
253
|
-
error?.type === 'NetworkError' ||
|
|
254
|
-
error?.type === 'InternalError';
|
|
1935
|
+
return {
|
|
1936
|
+
issues,
|
|
1937
|
+
truncated,
|
|
1938
|
+
};
|
|
1939
|
+
}, 'fetchIssues');
|
|
255
1940
|
}
|
|
256
1941
|
|
|
257
1942
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
1943
|
+
* Fetch issues by project and optional state filter
|
|
1944
|
+
* OPTIMIZED: Uses rawRequest with custom GraphQL to fetch all relations in ONE request
|
|
1945
|
+
* instead of N+1 requests (1 for list + 5 per issue for lazy-loaded relations)
|
|
1946
|
+
* @param {LinearClient} client - Linear SDK client
|
|
1947
|
+
* @param {string} projectId - Project ID to filter by
|
|
1948
|
+
* @param {Array<string>|null} states - List of state names to include (null = all states)
|
|
1949
|
+
* @param {Object} options
|
|
1950
|
+
* @param {string|null} options.assigneeId - Assignee ID to filter by (null = all assignees)
|
|
1951
|
+
* @param {string|null} options.teamId - Team ID to filter by (null = all teams)
|
|
1952
|
+
* @param {number} options.limit - Maximum number of issues to fetch
|
|
1953
|
+
* @returns {Promise<{issues: Array, truncated: boolean}>}
|
|
261
1954
|
*/
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
1955
|
+
export async function fetchIssuesByProject(client, projectId, states, options = {}) {
|
|
1956
|
+
return withLinearErrorHandling(async () => {
|
|
1957
|
+
const { assigneeId = null, teamId = null, limit = 20 } = options;
|
|
265
1958
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
? new Date(error.requestsResetAt).toLocaleTimeString()
|
|
270
|
-
: '1 hour';
|
|
1959
|
+
const filter = {
|
|
1960
|
+
project: { id: { eq: projectId } },
|
|
1961
|
+
};
|
|
271
1962
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
`Hint: Reduce request frequency or wait before retrying.`
|
|
276
|
-
);
|
|
277
|
-
}
|
|
1963
|
+
if (states && states.length > 0) {
|
|
1964
|
+
filter.state = { name: { in: states } };
|
|
1965
|
+
}
|
|
278
1966
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return new Error(
|
|
283
|
-
`${message}\nHint: Check your Linear API key or OAuth token permissions.`
|
|
284
|
-
);
|
|
285
|
-
}
|
|
1967
|
+
if (assigneeId) {
|
|
1968
|
+
filter.assignee = { id: { eq: assigneeId } };
|
|
1969
|
+
}
|
|
286
1970
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
`Network error while communicating with Linear API.\nHint: Check your internet connection and try again.`
|
|
291
|
-
);
|
|
292
|
-
}
|
|
1971
|
+
if (teamId) {
|
|
1972
|
+
filter.team = { id: { eq: teamId } };
|
|
1973
|
+
}
|
|
293
1974
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
1975
|
+
// Use optimized rawRequest to fetch issues with ALL relations in ONE request
|
|
1976
|
+
// This eliminates the N+1 problem where each issue triggered 5 additional API calls
|
|
1977
|
+
const { data } = await executeOptimizedQuery(client, ISSUES_WITH_RELATIONS_QUERY, {
|
|
1978
|
+
first: limit,
|
|
1979
|
+
filter,
|
|
1980
|
+
});
|
|
300
1981
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
`Linear API error: ${message}`
|
|
305
|
-
);
|
|
306
|
-
}
|
|
1982
|
+
const nodes = data?.issues?.nodes || [];
|
|
1983
|
+
const pageInfo = data?.issues?.pageInfo;
|
|
1984
|
+
const hasNextPage = pageInfo?.hasNextPage ?? false;
|
|
307
1985
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
1986
|
+
// Transform raw GraphQL response directly - no lazy loading needed
|
|
1987
|
+
const issues = nodes.map(transformRawIssue);
|
|
311
1988
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
originalError: error?.message,
|
|
326
|
-
errorType: error?.type,
|
|
327
|
-
formattedMessage: formatted.message,
|
|
1989
|
+
debug('Fetched issues by project (optimized)', {
|
|
1990
|
+
projectId,
|
|
1991
|
+
stateCount: states?.length ?? 0,
|
|
1992
|
+
issueCount: issues.length,
|
|
1993
|
+
truncated: hasNextPage,
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
const truncated = hasNextPage || nodes.length >= limit;
|
|
1997
|
+
if (truncated) {
|
|
1998
|
+
warn('Issues query may be truncated', {
|
|
1999
|
+
limit,
|
|
2000
|
+
returned: nodes.length,
|
|
2001
|
+
hasNextPage,
|
|
328
2002
|
});
|
|
329
|
-
throw formatted;
|
|
330
2003
|
}
|
|
331
|
-
|
|
332
|
-
|
|
2004
|
+
|
|
2005
|
+
return {
|
|
2006
|
+
issues,
|
|
2007
|
+
truncated,
|
|
2008
|
+
};
|
|
2009
|
+
}, 'fetchIssuesByProject');
|
|
333
2010
|
}
|
|
334
2011
|
|
|
335
2012
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* @param {Function} fn - Async handler function to wrap
|
|
341
|
-
* @param {string} operation - Description of the operation for error messages
|
|
342
|
-
* @returns {Promise<*>} Result of the wrapped function
|
|
343
|
-
* @example
|
|
344
|
-
* ```js
|
|
345
|
-
* export async function executeIssueList(client, params) {
|
|
346
|
-
* return withHandlerErrorHandling(async () => {
|
|
347
|
-
* // ... implementation
|
|
348
|
-
* }, 'executeIssueList');
|
|
349
|
-
* }
|
|
350
|
-
* ```
|
|
2013
|
+
* Fetch all accessible projects from Linear API
|
|
2014
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2015
|
+
* @param {{ includeArchived?: boolean }} options - Fetch options
|
|
2016
|
+
* @returns {Promise<Array<{id: string, name: string}>>}
|
|
351
2017
|
*/
|
|
352
|
-
export async function
|
|
2018
|
+
export async function fetchProjects(client, options = {}) {
|
|
353
2019
|
return withLinearErrorHandling(async () => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
2020
|
+
const { includeArchived = false, forceGraphql = false } = options;
|
|
2021
|
+
const cacheKey = getClientCacheKey(client);
|
|
2022
|
+
const scopedCacheKey = `${cacheKey}::projects::${includeArchived ? 'all' : 'active'}::${forceGraphql ? 'graphql' : 'sdk'}`;
|
|
2023
|
+
const cached = getCache(projectsCache, scopedCacheKey);
|
|
2024
|
+
if (cached) return cached;
|
|
2025
|
+
|
|
2026
|
+
let nodes = [];
|
|
2027
|
+
if (includeArchived || forceGraphql) {
|
|
2028
|
+
const data = await executeGraphQL(client, PROJECTS_LOOKUP_QUERY, {
|
|
2029
|
+
includeArchived,
|
|
361
2030
|
});
|
|
362
|
-
|
|
2031
|
+
nodes = data?.projects?.nodes ?? [];
|
|
2032
|
+
} else {
|
|
2033
|
+
const result = await client.projects();
|
|
2034
|
+
nodes = result.nodes ?? [];
|
|
363
2035
|
}
|
|
364
|
-
}, operation);
|
|
365
|
-
}
|
|
366
2036
|
|
|
367
|
-
|
|
2037
|
+
debug('Fetched Linear projects', {
|
|
2038
|
+
projectCount: nodes.length,
|
|
2039
|
+
includeArchived,
|
|
2040
|
+
projects: nodes.map((p) => ({ id: p.id, name: p.name })),
|
|
2041
|
+
});
|
|
368
2042
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
2043
|
+
const projects = nodes.map((p) => ({
|
|
2044
|
+
id: p.id,
|
|
2045
|
+
name: p.name,
|
|
2046
|
+
slugId: p.slugId ?? null,
|
|
2047
|
+
archivedAt: p.archivedAt ?? null,
|
|
2048
|
+
}));
|
|
2049
|
+
setCache(projectsCache, scopedCacheKey, projects, CACHE_TTL_MS.projects);
|
|
2050
|
+
return projects;
|
|
2051
|
+
}, 'fetchProjects');
|
|
374
2052
|
}
|
|
375
2053
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
2054
|
+
export async function fetchProjectDetails(client, projectRef, options = {}) {
|
|
2055
|
+
return withLinearErrorHandling(async () => {
|
|
2056
|
+
const { milestoneLimit = 10 } = options;
|
|
2057
|
+
const ref = String(projectRef || '').trim();
|
|
2058
|
+
const projectId = isLinearId(ref)
|
|
2059
|
+
? ref
|
|
2060
|
+
: (await resolveProjectRef(client, ref)).id;
|
|
2061
|
+
const data = await executeGraphQL(client, PROJECT_DETAILS_QUERY, {
|
|
2062
|
+
id: projectId,
|
|
2063
|
+
milestoneLimit,
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
if (!data?.project) {
|
|
2067
|
+
throw new Error(`Project not found: ${projectRef}`);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
return transformProject(data.project);
|
|
2071
|
+
}, 'fetchProjectDetails');
|
|
383
2072
|
}
|
|
384
2073
|
|
|
385
2074
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
2075
|
+
* Fetch available workspaces (organization context) from Linear API
|
|
2076
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2077
|
+
* @returns {Promise<Array<{id: string, name: string}>>}
|
|
388
2078
|
*/
|
|
389
|
-
async function
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
2079
|
+
export async function fetchWorkspaces(client) {
|
|
2080
|
+
return withLinearErrorHandling(async () => {
|
|
2081
|
+
const viewer = await client.viewer;
|
|
2082
|
+
const organization = await (viewer?.organization?.catch?.(() => null) ?? viewer?.organization ?? null);
|
|
393
2083
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return result || null;
|
|
2084
|
+
if (!organization) {
|
|
2085
|
+
debug('No organization available from viewer context');
|
|
2086
|
+
return [];
|
|
398
2087
|
}
|
|
399
2088
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
2089
|
+
const workspace = { id: organization.id, name: organization.name || organization.urlKey || 'Workspace' };
|
|
2090
|
+
|
|
2091
|
+
debug('Fetched Linear workspace from viewer organization', {
|
|
2092
|
+
workspace,
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
return [workspace];
|
|
2096
|
+
}, 'fetchWorkspaces');
|
|
405
2097
|
}
|
|
406
2098
|
|
|
407
2099
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
2100
|
+
* Fetch all accessible teams from Linear API
|
|
2101
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2102
|
+
* @returns {Promise<Array<{id: string, key: string, name: string}>>}
|
|
410
2103
|
*/
|
|
411
|
-
async function
|
|
412
|
-
|
|
2104
|
+
export async function fetchTeams(client) {
|
|
2105
|
+
return withLinearErrorHandling(async () => {
|
|
2106
|
+
const cacheKey = getClientCacheKey(client);
|
|
2107
|
+
const cached = getCache(teamsCache, cacheKey);
|
|
2108
|
+
if (cached) return cached;
|
|
413
2109
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const hasRelations = sdkIssue.state?.id || sdkIssue.assignee?.id || sdkIssue.project?.id;
|
|
2110
|
+
const result = await client.teams();
|
|
2111
|
+
const nodes = result.nodes ?? [];
|
|
417
2112
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
let assignee = null;
|
|
423
|
-
let projectMilestone = null;
|
|
2113
|
+
debug('Fetched Linear teams', {
|
|
2114
|
+
teamCount: nodes.length,
|
|
2115
|
+
teams: nodes.map((t) => ({ id: t.id, key: t.key, name: t.name })),
|
|
2116
|
+
});
|
|
424
2117
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
2118
|
+
const teams = nodes.map(t => ({ id: t.id, key: t.key, name: t.name }));
|
|
2119
|
+
setCache(teamsCache, cacheKey, teams, CACHE_TTL_MS.teams);
|
|
2120
|
+
return teams;
|
|
2121
|
+
}, 'fetchTeams');
|
|
2122
|
+
}
|
|
430
2123
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
2124
|
+
/**
|
|
2125
|
+
* Resolve a team reference (key, name, or ID) to a team object
|
|
2126
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2127
|
+
* @param {string} teamRef - Team key, name, or ID
|
|
2128
|
+
* @returns {Promise<{id: string, key: string, name: string}>}
|
|
2129
|
+
*/
|
|
2130
|
+
export async function resolveTeamRef(client, teamRef) {
|
|
2131
|
+
const ref = String(teamRef || '').trim();
|
|
2132
|
+
if (!ref) {
|
|
2133
|
+
throw new Error('Missing team reference');
|
|
435
2134
|
}
|
|
436
2135
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
2136
|
+
// If it looks like a Linear ID (UUID), try a minimal GraphQL lookup first.
|
|
2137
|
+
if (isLinearId(ref)) {
|
|
2138
|
+
try {
|
|
2139
|
+
const direct = await fetchTeamMinimal(client, ref);
|
|
2140
|
+
if (direct) {
|
|
2141
|
+
return direct;
|
|
2142
|
+
}
|
|
2143
|
+
} catch {
|
|
2144
|
+
// fall back to cached/full-team lookup below
|
|
2145
|
+
}
|
|
442
2146
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
2147
|
+
const teams = await fetchTeams(client);
|
|
2148
|
+
const byId = teams.find((t) => t.id === ref);
|
|
2149
|
+
if (byId) {
|
|
2150
|
+
return byId;
|
|
2151
|
+
}
|
|
2152
|
+
throw new Error(`Team not found with ID: ${ref}`);
|
|
447
2153
|
}
|
|
448
2154
|
|
|
449
|
-
|
|
450
|
-
projectMilestone = sdkIssue.projectMilestone;
|
|
451
|
-
} else if (sdkIssue._data?.projectMilestone) {
|
|
452
|
-
projectMilestone = { id: sdkIssue._data.projectMilestone?.id, name: sdkIssue._data.projectMilestone?.name };
|
|
453
|
-
}
|
|
2155
|
+
const teams = await fetchTeams(client);
|
|
454
2156
|
|
|
455
|
-
//
|
|
456
|
-
const
|
|
2157
|
+
// Try exact key match (e.g., "ENG")
|
|
2158
|
+
const byKey = teams.find((t) => t.key === ref);
|
|
2159
|
+
if (byKey) {
|
|
2160
|
+
return byKey;
|
|
2161
|
+
}
|
|
457
2162
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
safeResolveRelation(sdkIssue, 'project'),
|
|
464
|
-
safeResolveRelation(sdkIssue, 'assignee'),
|
|
465
|
-
safeResolveRelation(sdkIssue, 'projectMilestone'),
|
|
466
|
-
]);
|
|
2163
|
+
// Try exact name match
|
|
2164
|
+
const exactName = teams.find((t) => t.name === ref);
|
|
2165
|
+
if (exactName) {
|
|
2166
|
+
return exactName;
|
|
2167
|
+
}
|
|
467
2168
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
2169
|
+
// Try case-insensitive key or name match
|
|
2170
|
+
const lowerRef = ref.toLowerCase();
|
|
2171
|
+
const insensitiveMatch = teams.find(
|
|
2172
|
+
(t) => t.key?.toLowerCase() === lowerRef || t.name?.toLowerCase() === lowerRef
|
|
2173
|
+
);
|
|
2174
|
+
if (insensitiveMatch) {
|
|
2175
|
+
return insensitiveMatch;
|
|
473
2176
|
}
|
|
474
2177
|
|
|
475
|
-
|
|
476
|
-
id: sdkIssue.id,
|
|
477
|
-
identifier: sdkIssue.identifier,
|
|
478
|
-
title: sdkIssue.title,
|
|
479
|
-
description: sdkIssue.description,
|
|
480
|
-
url: sdkIssue.url,
|
|
481
|
-
branchName: sdkIssue.branchName,
|
|
482
|
-
priority: sdkIssue.priority,
|
|
483
|
-
state: state ? { id: state.id, name: state.name, type: state.type } : null,
|
|
484
|
-
team: team ? { id: team.id, key: team.key, name: team.name } : null,
|
|
485
|
-
project: project ? { id: project.id, name: project.name } : null,
|
|
486
|
-
projectMilestone: projectMilestone ? { id: projectMilestone.id, name: projectMilestone.name } : null,
|
|
487
|
-
assignee: assignee ? { id: assignee.id, name: assignee.name, displayName: assignee.displayName } : null,
|
|
488
|
-
};
|
|
2178
|
+
throw new Error(`Team not found: ${ref}. Available teams: ${teams.map((t) => `${t.key} (${t.name})`).join(', ')}`);
|
|
489
2179
|
}
|
|
490
2180
|
|
|
491
2181
|
/**
|
|
492
|
-
* Resolve
|
|
2182
|
+
* Resolve an issue by ID or identifier
|
|
2183
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2184
|
+
* @param {string} issueRef - Issue identifier (ABC-123) or Linear issue ID
|
|
2185
|
+
* @returns {Promise<Object>} Resolved issue object
|
|
493
2186
|
*/
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (!target) return null;
|
|
2187
|
+
export async function resolveIssue(client, issueRef) {
|
|
2188
|
+
return withLinearErrorHandling(async () => {
|
|
2189
|
+
const lookup = normalizeIssueLookupInput(issueRef);
|
|
498
2190
|
|
|
499
|
-
|
|
500
|
-
|
|
2191
|
+
try {
|
|
2192
|
+
const issue = await fetchIssueMinimal(client, lookup);
|
|
2193
|
+
if (issue) {
|
|
2194
|
+
return issue;
|
|
2195
|
+
}
|
|
2196
|
+
} catch {
|
|
2197
|
+
// Fall through to not-found error below
|
|
2198
|
+
}
|
|
501
2199
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
2200
|
+
throw new Error(`Issue not found: ${lookup}`);
|
|
2201
|
+
}, 'resolveIssue');
|
|
2202
|
+
}
|
|
505
2203
|
|
|
506
|
-
|
|
507
|
-
|
|
2204
|
+
/**
|
|
2205
|
+
* Get workflow states for a team
|
|
2206
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2207
|
+
* @param {string} teamRef - Team ID or key
|
|
2208
|
+
* @returns {Promise<Array<{id: string, name: string, type: string}>>}
|
|
2209
|
+
*/
|
|
2210
|
+
export async function getTeamWorkflowStates(client, teamRef) {
|
|
2211
|
+
return withLinearErrorHandling(async () => {
|
|
2212
|
+
const cacheKey = `${getClientCacheKey(client)}::${teamRef}`;
|
|
2213
|
+
const cached = getCache(teamStatesCache, cacheKey);
|
|
2214
|
+
if (cached) return cached;
|
|
508
2215
|
|
|
509
|
-
|
|
2216
|
+
const mapped = await fetchTeamStatesByQuery(client, teamRef);
|
|
2217
|
+
if (!mapped) {
|
|
2218
|
+
throw new Error(`Team not found: ${teamRef}`);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
setCache(teamStatesCache, cacheKey, mapped, CACHE_TTL_MS.teamStates);
|
|
2222
|
+
return mapped;
|
|
2223
|
+
}, 'getTeamWorkflowStates');
|
|
510
2224
|
}
|
|
511
2225
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
2226
|
+
/**
|
|
2227
|
+
* Resolve a project reference (name or ID) to a project object
|
|
2228
|
+
* @param {LinearClient} client - Linear SDK client
|
|
2229
|
+
* @param {string} projectRef - Project name or ID
|
|
2230
|
+
* @param {{ includeArchived?: boolean }} options - Lookup options
|
|
2231
|
+
* @returns {Promise<{id: string, name: string}>}
|
|
2232
|
+
*/
|
|
2233
|
+
export async function resolveProjectRef(client, projectRef, options = {}) {
|
|
2234
|
+
const ref = String(projectRef || '').trim();
|
|
2235
|
+
const { includeArchived = false } = options;
|
|
2236
|
+
if (!ref) {
|
|
2237
|
+
throw new Error('Missing project reference');
|
|
2238
|
+
}
|
|
515
2239
|
|
|
516
|
-
|
|
517
|
-
if (
|
|
2240
|
+
// If it looks like a Linear ID (UUID), try a minimal GraphQL lookup first.
|
|
2241
|
+
if (isLinearId(ref)) {
|
|
2242
|
+
try {
|
|
2243
|
+
const direct = await fetchProjectMinimal(client, ref);
|
|
2244
|
+
if (direct) {
|
|
2245
|
+
return direct;
|
|
2246
|
+
}
|
|
2247
|
+
} catch {
|
|
2248
|
+
// fall back to cached/full-project lookup below
|
|
2249
|
+
}
|
|
518
2250
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
2251
|
+
const projectsById = await fetchProjects(client, { includeArchived });
|
|
2252
|
+
const byId = projectsById.find((p) => p.id === ref);
|
|
2253
|
+
if (byId) {
|
|
2254
|
+
return byId;
|
|
2255
|
+
}
|
|
2256
|
+
throw new Error(`Project not found with ID: ${ref}`);
|
|
2257
|
+
}
|
|
522
2258
|
|
|
523
|
-
|
|
524
|
-
|
|
2259
|
+
const lookupCandidates = getProjectLookupCandidates(ref);
|
|
2260
|
+
const lookupValue = lookupCandidates[0] || ref;
|
|
2261
|
+
const shouldUseGraphqlLookup = lookupValue !== ref;
|
|
2262
|
+
const projects = await fetchProjects(client, {
|
|
2263
|
+
includeArchived,
|
|
2264
|
+
forceGraphql: shouldUseGraphqlLookup,
|
|
2265
|
+
});
|
|
525
2266
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
return
|
|
2267
|
+
// Try exact name match
|
|
2268
|
+
const exactName = projects.find((p) => p.name === ref);
|
|
2269
|
+
if (exactName) {
|
|
2270
|
+
return exactName;
|
|
530
2271
|
}
|
|
531
|
-
return String(value)
|
|
532
|
-
.split(',')
|
|
533
|
-
.map((v) => v.trim())
|
|
534
|
-
.filter(Boolean);
|
|
535
|
-
}
|
|
536
2272
|
|
|
537
|
-
//
|
|
2273
|
+
// Try exact slug match
|
|
2274
|
+
const exactSlug = projects.find((p) => lookupCandidates.includes(p.slugId));
|
|
2275
|
+
if (exactSlug) {
|
|
2276
|
+
return exactSlug;
|
|
2277
|
+
}
|
|
538
2278
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
return withLinearErrorHandling(async () => {
|
|
546
|
-
const cacheKey = getClientCacheKey(client);
|
|
547
|
-
const cached = getCache(viewerCache, cacheKey);
|
|
548
|
-
if (cached) return cached;
|
|
2279
|
+
// Try case-insensitive name match
|
|
2280
|
+
const lowerRef = ref.toLowerCase();
|
|
2281
|
+
const insensitiveName = projects.find((p) => p.name?.toLowerCase() === lowerRef);
|
|
2282
|
+
if (insensitiveName) {
|
|
2283
|
+
return insensitiveName;
|
|
2284
|
+
}
|
|
549
2285
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
2286
|
+
// Try case-insensitive slug match
|
|
2287
|
+
const lowerLookupValues = lookupCandidates.map((candidate) => candidate.toLowerCase());
|
|
2288
|
+
const insensitiveSlug = projects.find((p) => p.slugId && lowerLookupValues.includes(p.slugId.toLowerCase()));
|
|
2289
|
+
if (insensitiveSlug) {
|
|
2290
|
+
return insensitiveSlug;
|
|
2291
|
+
}
|
|
556
2292
|
|
|
557
|
-
|
|
558
|
-
return result;
|
|
559
|
-
}, 'fetchViewer');
|
|
2293
|
+
throw new Error(`Project not found: ${ref}. Available projects: ${projects.map((p) => p.name).join(', ')}`);
|
|
560
2294
|
}
|
|
561
2295
|
|
|
562
|
-
export
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Fetch issues in specific states, optionally filtered by assignee
|
|
566
|
-
* OPTIMIZED: Uses rawRequest with custom GraphQL to fetch all relations in ONE request
|
|
567
|
-
* @param {LinearClient} client - Linear SDK client
|
|
568
|
-
* @param {string|null} assigneeId - Assignee ID to filter by (null = all assignees)
|
|
569
|
-
* @param {Array<string>} openStates - List of state names to include
|
|
570
|
-
* @param {number} limit - Maximum number of issues to fetch
|
|
571
|
-
* @returns {Promise<{issues: Array, truncated: boolean}>}
|
|
572
|
-
*/
|
|
573
|
-
export async function fetchIssues(client, assigneeId, openStates, limit) {
|
|
2296
|
+
export async function createProject(client, input) {
|
|
574
2297
|
return withLinearErrorHandling(async () => {
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
2298
|
+
const name = String(input.name || '').trim();
|
|
2299
|
+
if (!name) {
|
|
2300
|
+
throw new Error('Missing required field: name');
|
|
2301
|
+
}
|
|
578
2302
|
|
|
579
|
-
|
|
580
|
-
|
|
2303
|
+
const teamIds = Array.isArray(input.teamIds)
|
|
2304
|
+
? input.teamIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2305
|
+
: [];
|
|
2306
|
+
|
|
2307
|
+
if (teamIds.length === 0) {
|
|
2308
|
+
throw new Error('Missing required field: teamIds');
|
|
581
2309
|
}
|
|
582
2310
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
});
|
|
2311
|
+
const createInput = {
|
|
2312
|
+
name,
|
|
2313
|
+
teamIds,
|
|
2314
|
+
};
|
|
588
2315
|
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
2316
|
+
for (const field of ['description', 'color', 'icon', 'leadId', 'startDate', 'targetDate']) {
|
|
2317
|
+
if (input[field] !== undefined) {
|
|
2318
|
+
createInput[field] = input[field];
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
592
2321
|
|
|
593
|
-
|
|
594
|
-
|
|
2322
|
+
if (input.priority !== undefined) {
|
|
2323
|
+
createInput.priority = input.priority;
|
|
2324
|
+
}
|
|
595
2325
|
|
|
596
|
-
|
|
597
|
-
|
|
2326
|
+
const payload = await executeGraphQL(client, PROJECT_CREATE_MUTATION, {
|
|
2327
|
+
input: createInput,
|
|
598
2328
|
});
|
|
599
2329
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
warn('Issues query may be truncated', {
|
|
603
|
-
limit,
|
|
604
|
-
returned: nodes.length,
|
|
605
|
-
hasNextPage,
|
|
606
|
-
});
|
|
2330
|
+
if (!payload?.projectCreate?.success || !payload?.projectCreate?.project?.id) {
|
|
2331
|
+
throw new Error('Failed to create project');
|
|
607
2332
|
}
|
|
608
2333
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
};
|
|
613
|
-
}, 'fetchIssues');
|
|
2334
|
+
invalidateProjectsCache(client);
|
|
2335
|
+
return fetchProjectDetails(client, payload.projectCreate.project.id);
|
|
2336
|
+
}, 'createProject');
|
|
614
2337
|
}
|
|
615
2338
|
|
|
616
|
-
|
|
617
|
-
* Fetch issues by project and optional state filter
|
|
618
|
-
* OPTIMIZED: Uses rawRequest with custom GraphQL to fetch all relations in ONE request
|
|
619
|
-
* instead of N+1 requests (1 for list + 5 per issue for lazy-loaded relations)
|
|
620
|
-
* @param {LinearClient} client - Linear SDK client
|
|
621
|
-
* @param {string} projectId - Project ID to filter by
|
|
622
|
-
* @param {Array<string>|null} states - List of state names to include (null = all states)
|
|
623
|
-
* @param {Object} options
|
|
624
|
-
* @param {string|null} options.assigneeId - Assignee ID to filter by (null = all assignees)
|
|
625
|
-
* @param {string|null} options.teamId - Team ID to filter by (null = all teams)
|
|
626
|
-
* @param {number} options.limit - Maximum number of issues to fetch
|
|
627
|
-
* @returns {Promise<{issues: Array, truncated: boolean}>}
|
|
628
|
-
*/
|
|
629
|
-
export async function fetchIssuesByProject(client, projectId, states, options = {}) {
|
|
2339
|
+
export async function updateProject(client, projectRef, patch = {}) {
|
|
630
2340
|
return withLinearErrorHandling(async () => {
|
|
631
|
-
const
|
|
2341
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
2342
|
+
const updateInput = {};
|
|
632
2343
|
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
2344
|
+
for (const field of ['name', 'description', 'content', 'color', 'icon', 'startDate', 'targetDate']) {
|
|
2345
|
+
if (patch[field] !== undefined) {
|
|
2346
|
+
updateInput[field] = patch[field];
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
636
2349
|
|
|
637
|
-
if (
|
|
638
|
-
|
|
2350
|
+
if (patch.priority !== undefined) {
|
|
2351
|
+
updateInput.priority = patch.priority;
|
|
639
2352
|
}
|
|
640
2353
|
|
|
641
|
-
if (
|
|
642
|
-
|
|
2354
|
+
if (patch.leadId !== undefined) {
|
|
2355
|
+
updateInput.leadId = patch.leadId;
|
|
643
2356
|
}
|
|
644
2357
|
|
|
645
|
-
if (
|
|
646
|
-
|
|
2358
|
+
if (patch.teamIds !== undefined) {
|
|
2359
|
+
updateInput.teamIds = patch.teamIds;
|
|
647
2360
|
}
|
|
648
2361
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
2362
|
+
if (Object.keys(updateInput).length === 0) {
|
|
2363
|
+
throw new Error('No update fields provided');
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_MUTATION, {
|
|
2367
|
+
id: resolved.id,
|
|
2368
|
+
input: updateInput,
|
|
654
2369
|
});
|
|
655
2370
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
2371
|
+
if (!payload?.projectUpdate?.success || !payload?.projectUpdate?.project?.id) {
|
|
2372
|
+
throw new Error('Failed to update project');
|
|
2373
|
+
}
|
|
659
2374
|
|
|
660
|
-
|
|
661
|
-
const
|
|
2375
|
+
invalidateProjectsCache(client);
|
|
2376
|
+
const project = await fetchProjectDetails(client, payload.projectUpdate.project.id);
|
|
662
2377
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
2378
|
+
return {
|
|
2379
|
+
project,
|
|
2380
|
+
changed: Object.keys(updateInput),
|
|
2381
|
+
};
|
|
2382
|
+
}, 'updateProject');
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
export async function deleteProject(client, projectRef) {
|
|
2386
|
+
return withLinearErrorHandling(async () => {
|
|
2387
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
2388
|
+
const payload = await executeGraphQL(client, PROJECT_DELETE_MUTATION, {
|
|
2389
|
+
id: resolved.id,
|
|
668
2390
|
});
|
|
669
2391
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
warn('Issues query may be truncated', {
|
|
673
|
-
limit,
|
|
674
|
-
returned: nodes.length,
|
|
675
|
-
hasNextPage,
|
|
676
|
-
});
|
|
2392
|
+
if (!payload?.projectDelete?.success) {
|
|
2393
|
+
throw new Error('Failed to delete project');
|
|
677
2394
|
}
|
|
678
2395
|
|
|
2396
|
+
invalidateProjectsCache(client);
|
|
2397
|
+
|
|
679
2398
|
return {
|
|
680
|
-
|
|
681
|
-
|
|
2399
|
+
success: true,
|
|
2400
|
+
projectId: resolved.id,
|
|
2401
|
+
name: resolved.name,
|
|
2402
|
+
entity: transformProject(payload.projectDelete.entity),
|
|
682
2403
|
};
|
|
683
|
-
}, '
|
|
2404
|
+
}, 'deleteProject');
|
|
684
2405
|
}
|
|
685
2406
|
|
|
686
|
-
|
|
687
|
-
* Fetch all accessible projects from Linear API
|
|
688
|
-
* @param {LinearClient} client - Linear SDK client
|
|
689
|
-
* @returns {Promise<Array<{id: string, name: string}>>}
|
|
690
|
-
*/
|
|
691
|
-
export async function fetchProjects(client) {
|
|
2407
|
+
export async function archiveProject(client, projectRef) {
|
|
692
2408
|
return withLinearErrorHandling(async () => {
|
|
693
|
-
const
|
|
694
|
-
const
|
|
695
|
-
|
|
2409
|
+
const resolved = await resolveProjectRef(client, projectRef);
|
|
2410
|
+
const payload = await executeGraphQL(client, PROJECT_ARCHIVE_MUTATION, {
|
|
2411
|
+
id: resolved.id,
|
|
2412
|
+
});
|
|
696
2413
|
|
|
697
|
-
|
|
698
|
-
|
|
2414
|
+
if (!payload?.projectArchiveResult?.success) {
|
|
2415
|
+
throw new Error('Failed to archive project');
|
|
2416
|
+
}
|
|
699
2417
|
|
|
700
|
-
|
|
701
|
-
projectCount: nodes.length,
|
|
702
|
-
projects: nodes.map((p) => ({ id: p.id, name: p.name })),
|
|
703
|
-
});
|
|
2418
|
+
invalidateProjectsCache(client);
|
|
704
2419
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
2420
|
+
return {
|
|
2421
|
+
success: true,
|
|
2422
|
+
projectId: resolved.id,
|
|
2423
|
+
name: resolved.name,
|
|
2424
|
+
entity: transformProject(payload.projectArchiveResult.entity),
|
|
2425
|
+
};
|
|
2426
|
+
}, 'archiveProject');
|
|
709
2427
|
}
|
|
710
2428
|
|
|
711
|
-
|
|
712
|
-
* Fetch available workspaces (organization context) from Linear API
|
|
713
|
-
* @param {LinearClient} client - Linear SDK client
|
|
714
|
-
* @returns {Promise<Array<{id: string, name: string}>>}
|
|
715
|
-
*/
|
|
716
|
-
export async function fetchWorkspaces(client) {
|
|
2429
|
+
export async function unarchiveProject(client, projectRef) {
|
|
717
2430
|
return withLinearErrorHandling(async () => {
|
|
718
|
-
const
|
|
719
|
-
const
|
|
2431
|
+
const ref = String(projectRef || '').trim();
|
|
2432
|
+
const resolved = isLinearId(ref)
|
|
2433
|
+
? { id: ref, name: null }
|
|
2434
|
+
: await resolveProjectRef(client, ref, { includeArchived: true });
|
|
720
2435
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
2436
|
+
const payload = await executeGraphQL(client, PROJECT_UNARCHIVE_MUTATION, {
|
|
2437
|
+
id: resolved.id,
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
if (!payload?.projectUnarchive?.success) {
|
|
2441
|
+
throw new Error('Failed to unarchive project');
|
|
724
2442
|
}
|
|
725
2443
|
|
|
726
|
-
|
|
2444
|
+
invalidateProjectsCache(client);
|
|
2445
|
+
const project = await fetchProjectDetails(client, resolved.id);
|
|
727
2446
|
|
|
728
|
-
|
|
729
|
-
|
|
2447
|
+
return {
|
|
2448
|
+
success: true,
|
|
2449
|
+
project,
|
|
2450
|
+
};
|
|
2451
|
+
}, 'unarchiveProject');
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
export async function fetchProjectUpdates(client, projectRef, options = {}) {
|
|
2455
|
+
return withLinearErrorHandling(async () => {
|
|
2456
|
+
const includeArchived = options.includeArchived === true;
|
|
2457
|
+
const limit = normalizePositiveInteger(options.limit, 'limit', 10);
|
|
2458
|
+
const resolved = await resolveProjectRef(client, projectRef, { includeArchived });
|
|
2459
|
+
const data = await executeGraphQL(client, PROJECT_UPDATES_BY_PROJECT_QUERY, {
|
|
2460
|
+
id: resolved.id,
|
|
2461
|
+
first: limit,
|
|
2462
|
+
includeArchived,
|
|
730
2463
|
});
|
|
731
2464
|
|
|
732
|
-
|
|
733
|
-
|
|
2465
|
+
const nodes = data?.project?.projectUpdates?.nodes || [];
|
|
2466
|
+
return {
|
|
2467
|
+
project: {
|
|
2468
|
+
id: data?.project?.id || resolved.id,
|
|
2469
|
+
name: data?.project?.name || resolved.name,
|
|
2470
|
+
},
|
|
2471
|
+
updates: nodes.map(transformProjectUpdate),
|
|
2472
|
+
};
|
|
2473
|
+
}, 'fetchProjectUpdates');
|
|
734
2474
|
}
|
|
735
2475
|
|
|
736
|
-
|
|
737
|
-
* Fetch all accessible teams from Linear API
|
|
738
|
-
* @param {LinearClient} client - Linear SDK client
|
|
739
|
-
* @returns {Promise<Array<{id: string, key: string, name: string}>>}
|
|
740
|
-
*/
|
|
741
|
-
export async function fetchTeams(client) {
|
|
2476
|
+
export async function fetchProjectUpdateDetails(client, projectUpdateId) {
|
|
742
2477
|
return withLinearErrorHandling(async () => {
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
2478
|
+
const id = String(projectUpdateId || '').trim();
|
|
2479
|
+
if (!id) {
|
|
2480
|
+
throw new Error('Missing required field: projectUpdate');
|
|
2481
|
+
}
|
|
746
2482
|
|
|
747
|
-
const
|
|
748
|
-
const nodes = result.nodes ?? [];
|
|
2483
|
+
const data = await executeGraphQL(client, PROJECT_UPDATE_DETAILS_QUERY, { id });
|
|
749
2484
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
2485
|
+
if (!data?.projectUpdate) {
|
|
2486
|
+
throw new Error(`Project update not found: ${id}`);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
return transformProjectUpdate(data.projectUpdate);
|
|
2490
|
+
}, 'fetchProjectUpdateDetails');
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
export async function createProjectUpdate(client, input) {
|
|
2494
|
+
return withLinearErrorHandling(async () => {
|
|
2495
|
+
const projectId = String(input.projectId || '').trim();
|
|
2496
|
+
if (!projectId) {
|
|
2497
|
+
throw new Error('Missing required field: projectId');
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
const createInput = { projectId };
|
|
2501
|
+
if (input.body !== undefined) createInput.body = String(input.body);
|
|
2502
|
+
if (input.health !== undefined) createInput.health = normalizeProjectUpdateHealth(input.health);
|
|
2503
|
+
if (input.isDiffHidden !== undefined) createInput.isDiffHidden = input.isDiffHidden;
|
|
2504
|
+
|
|
2505
|
+
if (createInput.body === undefined && createInput.health === undefined) {
|
|
2506
|
+
throw new Error('At least one of body or health is required');
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_CREATE_MUTATION, {
|
|
2510
|
+
input: createInput,
|
|
753
2511
|
});
|
|
754
2512
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
2513
|
+
if (!payload?.projectUpdateCreate?.success || !payload?.projectUpdateCreate?.projectUpdate?.id) {
|
|
2514
|
+
throw new Error('Failed to create project update');
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
return fetchProjectUpdateDetails(client, payload.projectUpdateCreate.projectUpdate.id);
|
|
2518
|
+
}, 'createProjectUpdate');
|
|
759
2519
|
}
|
|
760
2520
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
2521
|
+
export async function updateProjectUpdate(client, projectUpdateId, patch = {}) {
|
|
2522
|
+
return withLinearErrorHandling(async () => {
|
|
2523
|
+
const id = String(projectUpdateId || '').trim();
|
|
2524
|
+
if (!id) {
|
|
2525
|
+
throw new Error('Missing required field: projectUpdate');
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
const updateInput = {};
|
|
2529
|
+
if (patch.body !== undefined) updateInput.body = String(patch.body);
|
|
2530
|
+
if (patch.health !== undefined) updateInput.health = normalizeProjectUpdateHealth(patch.health);
|
|
2531
|
+
if (patch.isDiffHidden !== undefined) updateInput.isDiffHidden = patch.isDiffHidden;
|
|
2532
|
+
|
|
2533
|
+
if (Object.keys(updateInput).length === 0) {
|
|
2534
|
+
throw new Error('No update fields provided');
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_UPDATE_MUTATION, {
|
|
2538
|
+
id,
|
|
2539
|
+
input: updateInput,
|
|
2540
|
+
});
|
|
772
2541
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
try {
|
|
776
|
-
const direct = await client.team(ref);
|
|
777
|
-
if (direct) {
|
|
778
|
-
return { id: direct.id, key: direct.key, name: direct.name };
|
|
779
|
-
}
|
|
780
|
-
} catch {
|
|
781
|
-
// fall back to cached/full-team lookup below
|
|
2542
|
+
if (!payload?.projectUpdateUpdate?.success || !payload?.projectUpdateUpdate?.projectUpdate?.id) {
|
|
2543
|
+
throw new Error('Failed to update project update');
|
|
782
2544
|
}
|
|
783
2545
|
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
2546
|
+
const projectUpdate = await fetchProjectUpdateDetails(client, payload.projectUpdateUpdate.projectUpdate.id);
|
|
2547
|
+
return {
|
|
2548
|
+
projectUpdate,
|
|
2549
|
+
changed: Object.keys(updateInput),
|
|
2550
|
+
};
|
|
2551
|
+
}, 'updateProjectUpdate');
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
export async function archiveProjectUpdate(client, projectUpdateId) {
|
|
2555
|
+
return withLinearErrorHandling(async () => {
|
|
2556
|
+
const id = String(projectUpdateId || '').trim();
|
|
2557
|
+
if (!id) {
|
|
2558
|
+
throw new Error('Missing required field: projectUpdate');
|
|
787
2559
|
}
|
|
788
|
-
throw new Error(`Team not found with ID: ${ref}`);
|
|
789
|
-
}
|
|
790
2560
|
|
|
791
|
-
|
|
2561
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_ARCHIVE_MUTATION, { id });
|
|
2562
|
+
if (!payload?.projectUpdateArchive?.success) {
|
|
2563
|
+
throw new Error('Failed to archive project update');
|
|
2564
|
+
}
|
|
792
2565
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
2566
|
+
return {
|
|
2567
|
+
success: true,
|
|
2568
|
+
projectUpdateId: id,
|
|
2569
|
+
};
|
|
2570
|
+
}, 'archiveProjectUpdate');
|
|
2571
|
+
}
|
|
798
2572
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
2573
|
+
export async function unarchiveProjectUpdate(client, projectUpdateId) {
|
|
2574
|
+
return withLinearErrorHandling(async () => {
|
|
2575
|
+
const id = String(projectUpdateId || '').trim();
|
|
2576
|
+
if (!id) {
|
|
2577
|
+
throw new Error('Missing required field: projectUpdate');
|
|
2578
|
+
}
|
|
804
2579
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
);
|
|
810
|
-
if (insensitiveMatch) {
|
|
811
|
-
return insensitiveMatch;
|
|
812
|
-
}
|
|
2580
|
+
const payload = await executeGraphQL(client, PROJECT_UPDATE_UNARCHIVE_MUTATION, { id });
|
|
2581
|
+
if (!payload?.projectUpdateUnarchive?.success) {
|
|
2582
|
+
throw new Error('Failed to unarchive project update');
|
|
2583
|
+
}
|
|
813
2584
|
|
|
814
|
-
|
|
2585
|
+
const projectUpdate = await fetchProjectUpdateDetails(client, id);
|
|
2586
|
+
return {
|
|
2587
|
+
success: true,
|
|
2588
|
+
projectUpdate,
|
|
2589
|
+
};
|
|
2590
|
+
}, 'unarchiveProjectUpdate');
|
|
815
2591
|
}
|
|
816
2592
|
|
|
817
|
-
|
|
818
|
-
* Resolve an issue by ID or identifier
|
|
819
|
-
* @param {LinearClient} client - Linear SDK client
|
|
820
|
-
* @param {string} issueRef - Issue identifier (ABC-123) or Linear issue ID
|
|
821
|
-
* @returns {Promise<Object>} Resolved issue object
|
|
822
|
-
*/
|
|
823
|
-
export async function resolveIssue(client, issueRef) {
|
|
2593
|
+
export async function fetchDocumentDetails(client, documentRef) {
|
|
824
2594
|
return withLinearErrorHandling(async () => {
|
|
825
|
-
const
|
|
2595
|
+
const id = String(documentRef || '').trim();
|
|
2596
|
+
if (!id) {
|
|
2597
|
+
throw new Error('Missing required field: document');
|
|
2598
|
+
}
|
|
826
2599
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return transformIssue(issue);
|
|
832
|
-
}
|
|
833
|
-
} catch (err) {
|
|
834
|
-
// Fall through to error
|
|
2600
|
+
const data = await executeGraphQL(client, DOCUMENT_DETAILS_QUERY, { id });
|
|
2601
|
+
|
|
2602
|
+
if (!data?.document) {
|
|
2603
|
+
throw new Error(`Document not found: ${id}`);
|
|
835
2604
|
}
|
|
836
2605
|
|
|
837
|
-
|
|
838
|
-
}, '
|
|
2606
|
+
return transformDocument(data.document);
|
|
2607
|
+
}, 'fetchDocumentDetails');
|
|
839
2608
|
}
|
|
840
2609
|
|
|
841
|
-
|
|
842
|
-
* Get workflow states for a team
|
|
843
|
-
* @param {LinearClient} client - Linear SDK client
|
|
844
|
-
* @param {string} teamRef - Team ID or key
|
|
845
|
-
* @returns {Promise<Array<{id: string, name: string, type: string}>>}
|
|
846
|
-
*/
|
|
847
|
-
export async function getTeamWorkflowStates(client, teamRef) {
|
|
2610
|
+
export async function createDocument(client, input = {}) {
|
|
848
2611
|
return withLinearErrorHandling(async () => {
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
2612
|
+
const title = String(input.title || '').trim();
|
|
2613
|
+
if (!title) {
|
|
2614
|
+
throw new Error('Missing required field: title');
|
|
2615
|
+
}
|
|
852
2616
|
|
|
853
|
-
const
|
|
854
|
-
if (
|
|
855
|
-
|
|
2617
|
+
const createInput = { title };
|
|
2618
|
+
if (input.projectId !== undefined) createInput.projectId = input.projectId;
|
|
2619
|
+
if (input.issueId !== undefined) createInput.issueId = input.issueId;
|
|
2620
|
+
|
|
2621
|
+
if (!createInput.projectId && !createInput.issueId) {
|
|
2622
|
+
throw new Error('Document create requires either projectId or issueId');
|
|
856
2623
|
}
|
|
857
2624
|
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}));
|
|
2625
|
+
for (const field of ['content', 'icon', 'color']) {
|
|
2626
|
+
if (input[field] !== undefined) {
|
|
2627
|
+
createInput[field] = input[field];
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
864
2630
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
2631
|
+
const payload = await executeGraphQL(client, DOCUMENT_CREATE_MUTATION, {
|
|
2632
|
+
input: createInput,
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
if (!payload?.documentCreate?.success || !payload?.documentCreate?.document?.id) {
|
|
2636
|
+
throw new Error('Failed to create document');
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
return fetchDocumentDetails(client, payload.documentCreate.document.id);
|
|
2640
|
+
}, 'createDocument');
|
|
868
2641
|
}
|
|
869
2642
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
export async function resolveProjectRef(client, projectRef) {
|
|
877
|
-
const ref = String(projectRef || '').trim();
|
|
878
|
-
if (!ref) {
|
|
879
|
-
throw new Error('Missing project reference');
|
|
880
|
-
}
|
|
2643
|
+
export async function updateDocument(client, documentRef, patch = {}) {
|
|
2644
|
+
return withLinearErrorHandling(async () => {
|
|
2645
|
+
const id = String(documentRef || '').trim();
|
|
2646
|
+
if (!id) {
|
|
2647
|
+
throw new Error('Missing required field: document');
|
|
2648
|
+
}
|
|
881
2649
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (direct) {
|
|
887
|
-
return { id: direct.id, name: direct.name };
|
|
2650
|
+
const updateInput = {};
|
|
2651
|
+
for (const field of ['title', 'content', 'icon', 'color', 'projectId', 'issueId']) {
|
|
2652
|
+
if (patch[field] !== undefined) {
|
|
2653
|
+
updateInput[field] = patch[field];
|
|
888
2654
|
}
|
|
889
|
-
} catch {
|
|
890
|
-
// fall back to cached/full-project lookup below
|
|
891
2655
|
}
|
|
892
2656
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (byId) {
|
|
896
|
-
return byId;
|
|
2657
|
+
if (Object.keys(updateInput).length === 0) {
|
|
2658
|
+
throw new Error('No update fields provided');
|
|
897
2659
|
}
|
|
898
|
-
throw new Error(`Project not found with ID: ${ref}`);
|
|
899
|
-
}
|
|
900
2660
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (exactName) {
|
|
906
|
-
return exactName;
|
|
907
|
-
}
|
|
2661
|
+
const payload = await executeGraphQL(client, DOCUMENT_UPDATE_MUTATION, {
|
|
2662
|
+
id,
|
|
2663
|
+
input: updateInput,
|
|
2664
|
+
});
|
|
908
2665
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
if (insensitiveName) {
|
|
913
|
-
return insensitiveName;
|
|
914
|
-
}
|
|
2666
|
+
if (!payload?.documentUpdate?.success || !payload?.documentUpdate?.document?.id) {
|
|
2667
|
+
throw new Error('Failed to update document');
|
|
2668
|
+
}
|
|
915
2669
|
|
|
916
|
-
|
|
2670
|
+
const document = await fetchDocumentDetails(client, payload.documentUpdate.document.id);
|
|
2671
|
+
return {
|
|
2672
|
+
document,
|
|
2673
|
+
changed: Object.keys(updateInput),
|
|
2674
|
+
};
|
|
2675
|
+
}, 'updateDocument');
|
|
917
2676
|
}
|
|
918
2677
|
|
|
919
2678
|
/**
|
|
@@ -924,110 +2683,201 @@ export async function resolveProjectRef(client, projectRef) {
|
|
|
924
2683
|
* @param {boolean} [options.includeComments=true] - Include comments in response
|
|
925
2684
|
* @returns {Promise<Object>} Issue details
|
|
926
2685
|
*/
|
|
2686
|
+
async function fetchIssueDetailsViaSdk(client, issueRef, options = {}) {
|
|
2687
|
+
const { includeComments = true } = options;
|
|
2688
|
+
|
|
2689
|
+
const lookup = normalizeIssueLookupInput(issueRef);
|
|
2690
|
+
const sdkIssue = await client.issue(lookup);
|
|
2691
|
+
|
|
2692
|
+
if (!sdkIssue) {
|
|
2693
|
+
throw new Error(`Issue not found: ${lookup}`);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
const [
|
|
2697
|
+
state,
|
|
2698
|
+
team,
|
|
2699
|
+
project,
|
|
2700
|
+
projectMilestone,
|
|
2701
|
+
assignee,
|
|
2702
|
+
creator,
|
|
2703
|
+
labelsResult,
|
|
2704
|
+
parent,
|
|
2705
|
+
childrenResult,
|
|
2706
|
+
commentsResult,
|
|
2707
|
+
attachmentsResult,
|
|
2708
|
+
] = await Promise.all([
|
|
2709
|
+
sdkIssue.state?.catch?.(() => null) ?? sdkIssue.state,
|
|
2710
|
+
sdkIssue.team?.catch?.(() => null) ?? sdkIssue.team,
|
|
2711
|
+
sdkIssue.project?.catch?.(() => null) ?? sdkIssue.project,
|
|
2712
|
+
sdkIssue.projectMilestone?.catch?.(() => null) ?? sdkIssue.projectMilestone,
|
|
2713
|
+
sdkIssue.assignee?.catch?.(() => null) ?? sdkIssue.assignee,
|
|
2714
|
+
sdkIssue.creator?.catch?.(() => null) ?? sdkIssue.creator,
|
|
2715
|
+
sdkIssue.labels?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.labels?.() ?? { nodes: [] },
|
|
2716
|
+
sdkIssue.parent?.catch?.(() => null) ?? sdkIssue.parent,
|
|
2717
|
+
sdkIssue.children?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.children?.() ?? { nodes: [] },
|
|
2718
|
+
includeComments ? (sdkIssue.comments?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.comments?.() ?? { nodes: [] }) : Promise.resolve({ nodes: [] }),
|
|
2719
|
+
sdkIssue.attachments?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.attachments?.() ?? { nodes: [] },
|
|
2720
|
+
]);
|
|
2721
|
+
|
|
2722
|
+
let transformedParent = null;
|
|
2723
|
+
if (parent) {
|
|
2724
|
+
const parentState = await parent.state?.catch?.(() => null) ?? parent.state;
|
|
2725
|
+
transformedParent = {
|
|
2726
|
+
identifier: parent.identifier,
|
|
2727
|
+
title: parent.title,
|
|
2728
|
+
state: parentState ? { name: parentState.name, color: parentState.color } : null,
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const children = (childrenResult.nodes || []).map(c => ({
|
|
2733
|
+
identifier: c.identifier,
|
|
2734
|
+
title: c.title,
|
|
2735
|
+
state: c.state ? { name: c.state.name, color: c.state.color } : null,
|
|
2736
|
+
}));
|
|
2737
|
+
|
|
2738
|
+
const comments = (commentsResult.nodes || []).map(c => ({
|
|
2739
|
+
id: c.id,
|
|
2740
|
+
body: c.body,
|
|
2741
|
+
createdAt: c.createdAt,
|
|
2742
|
+
updatedAt: c.updatedAt,
|
|
2743
|
+
user: c.user ? { name: c.user.name, displayName: c.user.displayName } : null,
|
|
2744
|
+
externalUser: c.externalUser ? { name: c.externalUser.name, displayName: c.externalUser.displayName } : null,
|
|
2745
|
+
parent: c.parent ? { id: c.parent.id } : null,
|
|
2746
|
+
}));
|
|
2747
|
+
|
|
2748
|
+
const attachments = (attachmentsResult.nodes || []).map(a => ({
|
|
2749
|
+
id: a.id,
|
|
2750
|
+
title: a.title,
|
|
2751
|
+
url: a.url,
|
|
2752
|
+
subtitle: a.subtitle,
|
|
2753
|
+
sourceType: a.sourceType,
|
|
2754
|
+
createdAt: a.createdAt,
|
|
2755
|
+
}));
|
|
2756
|
+
|
|
2757
|
+
const labels = (labelsResult.nodes || []).map(l => ({
|
|
2758
|
+
id: l.id,
|
|
2759
|
+
name: l.name,
|
|
2760
|
+
color: l.color,
|
|
2761
|
+
}));
|
|
2762
|
+
|
|
2763
|
+
return {
|
|
2764
|
+
identifier: sdkIssue.identifier,
|
|
2765
|
+
title: sdkIssue.title,
|
|
2766
|
+
description: sdkIssue.description,
|
|
2767
|
+
url: sdkIssue.url,
|
|
2768
|
+
branchName: sdkIssue.branchName,
|
|
2769
|
+
priority: sdkIssue.priority,
|
|
2770
|
+
estimate: sdkIssue.estimate,
|
|
2771
|
+
createdAt: sdkIssue.createdAt,
|
|
2772
|
+
updatedAt: sdkIssue.updatedAt,
|
|
2773
|
+
state: state ? { name: state.name, color: state.color, type: state.type } : null,
|
|
2774
|
+
team: team ? { id: team.id, key: team.key, name: team.name } : null,
|
|
2775
|
+
project: project ? { id: project.id, name: project.name } : null,
|
|
2776
|
+
projectMilestone: projectMilestone ? { id: projectMilestone.id, name: projectMilestone.name } : null,
|
|
2777
|
+
assignee: assignee ? { id: assignee.id, name: assignee.name, displayName: assignee.displayName } : null,
|
|
2778
|
+
creator: creator ? { id: creator.id, name: creator.name, displayName: creator.displayName } : null,
|
|
2779
|
+
labels,
|
|
2780
|
+
parent: transformedParent,
|
|
2781
|
+
children,
|
|
2782
|
+
comments,
|
|
2783
|
+
attachments,
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
|
|
927
2787
|
export async function fetchIssueDetails(client, issueRef, options = {}) {
|
|
928
2788
|
return withLinearErrorHandling(async () => {
|
|
929
2789
|
const { includeComments = true } = options;
|
|
930
2790
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const sdkIssue = await client.issue(lookup);
|
|
934
|
-
|
|
935
|
-
if (!sdkIssue) {
|
|
936
|
-
throw new Error(`Issue not found: ${lookup}`);
|
|
2791
|
+
if (!getRawRequest(client)) {
|
|
2792
|
+
return fetchIssueDetailsViaSdk(client, issueRef, options);
|
|
937
2793
|
}
|
|
938
2794
|
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
assignee,
|
|
946
|
-
creator,
|
|
947
|
-
labelsResult,
|
|
948
|
-
parent,
|
|
949
|
-
childrenResult,
|
|
950
|
-
commentsResult,
|
|
951
|
-
attachmentsResult,
|
|
952
|
-
] = await Promise.all([
|
|
953
|
-
sdkIssue.state?.catch?.(() => null) ?? sdkIssue.state,
|
|
954
|
-
sdkIssue.team?.catch?.(() => null) ?? sdkIssue.team,
|
|
955
|
-
sdkIssue.project?.catch?.(() => null) ?? sdkIssue.project,
|
|
956
|
-
sdkIssue.projectMilestone?.catch?.(() => null) ?? sdkIssue.projectMilestone,
|
|
957
|
-
sdkIssue.assignee?.catch?.(() => null) ?? sdkIssue.assignee,
|
|
958
|
-
sdkIssue.creator?.catch?.(() => null) ?? sdkIssue.creator,
|
|
959
|
-
sdkIssue.labels?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.labels?.() ?? { nodes: [] },
|
|
960
|
-
sdkIssue.parent?.catch?.(() => null) ?? sdkIssue.parent,
|
|
961
|
-
sdkIssue.children?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.children?.() ?? { nodes: [] },
|
|
962
|
-
includeComments ? (sdkIssue.comments?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.comments?.() ?? { nodes: [] }) : Promise.resolve({ nodes: [] }),
|
|
963
|
-
sdkIssue.attachments?.()?.catch?.(() => ({ nodes: [] })) ?? sdkIssue.attachments?.() ?? { nodes: [] },
|
|
964
|
-
]);
|
|
2795
|
+
const lookup = normalizeIssueLookupInput(issueRef);
|
|
2796
|
+
const issueId = isLinearId(lookup)
|
|
2797
|
+
? lookup
|
|
2798
|
+
: (await resolveIssue(client, lookup)).id;
|
|
2799
|
+
const query = includeComments ? ISSUE_DETAILS_WITH_COMMENTS_QUERY : ISSUE_DETAILS_QUERY;
|
|
2800
|
+
const data = await executeGraphQL(client, query, { id: issueId });
|
|
965
2801
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (parent) {
|
|
969
|
-
const parentState = await parent.state?.catch?.(() => null) ?? parent.state;
|
|
970
|
-
transformedParent = {
|
|
971
|
-
identifier: parent.identifier,
|
|
972
|
-
title: parent.title,
|
|
973
|
-
state: parentState ? { name: parentState.name, color: parentState.color } : null,
|
|
974
|
-
};
|
|
2802
|
+
if (!data?.issue) {
|
|
2803
|
+
throw new Error(`Issue not found: ${lookup}`);
|
|
975
2804
|
}
|
|
976
2805
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
state: c.state ? { name: c.state.name, color: c.state.color } : null,
|
|
981
|
-
}));
|
|
982
|
-
|
|
983
|
-
const comments = (commentsResult.nodes || []).map(c => ({
|
|
984
|
-
id: c.id,
|
|
985
|
-
body: c.body,
|
|
986
|
-
createdAt: c.createdAt,
|
|
987
|
-
updatedAt: c.updatedAt,
|
|
988
|
-
user: c.user ? { name: c.user.name, displayName: c.user.displayName } : null,
|
|
989
|
-
externalUser: c.externalUser ? { name: c.externalUser.name, displayName: c.externalUser.displayName } : null,
|
|
990
|
-
parent: c.parent ? { id: c.parent.id } : null,
|
|
991
|
-
}));
|
|
2806
|
+
return transformRawIssueDetails(data.issue, { includeComments });
|
|
2807
|
+
}, 'fetchIssueDetails');
|
|
2808
|
+
}
|
|
992
2809
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
2810
|
+
export async function fetchIssueActivity(client, issueRef, options = {}) {
|
|
2811
|
+
return withLinearErrorHandling(async () => {
|
|
2812
|
+
const limit = normalizePositiveInteger(options.limit, 'limit', 20);
|
|
2813
|
+
const includeArchived = options.includeArchived === true;
|
|
2814
|
+
const resolved = await resolveIssue(client, issueRef);
|
|
2815
|
+
const data = await executeGraphQL(client, ISSUE_ACTIVITY_QUERY, {
|
|
2816
|
+
id: resolved.id,
|
|
2817
|
+
first: limit,
|
|
2818
|
+
includeArchived,
|
|
2819
|
+
});
|
|
1001
2820
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
color: l.color,
|
|
1006
|
-
}));
|
|
2821
|
+
if (!data?.issue) {
|
|
2822
|
+
throw new Error(`Issue not found: ${issueRef}`);
|
|
2823
|
+
}
|
|
1007
2824
|
|
|
1008
2825
|
return {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
2826
|
+
issue: {
|
|
2827
|
+
id: data.issue.id,
|
|
2828
|
+
identifier: data.issue.identifier,
|
|
2829
|
+
title: data.issue.title,
|
|
2830
|
+
url: data.issue.url ?? null,
|
|
2831
|
+
},
|
|
2832
|
+
activity: (data.issue.history?.nodes || []).map((entry) => ({
|
|
2833
|
+
id: entry.id,
|
|
2834
|
+
createdAt: entry.createdAt ?? null,
|
|
2835
|
+
updatedAt: entry.updatedAt ?? null,
|
|
2836
|
+
actor: entry.actor ? {
|
|
2837
|
+
id: entry.actor.id,
|
|
2838
|
+
name: entry.actor.name,
|
|
2839
|
+
displayName: entry.actor.displayName,
|
|
2840
|
+
} : null,
|
|
2841
|
+
fromState: entry.fromState ? { id: entry.fromState.id, name: entry.fromState.name } : null,
|
|
2842
|
+
toState: entry.toState ? { id: entry.toState.id, name: entry.toState.name } : null,
|
|
2843
|
+
fromAssignee: entry.fromAssignee ? {
|
|
2844
|
+
id: entry.fromAssignee.id,
|
|
2845
|
+
name: entry.fromAssignee.name,
|
|
2846
|
+
displayName: entry.fromAssignee.displayName,
|
|
2847
|
+
} : null,
|
|
2848
|
+
toAssignee: entry.toAssignee ? {
|
|
2849
|
+
id: entry.toAssignee.id,
|
|
2850
|
+
name: entry.toAssignee.name,
|
|
2851
|
+
displayName: entry.toAssignee.displayName,
|
|
2852
|
+
} : null,
|
|
2853
|
+
fromTitle: entry.fromTitle ?? null,
|
|
2854
|
+
toTitle: entry.toTitle ?? null,
|
|
2855
|
+
fromPriority: entry.fromPriority ?? null,
|
|
2856
|
+
toPriority: entry.toPriority ?? null,
|
|
2857
|
+
fromProject: entry.fromProject ? { id: entry.fromProject.id, name: entry.fromProject.name } : null,
|
|
2858
|
+
toProject: entry.toProject ? { id: entry.toProject.id, name: entry.toProject.name } : null,
|
|
2859
|
+
fromProjectMilestone: entry.fromProjectMilestone ? { id: entry.fromProjectMilestone.id, name: entry.fromProjectMilestone.name } : null,
|
|
2860
|
+
toProjectMilestone: entry.toProjectMilestone ? { id: entry.toProjectMilestone.id, name: entry.toProjectMilestone.name } : null,
|
|
2861
|
+
addedLabels: (entry.addedLabels || []).map((label) => ({ id: label.id, name: label.name })),
|
|
2862
|
+
removedLabels: (entry.removedLabels || []).map((label) => ({ id: label.id, name: label.name })),
|
|
2863
|
+
relationChanges: (entry.relationChanges || []).map((relation) => ({
|
|
2864
|
+
identifier: relation.identifier,
|
|
2865
|
+
type: relation.type,
|
|
2866
|
+
})),
|
|
2867
|
+
attachment: entry.attachment ? {
|
|
2868
|
+
id: entry.attachment.id,
|
|
2869
|
+
title: entry.attachment.title,
|
|
2870
|
+
url: entry.attachment.url,
|
|
2871
|
+
} : null,
|
|
2872
|
+
archived: entry.archived ?? null,
|
|
2873
|
+
archivedAt: entry.archivedAt ?? null,
|
|
2874
|
+
autoArchived: entry.autoArchived ?? false,
|
|
2875
|
+
autoClosed: entry.autoClosed ?? false,
|
|
2876
|
+
trashed: entry.trashed ?? null,
|
|
2877
|
+
updatedDescription: entry.updatedDescription ?? false,
|
|
2878
|
+
})),
|
|
1029
2879
|
};
|
|
1030
|
-
}, '
|
|
2880
|
+
}, 'fetchIssueActivity');
|
|
1031
2881
|
}
|
|
1032
2882
|
|
|
1033
2883
|
// ===== MUTATION FUNCTIONS =====
|
|
@@ -1041,17 +2891,17 @@ export async function fetchIssueDetails(client, issueRef, options = {}) {
|
|
|
1041
2891
|
*/
|
|
1042
2892
|
export async function setIssueState(client, issueId, stateId) {
|
|
1043
2893
|
return withLinearErrorHandling(async () => {
|
|
1044
|
-
const
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
2894
|
+
const updated = await performIssueUpdate(client, issueId, { stateId });
|
|
2895
|
+
if (updated) {
|
|
2896
|
+
return updated;
|
|
1047
2897
|
}
|
|
1048
2898
|
|
|
1049
|
-
const
|
|
1050
|
-
if (!
|
|
1051
|
-
throw new Error(
|
|
2899
|
+
const refreshed = await fetchIssueMinimalById(client, issueId);
|
|
2900
|
+
if (!refreshed) {
|
|
2901
|
+
throw new Error(`Issue not found: ${issueId}`);
|
|
1052
2902
|
}
|
|
1053
2903
|
|
|
1054
|
-
return
|
|
2904
|
+
return refreshed;
|
|
1055
2905
|
}, 'setIssueState');
|
|
1056
2906
|
}
|
|
1057
2907
|
|
|
@@ -1101,6 +2951,14 @@ export async function createIssue(client, input) {
|
|
|
1101
2951
|
createInput.priority = parsed;
|
|
1102
2952
|
}
|
|
1103
2953
|
|
|
2954
|
+
if (input.estimate !== undefined) {
|
|
2955
|
+
const parsed = Number.parseInt(String(input.estimate), 10);
|
|
2956
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
2957
|
+
throw new Error(`Invalid estimate: ${input.estimate}. Must be a non-negative integer.`);
|
|
2958
|
+
}
|
|
2959
|
+
createInput.estimate = parsed;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
1104
2962
|
if (input.assigneeId !== undefined) {
|
|
1105
2963
|
createInput.assigneeId = input.assigneeId;
|
|
1106
2964
|
}
|
|
@@ -1113,33 +2971,55 @@ export async function createIssue(client, input) {
|
|
|
1113
2971
|
createInput.stateId = input.stateId;
|
|
1114
2972
|
}
|
|
1115
2973
|
|
|
1116
|
-
|
|
2974
|
+
if (getRawRequest(client)) {
|
|
2975
|
+
const payload = await executeGraphQL(client, ISSUE_CREATE_MUTATION, { input: createInput });
|
|
2976
|
+
if (!payload?.issueCreate?.success) {
|
|
2977
|
+
throw new Error('Failed to create issue');
|
|
2978
|
+
}
|
|
1117
2979
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2980
|
+
const createdIssue = transformRawIssue(payload.issueCreate.issue ?? null);
|
|
2981
|
+
if (createdIssue) {
|
|
2982
|
+
return createdIssue;
|
|
2983
|
+
}
|
|
2984
|
+
} else {
|
|
2985
|
+
const result = await client.createIssue(createInput);
|
|
1121
2986
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|| result._issue?.id
|
|
1126
|
-
|| null;
|
|
2987
|
+
if (!result.success) {
|
|
2988
|
+
throw new Error('Failed to create issue');
|
|
2989
|
+
}
|
|
1127
2990
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
2991
|
+
const createdIssueId =
|
|
2992
|
+
result.issue?.id
|
|
2993
|
+
|| result._issue?.id
|
|
2994
|
+
|| null;
|
|
2995
|
+
|
|
2996
|
+
if (createdIssueId) {
|
|
2997
|
+
try {
|
|
2998
|
+
const fullIssue = await fetchIssueMinimalById(client, createdIssueId);
|
|
2999
|
+
if (fullIssue) {
|
|
3000
|
+
return fullIssue;
|
|
3001
|
+
}
|
|
3002
|
+
} catch {
|
|
3003
|
+
// continue to fallback
|
|
1134
3004
|
}
|
|
1135
|
-
} catch {
|
|
1136
|
-
// continue to fallback
|
|
1137
3005
|
}
|
|
3006
|
+
|
|
3007
|
+
return {
|
|
3008
|
+
id: createdIssueId,
|
|
3009
|
+
identifier: null,
|
|
3010
|
+
title,
|
|
3011
|
+
description: input.description ?? null,
|
|
3012
|
+
url: null,
|
|
3013
|
+
priority: input.priority ?? null,
|
|
3014
|
+
state: null,
|
|
3015
|
+
team: null,
|
|
3016
|
+
project: null,
|
|
3017
|
+
assignee: null,
|
|
3018
|
+
};
|
|
1138
3019
|
}
|
|
1139
3020
|
|
|
1140
|
-
// Minimal fallback when SDK payload does not expose a resolvable issue ID.
|
|
1141
3021
|
return {
|
|
1142
|
-
id:
|
|
3022
|
+
id: null,
|
|
1143
3023
|
identifier: null,
|
|
1144
3024
|
title,
|
|
1145
3025
|
description: input.description ?? null,
|
|
@@ -1211,6 +3091,9 @@ function buildFallbackUpdatedIssue(targetIssue, updateInput) {
|
|
|
1211
3091
|
if (Object.prototype.hasOwnProperty.call(updateInput, 'priority')) {
|
|
1212
3092
|
fallback.priority = updateInput.priority;
|
|
1213
3093
|
}
|
|
3094
|
+
if (Object.prototype.hasOwnProperty.call(updateInput, 'estimate')) {
|
|
3095
|
+
fallback.estimate = updateInput.estimate;
|
|
3096
|
+
}
|
|
1214
3097
|
|
|
1215
3098
|
if (Object.prototype.hasOwnProperty.call(updateInput, 'assigneeId')) {
|
|
1216
3099
|
const assigneeId = updateInput.assigneeId;
|
|
@@ -1281,6 +3164,15 @@ export async function updateIssue(client, issueRef, patch = {}) {
|
|
|
1281
3164
|
updateInput.priority = parsed;
|
|
1282
3165
|
}
|
|
1283
3166
|
|
|
3167
|
+
|
|
3168
|
+
if (patch.estimate !== undefined) {
|
|
3169
|
+
const parsed = Number.parseInt(String(patch.estimate), 10);
|
|
3170
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
3171
|
+
throw new Error(`Invalid estimate: ${patch.estimate}. Must be a non-negative integer.`);
|
|
3172
|
+
}
|
|
3173
|
+
updateInput.estimate = parsed;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
1284
3176
|
if (patch.state !== undefined) {
|
|
1285
3177
|
// Need to resolve state ID from team's workflow states
|
|
1286
3178
|
const team = targetIssue.team;
|
|
@@ -1335,14 +3227,7 @@ export async function updateIssue(client, issueRef, patch = {}) {
|
|
|
1335
3227
|
|
|
1336
3228
|
for (const childRef of parentOfRefs) {
|
|
1337
3229
|
const childIssue = await resolveIssue(client, childRef);
|
|
1338
|
-
|
|
1339
|
-
if (!childSdkIssue) {
|
|
1340
|
-
throw new Error(`Issue not found: ${childRef}`);
|
|
1341
|
-
}
|
|
1342
|
-
const rel = await childSdkIssue.update({ parentId: targetIssue.id });
|
|
1343
|
-
if (!rel.success) {
|
|
1344
|
-
throw new Error(`Failed to set parent for issue: ${childRef}`);
|
|
1345
|
-
}
|
|
3230
|
+
await performIssueUpdate(client, childIssue.id, { parentId: targetIssue.id });
|
|
1346
3231
|
}
|
|
1347
3232
|
|
|
1348
3233
|
for (const blockerRef of blockedByRefs) {
|
|
@@ -1381,16 +3266,7 @@ export async function updateIssue(client, issueRef, patch = {}) {
|
|
|
1381
3266
|
}
|
|
1382
3267
|
|
|
1383
3268
|
if (Object.keys(updateInput).length > 0) {
|
|
1384
|
-
|
|
1385
|
-
const sdkIssue = await client.issue(targetIssue.id);
|
|
1386
|
-
if (!sdkIssue) {
|
|
1387
|
-
throw new Error(`Issue not found: ${targetIssue.id}`);
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
const result = await sdkIssue.update(updateInput);
|
|
1391
|
-
if (!result.success) {
|
|
1392
|
-
throw new Error('Failed to update issue');
|
|
1393
|
-
}
|
|
3269
|
+
await performIssueUpdate(client, targetIssue.id, updateInput);
|
|
1394
3270
|
}
|
|
1395
3271
|
|
|
1396
3272
|
for (const relationInput of relationCreates) {
|
|
@@ -1405,8 +3281,7 @@ export async function updateIssue(client, issueRef, patch = {}) {
|
|
|
1405
3281
|
let updatedIssue = null;
|
|
1406
3282
|
let usedRateLimitFallback = false;
|
|
1407
3283
|
try {
|
|
1408
|
-
|
|
1409
|
-
updatedIssue = await transformIssue(updatedSdkIssue);
|
|
3284
|
+
updatedIssue = await fetchIssueMinimalById(client, targetIssue.id);
|
|
1410
3285
|
} catch (refreshError) {
|
|
1411
3286
|
const refreshMessage = String(refreshError?.message || refreshError || 'unknown');
|
|
1412
3287
|
const isRefreshRateLimited = refreshError?.type === 'Ratelimited' || refreshMessage.toLowerCase().includes('rate limit');
|
|
@@ -1507,13 +3382,120 @@ async function transformMilestone(sdkMilestone) {
|
|
|
1507
3382
|
name: sdkMilestone.name,
|
|
1508
3383
|
description: sdkMilestone.description,
|
|
1509
3384
|
progress: sdkMilestone.progress,
|
|
1510
|
-
order: sdkMilestone.
|
|
3385
|
+
order: sdkMilestone.sortOrder,
|
|
1511
3386
|
targetDate: sdkMilestone.targetDate,
|
|
1512
3387
|
status: sdkMilestone.status,
|
|
1513
3388
|
project: project ? { id: project.id, name: project.name } : null,
|
|
1514
3389
|
};
|
|
1515
3390
|
}
|
|
1516
3391
|
|
|
3392
|
+
function transformProject(rawProject) {
|
|
3393
|
+
if (!rawProject) return null;
|
|
3394
|
+
|
|
3395
|
+
const milestoneNodes = rawProject.projectMilestones?.nodes || [];
|
|
3396
|
+
const teamNodes = rawProject.teams?.nodes || [];
|
|
3397
|
+
|
|
3398
|
+
return {
|
|
3399
|
+
id: rawProject.id,
|
|
3400
|
+
name: rawProject.name,
|
|
3401
|
+
description: rawProject.description ?? '',
|
|
3402
|
+
content: rawProject.content ?? null,
|
|
3403
|
+
color: rawProject.color ?? null,
|
|
3404
|
+
icon: rawProject.icon ?? null,
|
|
3405
|
+
priority: rawProject.priority ?? null,
|
|
3406
|
+
progress: rawProject.progress ?? null,
|
|
3407
|
+
health: rawProject.health ?? null,
|
|
3408
|
+
startDate: rawProject.startDate ?? null,
|
|
3409
|
+
targetDate: rawProject.targetDate ?? null,
|
|
3410
|
+
slugId: rawProject.slugId ?? null,
|
|
3411
|
+
url: rawProject.url ?? null,
|
|
3412
|
+
archivedAt: rawProject.archivedAt ?? null,
|
|
3413
|
+
completedAt: rawProject.completedAt ?? null,
|
|
3414
|
+
canceledAt: rawProject.canceledAt ?? null,
|
|
3415
|
+
status: rawProject.status ? {
|
|
3416
|
+
id: rawProject.status.id,
|
|
3417
|
+
name: rawProject.status.name,
|
|
3418
|
+
type: rawProject.status.type,
|
|
3419
|
+
color: rawProject.status.color,
|
|
3420
|
+
} : null,
|
|
3421
|
+
lead: rawProject.lead ? {
|
|
3422
|
+
id: rawProject.lead.id,
|
|
3423
|
+
name: rawProject.lead.name,
|
|
3424
|
+
displayName: rawProject.lead.displayName,
|
|
3425
|
+
} : null,
|
|
3426
|
+
teams: teamNodes.map((team) => ({
|
|
3427
|
+
id: team.id,
|
|
3428
|
+
key: team.key,
|
|
3429
|
+
name: team.name,
|
|
3430
|
+
})),
|
|
3431
|
+
projectMilestones: milestoneNodes.map((milestone) => ({
|
|
3432
|
+
id: milestone.id,
|
|
3433
|
+
name: milestone.name,
|
|
3434
|
+
status: milestone.status,
|
|
3435
|
+
progress: milestone.progress ?? null,
|
|
3436
|
+
targetDate: milestone.targetDate ?? null,
|
|
3437
|
+
})),
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
function transformProjectUpdate(rawUpdate) {
|
|
3442
|
+
if (!rawUpdate) return null;
|
|
3443
|
+
|
|
3444
|
+
return {
|
|
3445
|
+
id: rawUpdate.id,
|
|
3446
|
+
body: rawUpdate.body ?? '',
|
|
3447
|
+
health: rawUpdate.health ?? null,
|
|
3448
|
+
createdAt: rawUpdate.createdAt ?? null,
|
|
3449
|
+
updatedAt: rawUpdate.updatedAt ?? null,
|
|
3450
|
+
archivedAt: rawUpdate.archivedAt ?? null,
|
|
3451
|
+
editedAt: rawUpdate.editedAt ?? null,
|
|
3452
|
+
url: rawUpdate.url ?? null,
|
|
3453
|
+
slugId: rawUpdate.slugId ?? null,
|
|
3454
|
+
isDiffHidden: rawUpdate.isDiffHidden ?? false,
|
|
3455
|
+
isStale: rawUpdate.isStale ?? false,
|
|
3456
|
+
project: rawUpdate.project ? {
|
|
3457
|
+
id: rawUpdate.project.id,
|
|
3458
|
+
name: rawUpdate.project.name,
|
|
3459
|
+
} : null,
|
|
3460
|
+
user: rawUpdate.user ? {
|
|
3461
|
+
id: rawUpdate.user.id,
|
|
3462
|
+
name: rawUpdate.user.name,
|
|
3463
|
+
displayName: rawUpdate.user.displayName,
|
|
3464
|
+
} : null,
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function transformDocument(rawDocument) {
|
|
3469
|
+
if (!rawDocument) return null;
|
|
3470
|
+
|
|
3471
|
+
return {
|
|
3472
|
+
id: rawDocument.id,
|
|
3473
|
+
title: rawDocument.title ?? '',
|
|
3474
|
+
content: rawDocument.content ?? '',
|
|
3475
|
+
icon: rawDocument.icon ?? null,
|
|
3476
|
+
color: rawDocument.color ?? null,
|
|
3477
|
+
slugId: rawDocument.slugId ?? null,
|
|
3478
|
+
url: rawDocument.url ?? null,
|
|
3479
|
+
archivedAt: rawDocument.archivedAt ?? null,
|
|
3480
|
+
createdAt: rawDocument.createdAt ?? null,
|
|
3481
|
+
updatedAt: rawDocument.updatedAt ?? null,
|
|
3482
|
+
project: rawDocument.project ? {
|
|
3483
|
+
id: rawDocument.project.id,
|
|
3484
|
+
name: rawDocument.project.name,
|
|
3485
|
+
} : null,
|
|
3486
|
+
issue: rawDocument.issue ? {
|
|
3487
|
+
id: rawDocument.issue.id,
|
|
3488
|
+
identifier: rawDocument.issue.identifier,
|
|
3489
|
+
title: rawDocument.issue.title,
|
|
3490
|
+
} : null,
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
function invalidateProjectsCache(client) {
|
|
3495
|
+
const cacheKey = getClientCacheKey(client);
|
|
3496
|
+
projectsCache.delete(cacheKey);
|
|
3497
|
+
}
|
|
3498
|
+
|
|
1517
3499
|
/**
|
|
1518
3500
|
* Fetch milestones for a project
|
|
1519
3501
|
* @param {LinearClient} client - Linear SDK client
|
|
@@ -1522,16 +3504,11 @@ async function transformMilestone(sdkMilestone) {
|
|
|
1522
3504
|
*/
|
|
1523
3505
|
export async function fetchProjectMilestones(client, projectId) {
|
|
1524
3506
|
return withLinearErrorHandling(async () => {
|
|
1525
|
-
const
|
|
1526
|
-
if (!
|
|
3507
|
+
const milestones = await fetchProjectMilestonesByQuery(client, projectId);
|
|
3508
|
+
if (!milestones) {
|
|
1527
3509
|
throw new Error(`Project not found: ${projectId}`);
|
|
1528
3510
|
}
|
|
1529
3511
|
|
|
1530
|
-
const result = await project.projectMilestones();
|
|
1531
|
-
const nodes = result.nodes || [];
|
|
1532
|
-
|
|
1533
|
-
const milestones = await Promise.all(nodes.map(transformMilestone));
|
|
1534
|
-
|
|
1535
3512
|
debug('Fetched project milestones', {
|
|
1536
3513
|
projectId,
|
|
1537
3514
|
milestoneCount: milestones.length,
|
|
@@ -1585,7 +3562,7 @@ export async function fetchMilestoneDetails(client, milestoneId) {
|
|
|
1585
3562
|
name: milestone.name,
|
|
1586
3563
|
description: milestone.description,
|
|
1587
3564
|
progress: milestone.progress,
|
|
1588
|
-
order: milestone.
|
|
3565
|
+
order: milestone.sortOrder,
|
|
1589
3566
|
targetDate: milestone.targetDate,
|
|
1590
3567
|
status: milestone.status,
|
|
1591
3568
|
project: project ? { id: project.id, name: project.name } : null,
|
|
@@ -1666,7 +3643,7 @@ export async function createProjectMilestone(client, input) {
|
|
|
1666
3643
|
name: created?.name || name,
|
|
1667
3644
|
description: created?.description ?? input.description ?? null,
|
|
1668
3645
|
progress: created?.progress ?? 0,
|
|
1669
|
-
order: created?.
|
|
3646
|
+
order: created?.sortOrder ?? null,
|
|
1670
3647
|
targetDate: created?.targetDate ?? input.targetDate ?? null,
|
|
1671
3648
|
status: created?.status ?? input.status ?? 'backlogged',
|
|
1672
3649
|
project: null,
|
|
@@ -1763,16 +3740,21 @@ export async function deleteIssue(client, issueRef) {
|
|
|
1763
3740
|
return withLinearErrorHandling(async () => {
|
|
1764
3741
|
const targetIssue = await resolveIssue(client, issueRef);
|
|
1765
3742
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
3743
|
+
let success = false;
|
|
3744
|
+
if (getRawRequest(client)) {
|
|
3745
|
+
const payload = await executeGraphQL(client, ISSUE_DELETE_MUTATION, { id: targetIssue.id });
|
|
3746
|
+
success = payload?.issueDelete?.success === true;
|
|
3747
|
+
} else {
|
|
3748
|
+
const sdkIssue = await client.issue(targetIssue.id);
|
|
3749
|
+
if (!sdkIssue) {
|
|
3750
|
+
throw new Error(`Issue not found: ${targetIssue.id}`);
|
|
3751
|
+
}
|
|
3752
|
+
const result = await sdkIssue.delete();
|
|
3753
|
+
success = result.success;
|
|
1770
3754
|
}
|
|
1771
3755
|
|
|
1772
|
-
const result = await sdkIssue.delete();
|
|
1773
|
-
|
|
1774
3756
|
return {
|
|
1775
|
-
success
|
|
3757
|
+
success,
|
|
1776
3758
|
issueId: targetIssue.id,
|
|
1777
3759
|
identifier: targetIssue.identifier,
|
|
1778
3760
|
};
|
|
@@ -2009,3 +3991,31 @@ export function formatIssueAsMarkdown(issueData, options = {}) {
|
|
|
2009
3991
|
|
|
2010
3992
|
return lines.join('\n');
|
|
2011
3993
|
}
|
|
3994
|
+
|
|
3995
|
+
export function formatIssueActivityAsMarkdown(issueData, options = {}) {
|
|
3996
|
+
const { limit } = options;
|
|
3997
|
+
const lines = [`# Activity for ${issueData.issue.identifier}: ${issueData.issue.title}`];
|
|
3998
|
+
|
|
3999
|
+
if (issueData.issue.url) {
|
|
4000
|
+
lines.push('');
|
|
4001
|
+
lines.push(`**URL:** ${issueData.issue.url}`);
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
if (!issueData.activity || issueData.activity.length === 0) {
|
|
4005
|
+
lines.push('');
|
|
4006
|
+
lines.push('No activity entries found.');
|
|
4007
|
+
return lines.join('\n');
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
lines.push('');
|
|
4011
|
+
lines.push(`Showing ${issueData.activity.length}${limit ? ` of up to ${limit}` : ''} activity entries.`);
|
|
4012
|
+
lines.push('');
|
|
4013
|
+
|
|
4014
|
+
for (const entry of issueData.activity) {
|
|
4015
|
+
const actor = entry.actor ? getUserDisplayName(entry.actor) : 'System';
|
|
4016
|
+
const timestamp = entry.createdAt ? formatRelativeTime(entry.createdAt) : 'unknown time';
|
|
4017
|
+
lines.push(`- **${actor}** ${summarizeIssueHistoryEntry(entry)} _(${timestamp})_`);
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
return lines.join('\n');
|
|
4021
|
+
}
|