@enrichlayer/el-linear 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +13 -0
  2. package/dist/commands/attachments.js +2 -1
  3. package/dist/commands/comments.js +17 -28
  4. package/dist/commands/cycles.js +2 -1
  5. package/dist/commands/documents.js +2 -1
  6. package/dist/commands/graphql.js +4 -6
  7. package/dist/commands/issue-id.js +1 -3
  8. package/dist/commands/issues/description.js +2 -6
  9. package/dist/commands/issues/link-references.d.ts +21 -0
  10. package/dist/commands/issues/link-references.js +171 -0
  11. package/dist/commands/issues/relations.d.ts +55 -0
  12. package/dist/commands/issues/relations.js +132 -0
  13. package/dist/commands/issues.js +65 -284
  14. package/dist/commands/labels.js +13 -23
  15. package/dist/commands/project-milestones.js +13 -20
  16. package/dist/commands/projects.js +67 -38
  17. package/dist/commands/refs.js +8 -0
  18. package/dist/commands/releases.js +26 -30
  19. package/dist/commands/search.js +21 -30
  20. package/dist/commands/teams.js +2 -1
  21. package/dist/commands/templates.js +9 -14
  22. package/dist/commands/users.js +2 -1
  23. package/dist/config/resolver.js +2 -3
  24. package/dist/queries/attachments-types.d.ts +30 -0
  25. package/dist/queries/attachments-types.js +5 -0
  26. package/dist/queries/comments-types.d.ts +49 -0
  27. package/dist/queries/comments-types.js +5 -0
  28. package/dist/queries/documents-types.d.ts +61 -0
  29. package/dist/queries/documents-types.js +9 -0
  30. package/dist/queries/introspect-types.d.ts +57 -0
  31. package/dist/queries/introspect-types.js +10 -0
  32. package/dist/queries/issues-types.d.ts +416 -0
  33. package/dist/queries/issues-types.js +23 -0
  34. package/dist/queries/issues.d.ts +2 -0
  35. package/dist/queries/issues.js +22 -0
  36. package/dist/queries/labels-types.d.ts +64 -0
  37. package/dist/queries/labels-types.js +5 -0
  38. package/dist/queries/project-milestones-types.d.ts +91 -0
  39. package/dist/queries/project-milestones-types.js +10 -0
  40. package/dist/queries/projects-types.d.ts +75 -0
  41. package/dist/queries/projects-types.js +5 -0
  42. package/dist/queries/projects.d.ts +2 -0
  43. package/dist/queries/projects.js +22 -0
  44. package/dist/queries/releases-types.d.ts +84 -0
  45. package/dist/queries/releases-types.js +5 -0
  46. package/dist/queries/search-types.d.ts +86 -0
  47. package/dist/queries/search-types.js +6 -0
  48. package/dist/queries/templates-types.d.ts +61 -0
  49. package/dist/queries/templates-types.js +9 -0
  50. package/dist/types/linear.d.ts +8 -2
  51. package/dist/utils/auto-link-references.js +29 -24
  52. package/dist/utils/graphql-attachments-service.js +6 -9
  53. package/dist/utils/graphql-documents-service.js +19 -25
  54. package/dist/utils/graphql-issues-service.d.ts +12 -2
  55. package/dist/utils/graphql-issues-service.js +151 -172
  56. package/dist/utils/output.d.ts +2 -0
  57. package/dist/utils/output.js +16 -6
  58. package/dist/utils/workspace-url.d.ts +5 -1
  59. package/dist/utils/workspace-url.js +35 -5
  60. package/package.json +1 -1
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./project-milestones.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * The selection sets vary across queries — some include `description`
6
+ * + `createdAt`, others only the lookup-relevant fields. Modelled as
7
+ * a base `MilestoneBaseRef` with extension shapes per query so each
8
+ * consumer reads only what was actually selected.
9
+ */
10
+ import type { IssueNode } from "./issues-types.js";
11
+ export interface MilestoneProjectRef {
12
+ id: string;
13
+ name: string;
14
+ }
15
+ /**
16
+ * Mirrors the milestone selection set in `LIST_PROJECT_MILESTONES_QUERY`
17
+ * — full milestone fields without the project-back-reference.
18
+ */
19
+ export interface MilestoneListNode {
20
+ id: string;
21
+ name: string;
22
+ description: string | null;
23
+ targetDate: string | null;
24
+ sortOrder: number;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }
28
+ /**
29
+ * Mirrors the milestone selection set in `GET_PROJECT_MILESTONE_BY_ID_QUERY`
30
+ * — list shape + project + issues connection.
31
+ */
32
+ export interface MilestoneDetailNode extends MilestoneListNode {
33
+ project: MilestoneProjectRef;
34
+ issues: {
35
+ nodes: IssueNode[];
36
+ };
37
+ }
38
+ /**
39
+ * Mirrors the milestone selection set in `FIND_PROJECT_MILESTONE_BY_NAME_*`
40
+ * — minimal lookup shape with project back-reference.
41
+ */
42
+ export interface MilestoneLookupNode {
43
+ id: string;
44
+ name: string;
45
+ targetDate: string | null;
46
+ sortOrder: number;
47
+ project: MilestoneProjectRef;
48
+ }
49
+ /**
50
+ * Mirrors the milestone selection set on the create / update mutations —
51
+ * list shape + project (no issues connection).
52
+ */
53
+ export interface MutatedMilestoneNode extends MilestoneListNode {
54
+ project: MilestoneProjectRef;
55
+ }
56
+ export interface ListProjectMilestonesResponse {
57
+ project: {
58
+ id: string;
59
+ name: string;
60
+ projectMilestones: {
61
+ nodes: MilestoneListNode[];
62
+ };
63
+ } | null;
64
+ }
65
+ export interface GetProjectMilestoneByIdResponse {
66
+ projectMilestone: MilestoneDetailNode | null;
67
+ }
68
+ export interface FindProjectMilestoneScopedResponse {
69
+ project: {
70
+ projectMilestones: {
71
+ nodes: MilestoneLookupNode[];
72
+ };
73
+ } | null;
74
+ }
75
+ export interface FindProjectMilestoneGlobalResponse {
76
+ projectMilestones: {
77
+ nodes: MilestoneLookupNode[];
78
+ };
79
+ }
80
+ export interface CreateProjectMilestoneResponse {
81
+ projectMilestoneCreate: {
82
+ success: boolean;
83
+ projectMilestone: MutatedMilestoneNode | null;
84
+ };
85
+ }
86
+ export interface UpdateProjectMilestoneResponse {
87
+ projectMilestoneUpdate: {
88
+ success: boolean;
89
+ projectMilestone: MutatedMilestoneNode | null;
90
+ };
91
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./project-milestones.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * The selection sets vary across queries — some include `description`
6
+ * + `createdAt`, others only the lookup-relevant fields. Modelled as
7
+ * a base `MilestoneBaseRef` with extension shapes per query so each
8
+ * consumer reads only what was actually selected.
9
+ */
10
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./projects.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export interface ProjectTeamRef {
6
+ id: string;
7
+ key: string;
8
+ name: string;
9
+ }
10
+ export interface ProjectBaseNode {
11
+ id: string;
12
+ name: string;
13
+ teams: {
14
+ nodes: ProjectTeamRef[];
15
+ };
16
+ }
17
+ /**
18
+ * Mirrors the project selection set used by `SEARCH_PROJECTS_BY_NAME_QUERY`
19
+ * and `CREATE_PROJECT_MUTATION` — base fields + state.
20
+ */
21
+ export interface ProjectWithStateNode extends ProjectBaseNode {
22
+ state: string;
23
+ }
24
+ export interface ProjectIssueRef {
25
+ id: string;
26
+ identifier: string;
27
+ title: string;
28
+ }
29
+ export interface ProjectWithIssuesNode extends ProjectBaseNode {
30
+ issues: {
31
+ nodes: ProjectIssueRef[];
32
+ };
33
+ }
34
+ export interface ProjectByIdResponse {
35
+ project: ProjectBaseNode | null;
36
+ }
37
+ export interface GetProjectResponse {
38
+ projects: {
39
+ nodes: ProjectBaseNode[];
40
+ };
41
+ }
42
+ export interface GetProjectTeamIssuesResponse {
43
+ project: ProjectWithIssuesNode | null;
44
+ }
45
+ export interface SearchProjectsByNameResponse {
46
+ projects: {
47
+ nodes: ProjectWithStateNode[];
48
+ };
49
+ }
50
+ export interface CreateProjectResponse {
51
+ projectCreate: {
52
+ success: boolean;
53
+ project: ProjectWithStateNode | null;
54
+ };
55
+ }
56
+ export interface UpdateProjectResponse {
57
+ projectUpdate: {
58
+ success: boolean;
59
+ project: ProjectBaseNode | null;
60
+ };
61
+ }
62
+ export interface ProjectArchiveEntity {
63
+ id: string;
64
+ }
65
+ export interface ProjectArchivePayload {
66
+ success: boolean;
67
+ lastSyncId: number;
68
+ entity: ProjectArchiveEntity | null;
69
+ }
70
+ export interface ArchiveProjectResponse {
71
+ projectArchive: ProjectArchivePayload;
72
+ }
73
+ export interface DeleteProjectResponse {
74
+ projectDelete: ProjectArchivePayload;
75
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./projects.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export {};
@@ -5,3 +5,5 @@ export declare const GET_PROJECT_TEAM_ISSUES_QUERY = "\n query GetProjectTeamIs
5
5
  export declare const SEARCH_PROJECTS_BY_NAME_QUERY = "\n query SearchProjectsByName($name: String!) {\n projects(filter: { name: { containsIgnoreCase: $name } }, first: 10) {\n nodes {\n id\n name\n state\n teams {\n nodes { id key name }\n }\n }\n }\n }\n";
6
6
  export declare const CREATE_PROJECT_MUTATION = "\n mutation CreateProject($input: ProjectCreateInput!) {\n projectCreate(input: $input) {\n success\n project {\n id\n name\n state\n teams {\n nodes { id key name }\n }\n }\n }\n }\n";
7
7
  export declare const UPDATE_PROJECT_MUTATION = "\n mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {\n projectUpdate(id: $id, input: $input) {\n success\n project {\n id\n name\n teams {\n nodes {\n id\n key\n name\n }\n }\n }\n }\n }\n";
8
+ export declare const ARCHIVE_PROJECT_MUTATION = "\n mutation ArchiveProject($id: String!) {\n projectArchive(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
9
+ export declare const DELETE_PROJECT_MUTATION = "\n mutation DeleteProject($id: String!) {\n projectDelete(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
@@ -102,3 +102,25 @@ export const UPDATE_PROJECT_MUTATION = `
102
102
  }
103
103
  }
104
104
  `;
105
+ export const ARCHIVE_PROJECT_MUTATION = `
106
+ mutation ArchiveProject($id: String!) {
107
+ projectArchive(id: $id) {
108
+ success
109
+ lastSyncId
110
+ entity {
111
+ id
112
+ }
113
+ }
114
+ }
115
+ `;
116
+ export const DELETE_PROJECT_MUTATION = `
117
+ mutation DeleteProject($id: String!) {
118
+ projectDelete(id: $id) {
119
+ success
120
+ lastSyncId
121
+ entity {
122
+ id
123
+ }
124
+ }
125
+ }
126
+ `;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./releases.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export interface ReleaseStageRef {
6
+ id: string;
7
+ name: string;
8
+ type: string;
9
+ color: string | null;
10
+ }
11
+ export interface ReleasePipelineRef {
12
+ id: string;
13
+ name: string;
14
+ }
15
+ export interface ReleaseDocumentRef {
16
+ id: string;
17
+ title: string;
18
+ slugId: string;
19
+ }
20
+ /** Mirrors the release selection set used by `GET_RELEASES_QUERY`. */
21
+ export interface ReleaseListNode {
22
+ id: string;
23
+ name: string;
24
+ description: string | null;
25
+ version: string | null;
26
+ url: string | null;
27
+ startDate: string | null;
28
+ targetDate: string | null;
29
+ startedAt: string | null;
30
+ completedAt: string | null;
31
+ canceledAt: string | null;
32
+ stage: ReleaseStageRef | null;
33
+ pipeline: ReleasePipelineRef | null;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+ /** Mirrors `GET_RELEASE_BY_ID_QUERY` — adds `documents`. */
38
+ export interface ReleaseDetailNode extends ReleaseListNode {
39
+ documents: {
40
+ nodes: ReleaseDocumentRef[];
41
+ };
42
+ }
43
+ /**
44
+ * Mirrors the release selection set on `CREATE_RELEASE_MUTATION` —
45
+ * a subset of `ReleaseListNode` (no startDate/targetDate/lifecycle
46
+ * timestamps; no documents).
47
+ */
48
+ export interface CreatedReleaseNode {
49
+ id: string;
50
+ name: string;
51
+ description: string | null;
52
+ version: string | null;
53
+ url: string | null;
54
+ stage: ReleaseStageRef | null;
55
+ pipeline: ReleasePipelineRef | null;
56
+ createdAt: string;
57
+ updatedAt: string;
58
+ }
59
+ export interface ReleasePipelineNode {
60
+ id: string;
61
+ name: string;
62
+ stages: {
63
+ nodes: ReleaseStageRef[];
64
+ };
65
+ }
66
+ export interface GetReleasesResponse {
67
+ releases: {
68
+ nodes: ReleaseListNode[];
69
+ };
70
+ }
71
+ export interface GetReleaseByIdResponse {
72
+ release: ReleaseDetailNode | null;
73
+ }
74
+ export interface CreateReleaseResponse {
75
+ releaseCreate: {
76
+ success: boolean;
77
+ release: CreatedReleaseNode | null;
78
+ };
79
+ }
80
+ export interface GetReleasePipelinesResponse {
81
+ releasePipelines: {
82
+ nodes: ReleasePipelineNode[];
83
+ };
84
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./releases.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Typed response shapes for `SEMANTIC_SEARCH_QUERY` and the inline
3
+ * templates listing in `commands/search.ts`. See `./issues-types.ts`
4
+ * for the rationale (ALL-937).
5
+ */
6
+ export interface SearchIssueRef {
7
+ id: string;
8
+ identifier: string;
9
+ title: string;
10
+ priority: number | null;
11
+ state: {
12
+ id: string;
13
+ name: string;
14
+ } | null;
15
+ team: {
16
+ id: string;
17
+ key: string;
18
+ name: string;
19
+ } | null;
20
+ assignee: {
21
+ id: string;
22
+ name: string;
23
+ url: string | null;
24
+ } | null;
25
+ project: {
26
+ id: string;
27
+ name: string;
28
+ } | null;
29
+ }
30
+ export interface SearchProjectRef {
31
+ id: string;
32
+ name: string;
33
+ state: string;
34
+ }
35
+ export interface SearchInitiativeRef {
36
+ id: string;
37
+ name: string;
38
+ status: string | null;
39
+ }
40
+ export interface SearchDocumentRef {
41
+ id: string;
42
+ title: string;
43
+ slugId: string | null;
44
+ project: {
45
+ id: string;
46
+ name: string;
47
+ } | null;
48
+ }
49
+ /**
50
+ * Mirrors a single semanticSearch result. The discriminator is `type`,
51
+ * but Linear returns *all* sub-objects nullable per row — only the one
52
+ * matching `type` is populated. Modelled as fully-optional sub-objects
53
+ * to match runtime reality.
54
+ */
55
+ export interface SemanticSearchResult {
56
+ type: string;
57
+ issue: SearchIssueRef | null;
58
+ project: SearchProjectRef | null;
59
+ initiative: SearchInitiativeRef | null;
60
+ document: SearchDocumentRef | null;
61
+ }
62
+ export interface SemanticSearchResponse {
63
+ semanticSearch: {
64
+ results: SemanticSearchResult[];
65
+ } | null;
66
+ }
67
+ /**
68
+ * Mirrors the inline `TEMPLATES_QUERY` in `commands/search.ts` —
69
+ * a smaller selection set than the full templates query, dropping
70
+ * fields the search command doesn't display.
71
+ */
72
+ export interface SearchTemplateNode {
73
+ id: string;
74
+ name: string;
75
+ type: string;
76
+ description: string | null;
77
+ team: {
78
+ key: string;
79
+ } | null;
80
+ creator: {
81
+ name: string;
82
+ } | null;
83
+ }
84
+ export interface SearchTemplatesResponse {
85
+ templates: SearchTemplateNode[];
86
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Typed response shapes for `SEMANTIC_SEARCH_QUERY` and the inline
3
+ * templates listing in `commands/search.ts`. See `./issues-types.ts`
4
+ * for the rationale (ALL-937).
5
+ */
6
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./templates.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * Note: `templates` is unusual — it returns the array directly rather
6
+ * than wrapped in `{ nodes: ... }`. The response type below mirrors
7
+ * what the API actually returns, not the connection convention.
8
+ */
9
+ export interface TemplateTeamRef {
10
+ id: string;
11
+ key: string;
12
+ name: string;
13
+ }
14
+ export interface TemplateCreatorRef {
15
+ id: string;
16
+ name: string;
17
+ }
18
+ /**
19
+ * Mirrors the template selection set used by `TEMPLATES_LIST_QUERY` /
20
+ * `TEMPLATE_BY_ID_QUERY` / `TEMPLATE_CREATE_MUTATION` /
21
+ * `TEMPLATE_UPDATE_MUTATION`. (Some of those omit `creator` and
22
+ * `createdAt`; we model the union — consumers only read what their
23
+ * query selected.)
24
+ */
25
+ export interface TemplateNode {
26
+ id: string;
27
+ name: string;
28
+ type: string;
29
+ description: string | null;
30
+ templateData: unknown;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ team: TemplateTeamRef | null;
34
+ creator: TemplateCreatorRef | null;
35
+ }
36
+ export interface TemplatesListResponse {
37
+ templates: TemplateNode[];
38
+ }
39
+ export interface GetTemplateResponse {
40
+ template: TemplateNode | null;
41
+ }
42
+ export interface CreateTemplateResponse {
43
+ templateCreate: {
44
+ success: boolean;
45
+ lastSyncId?: number;
46
+ template: TemplateNode | null;
47
+ };
48
+ }
49
+ export interface UpdateTemplateResponse {
50
+ templateUpdate: {
51
+ success: boolean;
52
+ lastSyncId?: number;
53
+ template: TemplateNode | null;
54
+ };
55
+ }
56
+ export interface DeleteTemplateResponse {
57
+ templateDelete: {
58
+ success: boolean;
59
+ lastSyncId?: number;
60
+ };
61
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./templates.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * Note: `templates` is unusual — it returns the array directly rather
6
+ * than wrapped in `{ nodes: ... }`. The response type below mirrors
7
+ * what the API actually returns, not the connection convention.
8
+ */
9
+ export {};
@@ -208,8 +208,14 @@ type GraphQLPrimitive = string | number | boolean | null | undefined;
208
208
  type GraphQLValue = GraphQLPrimitive | GraphQLResponseData | GraphQLResponseData[];
209
209
  /**
210
210
  * Recursive index type for raw GraphQL responses.
211
- * All nested property access requires narrowing via `as GraphQLResponseData`
212
- * or `as string` etc. — this prevents `any` from leaking into the type system.
211
+ *
212
+ * **Prefer per-query response types** (see `src/queries/*-types.ts`)
213
+ * over `GraphQLResponseData` for any new consumer. ALL-937 swept the
214
+ * old service/command consumers; this type is kept as the fallback
215
+ * for the few generic transports (e.g. `graphql-service.rawRequest`'s
216
+ * default generic). Don't reach for `as GraphQLResponseData` to skip
217
+ * a typed shape — the per-query types catch query/consumer drift at
218
+ * compile time. This type cannot.
213
219
  */
214
220
  export interface GraphQLResponseData {
215
221
  [key: string]: GraphQLValue;
@@ -26,37 +26,44 @@ function mergeCandidates(...sources) {
26
26
  }
27
27
  return [...byId.values()];
28
28
  }
29
- function mergeRelationNodes(nodes, peerKey, normalizeType, out) {
30
- for (const rel of nodes ?? []) {
31
- const peer = rel[peerKey];
29
+ function mergeOutgoingRelations(nodes, out) {
30
+ for (const rel of nodes) {
31
+ const peer = rel.relatedIssue;
32
32
  if (!peer) {
33
33
  continue;
34
34
  }
35
- const id = peer.identifier;
36
- const uuid = peer.id;
37
- const type = normalizeType(rel.type);
38
- if (id && !out.byIdentifier.has(id)) {
39
- out.byIdentifier.set(id, type);
35
+ if (!out.byIdentifier.has(peer.identifier)) {
36
+ out.byIdentifier.set(peer.identifier, rel.type);
40
37
  }
41
- if (uuid && !out.byUuid.has(uuid)) {
42
- out.byUuid.set(uuid, type);
38
+ if (!out.byUuid.has(peer.id)) {
39
+ out.byUuid.set(peer.id, rel.type);
40
+ }
41
+ }
42
+ }
43
+ function mergeIncomingRelations(nodes, out) {
44
+ for (const rel of nodes) {
45
+ const peer = rel.issue;
46
+ if (!peer) {
47
+ continue;
48
+ }
49
+ // Normalize incoming "blocks" → "blockedBy" so the reported type reads naturally
50
+ const type = rel.type === "blocks" ? "blockedBy" : rel.type;
51
+ if (!out.byIdentifier.has(peer.identifier)) {
52
+ out.byIdentifier.set(peer.identifier, type);
53
+ }
54
+ if (!out.byUuid.has(peer.id)) {
55
+ out.byUuid.set(peer.id, type);
43
56
  }
44
57
  }
45
58
  }
46
59
  async function fetchExistingRelations(issueId, graphQLService) {
47
- const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
48
- id: issueId,
49
- });
50
- const issue = result.issue;
60
+ const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: issueId });
51
61
  const out = { byIdentifier: new Map(), byUuid: new Map() };
52
- if (!issue) {
62
+ if (!result.issue) {
53
63
  return out;
54
64
  }
55
- const outgoing = issue.relations;
56
- mergeRelationNodes(outgoing?.nodes, "relatedIssue", (raw) => raw, out);
57
- const incoming = issue.inverseRelations;
58
- // Normalize incoming "blocks" → "blockedBy" so the reported type reads naturally
59
- mergeRelationNodes(incoming?.nodes, "issue", (raw) => (raw === "blocks" ? "blockedBy" : raw), out);
65
+ mergeOutgoingRelations(result.issue.relations.nodes, out);
66
+ mergeIncomingRelations(result.issue.inverseRelations.nodes, out);
60
67
  return out;
61
68
  }
62
69
  function describeError(err) {
@@ -70,11 +77,9 @@ async function createTypedRelation(ref, sourceId, resolvedId, graphQLService) {
70
77
  type: ref.type,
71
78
  },
72
79
  });
73
- const create = result.issueRelationCreate;
74
- const issueRelation = create?.issueRelation;
80
+ const issueRelation = result.issueRelationCreate.issueRelation;
75
81
  // The peer (the issue NOT identified by `sourceId`) is whichever side wasn't the source
76
- const peerKey = ref.reverse ? "issue" : "relatedIssue";
77
- const peer = issueRelation?.[peerKey];
82
+ const peer = ref.reverse ? issueRelation?.issue : issueRelation?.relatedIssue;
78
83
  return {
79
84
  identifier: peer?.identifier ?? ref.identifier,
80
85
  title: peer?.title ?? "",
@@ -4,9 +4,9 @@ function transformAttachment(att) {
4
4
  return {
5
5
  id: att.id,
6
6
  url: att.url,
7
- title: att.title || undefined,
8
- createdAt: att.createdAt || undefined,
9
- updatedAt: att.updatedAt || undefined,
7
+ title: att.title ?? undefined,
8
+ createdAt: att.createdAt ?? undefined,
9
+ updatedAt: att.updatedAt ?? undefined,
10
10
  };
11
11
  }
12
12
  class GraphQLAttachmentsService {
@@ -17,15 +17,14 @@ class GraphQLAttachmentsService {
17
17
  async createAttachment(input) {
18
18
  const result = await this.graphqlService.rawRequest(CREATE_ATTACHMENT_MUTATION, { input });
19
19
  const attachmentCreate = result.attachmentCreate;
20
- if (!attachmentCreate.success) {
20
+ if (!attachmentCreate.success || !attachmentCreate.attachment) {
21
21
  throw new Error(`Failed to create attachment on issue ${input.issueId} for URL "${input.url}"`);
22
22
  }
23
23
  return transformAttachment(attachmentCreate.attachment);
24
24
  }
25
25
  async deleteAttachment(id) {
26
26
  const result = await this.graphqlService.rawRequest(DELETE_ATTACHMENT_MUTATION, { id });
27
- const attachmentDelete = result.attachmentDelete;
28
- if (!attachmentDelete.success) {
27
+ if (!result.attachmentDelete.success) {
29
28
  throw new Error(`Failed to delete attachment: ${id}`);
30
29
  }
31
30
  return true;
@@ -35,9 +34,7 @@ class GraphQLAttachmentsService {
35
34
  if (!result.issue) {
36
35
  throw new Error(`Issue not found: ${issueId}`);
37
36
  }
38
- const issue = result.issue;
39
- const attachments = issue.attachments;
40
- return attachments.nodes.map(transformAttachment);
37
+ return result.issue.attachments.nodes.map(transformAttachment);
41
38
  }
42
39
  }
43
40
  export async function createGraphQLAttachmentsService(options) {