npm-cli-gh-issue-preparator 1.5.0 → 1.6.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.6.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.5.0...v1.6.0) (2026-01-26)
2
+
3
+
4
+ ### Features
5
+
6
+ * **core:** add prepareStatus method to ProjectRepository ([931b2ed](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/931b2ed9e4298235b48afa0b6a82d81a49ea78d2))
7
+
1
8
  # [1.5.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.4.0...v1.5.0) (2026-01-20)
2
9
 
3
10
 
@@ -46,12 +46,14 @@ class GitHubProjectRepository {
46
46
  fields(first: 100) {
47
47
  nodes {
48
48
  ... on ProjectV2SingleSelectField {
49
+ id
49
50
  name
50
51
  options {
51
52
  name
52
53
  }
53
54
  }
54
55
  ... on ProjectV2Field {
56
+ id
55
57
  name
56
58
  }
57
59
  }
@@ -66,12 +68,14 @@ class GitHubProjectRepository {
66
68
  fields(first: 100) {
67
69
  nodes {
68
70
  ... on ProjectV2SingleSelectField {
71
+ id
69
72
  name
70
73
  options {
71
74
  name
72
75
  }
73
76
  }
74
77
  ... on ProjectV2Field {
78
+ id
75
79
  name
76
80
  }
77
81
  }
@@ -116,6 +120,73 @@ class GitHubProjectRepository {
116
120
  name: project.title,
117
121
  statuses,
118
122
  customFieldNames: fields.map((f) => f.name),
123
+ statusFieldId: statusField?.id ?? null,
124
+ };
125
+ }
126
+ async prepareStatus(name, project) {
127
+ if (project.statuses.includes(name)) {
128
+ return project;
129
+ }
130
+ if (!project.statusFieldId) {
131
+ throw new Error(`Status field not found in project "${project.name}". ` +
132
+ `Cannot add status "${name}".`);
133
+ }
134
+ const existingOptions = project.statuses.map((statusName) => ({
135
+ name: statusName,
136
+ color: 'GRAY',
137
+ description: '',
138
+ }));
139
+ const newOptions = [
140
+ ...existingOptions,
141
+ { name, color: 'GRAY', description: '' },
142
+ ];
143
+ const mutation = `
144
+ mutation($fieldId: ID!, $singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!]!) {
145
+ updateProjectV2Field(input: {
146
+ fieldId: $fieldId
147
+ singleSelectOptions: $singleSelectOptions
148
+ }) {
149
+ projectV2Field {
150
+ ... on ProjectV2SingleSelectField {
151
+ id
152
+ name
153
+ options {
154
+ name
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ `;
161
+ const response = await fetch('https://api.github.com/graphql', {
162
+ method: 'POST',
163
+ headers: {
164
+ Authorization: `Bearer ${this.token}`,
165
+ 'Content-Type': 'application/json',
166
+ },
167
+ body: JSON.stringify({
168
+ query: mutation,
169
+ variables: {
170
+ fieldId: project.statusFieldId,
171
+ singleSelectOptions: newOptions,
172
+ },
173
+ }),
174
+ });
175
+ const responseData = await response.json();
176
+ if (!isGitHubApiResponse(responseData)) {
177
+ throw new Error('Invalid API response format');
178
+ }
179
+ if (!response.ok) {
180
+ throw new Error(`GitHub API error: ${JSON.stringify(responseData)}`);
181
+ }
182
+ if (typeof responseData === 'object' &&
183
+ responseData !== null &&
184
+ 'errors' in responseData) {
185
+ throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
186
+ }
187
+ return {
188
+ ...project,
189
+ statuses: [...project.statuses, name],
119
190
  };
120
191
  }
121
192
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GitHubProjectRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubProjectRepository.ts"],"names":[],"mappings":";;;AA4BA,SAAS,mBAAmB,CAAC,KAAc;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAa,uBAAuB;IAClC,YAA6B,KAAa;QAAb,UAAK,GAAL,KAAK,CAAQ;IAAG,CAAC;IAEtC,qBAAqB,CAAC,GAAW;QAIvC,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QAC1E,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO;gBACL,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;gBAClB,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC;aAC3B,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAC5E,IAAI,SAAS,EAAE,CAAC;YACd,OAAO;gBACL,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;gBACnB,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;aAC5B,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CACzB,gDAAgD,CACjD,CAAC;QACF,IAAI,SAAS,EAAE,CAAC;YACd,OAAO;gBACL,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;gBACnB,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC;aAC5B,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,+BAA+B,GAAG,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAW;QACxB,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC;QAEjE,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2CpB,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,gCAAgC,EAAE;YAC7D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE;gBACrC,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,YAAY;gBACnB,SAAS,EAAE;oBACT,KAAK;oBACL,MAAM,EAAE,QAAQ,CAAC,aAAa,EAAE,EAAE,CAAC;iBACpC;aACF,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,qBAAqB,SAAS,EAAE,CAAC,CAAC;QACpD,CAAC;QAED,MAAM,YAAY,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACpD,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACjD,CAAC;QAED,MAAM,MAAM,GAAsB,YAAY,CAAC;QAC/C,MAAM,OAAO,GACX,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,IAAI,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC;QACvE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,EAAE,CAAC,CAAC;QAC/C,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAa,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAE1E,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,IAAI,EAAE,OAAO,CAAC,KAAK;YACnB,QAAQ;YACR,gBAAgB,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAC5C,CAAC;IACJ,CAAC;CACF;AAhID,0DAgIC"}
1
+ {"version":3,"file":"GitHubProjectRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubProjectRepository.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-cli-gh-issue-preparator",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -12,6 +12,7 @@ describe('GitHubIssueRepository', () => {
12
12
  name: 'Test Project',
13
13
  statuses: ['Awaiting Workspace', 'Preparation', 'Done'],
14
14
  customFieldNames: ['Status', 'workspace'],
15
+ statusFieldId: 'status-field-id',
15
16
  };
16
17
 
17
18
  const mockUserProject: Project = {
@@ -20,6 +21,7 @@ describe('GitHubIssueRepository', () => {
20
21
  name: 'User Project',
21
22
  statuses: ['Todo', 'Done'],
22
23
  customFieldNames: ['Status'],
24
+ statusFieldId: 'status-field-id-user',
23
25
  };
24
26
 
25
27
  beforeEach(() => {
@@ -27,6 +27,7 @@ describe('GitHubProjectRepository', () => {
27
27
  fields: {
28
28
  nodes: [
29
29
  {
30
+ id: 'status-field-id',
30
31
  name: 'Status',
31
32
  options: [
32
33
  { name: 'Awaiting workspace' },
@@ -34,7 +35,7 @@ describe('GitHubProjectRepository', () => {
34
35
  { name: 'Done' },
35
36
  ],
36
37
  },
37
- { name: 'workspace' },
38
+ { id: 'workspace-field-id', name: 'workspace' },
38
39
  ],
39
40
  },
40
41
  },
@@ -51,6 +52,7 @@ describe('GitHubProjectRepository', () => {
51
52
  name: 'Test Project',
52
53
  statuses: ['Awaiting workspace', 'Preparation', 'Done'],
53
54
  customFieldNames: ['Status', 'workspace'],
55
+ statusFieldId: 'status-field-id',
54
56
  });
55
57
 
56
58
  expect(mockFetch).toHaveBeenCalledWith(
@@ -80,6 +82,7 @@ describe('GitHubProjectRepository', () => {
80
82
  fields: {
81
83
  nodes: [
82
84
  {
85
+ id: 'user-status-field-id',
83
86
  name: 'Status',
84
87
  options: [{ name: 'Todo' }, { name: 'Done' }],
85
88
  },
@@ -99,6 +102,7 @@ describe('GitHubProjectRepository', () => {
99
102
  name: 'User Project',
100
103
  statuses: ['Todo', 'Done'],
101
104
  customFieldNames: ['Status'],
105
+ statusFieldId: 'user-status-field-id',
102
106
  });
103
107
 
104
108
  expect(mockFetch).toHaveBeenCalledWith(
@@ -192,6 +196,7 @@ describe('GitHubProjectRepository', () => {
192
196
  fields: {
193
197
  nodes: [
194
198
  {
199
+ id: 'repo-status-field-id',
195
200
  name: 'Status',
196
201
  options: [{ name: 'Open' }, { name: 'Closed' }],
197
202
  },
@@ -211,6 +216,7 @@ describe('GitHubProjectRepository', () => {
211
216
  name: 'Repo Project',
212
217
  statuses: ['Open', 'Closed'],
213
218
  customFieldNames: ['Status'],
219
+ statusFieldId: 'repo-status-field-id',
214
220
  });
215
221
  });
216
222
 
@@ -229,6 +235,7 @@ describe('GitHubProjectRepository', () => {
229
235
  fields: {
230
236
  nodes: [
231
237
  {
238
+ id: 'priority-field-id',
232
239
  name: 'Priority',
233
240
  },
234
241
  ],
@@ -247,6 +254,150 @@ describe('GitHubProjectRepository', () => {
247
254
  name: 'Project Without Status',
248
255
  statuses: [],
249
256
  customFieldNames: ['Priority'],
257
+ statusFieldId: null,
258
+ });
259
+ });
260
+
261
+ describe('prepareStatus', () => {
262
+ it('should return project as-is when status already exists', async () => {
263
+ const project = {
264
+ id: 'project-id',
265
+ url: 'https://github.com/orgs/test-org/projects/1',
266
+ name: 'Test Project',
267
+ statuses: ['Todo', 'In Progress', 'Done'],
268
+ customFieldNames: ['Status'],
269
+ statusFieldId: 'status-field-id',
270
+ };
271
+
272
+ const result = await repository.prepareStatus('In Progress', project);
273
+
274
+ expect(result).toEqual(project);
275
+ expect(mockFetch).not.toHaveBeenCalled();
276
+ });
277
+
278
+ it('should add new status when it does not exist', async () => {
279
+ const project = {
280
+ id: 'project-id',
281
+ url: 'https://github.com/orgs/test-org/projects/1',
282
+ name: 'Test Project',
283
+ statuses: ['Todo', 'Done'],
284
+ customFieldNames: ['Status'],
285
+ statusFieldId: 'status-field-id',
286
+ };
287
+
288
+ mockFetch.mockResolvedValueOnce({
289
+ ok: true,
290
+ json: jest.fn().mockResolvedValue({
291
+ data: {
292
+ updateProjectV2Field: {
293
+ projectV2Field: {
294
+ id: 'status-field-id',
295
+ name: 'Status',
296
+ options: [
297
+ { name: 'Todo' },
298
+ { name: 'Done' },
299
+ { name: 'In Progress' },
300
+ ],
301
+ },
302
+ },
303
+ },
304
+ }),
305
+ });
306
+
307
+ const result = await repository.prepareStatus('In Progress', project);
308
+
309
+ expect(result).toEqual({
310
+ ...project,
311
+ statuses: ['Todo', 'Done', 'In Progress'],
312
+ });
313
+
314
+ expect(mockFetch).toHaveBeenCalledWith(
315
+ 'https://api.github.com/graphql',
316
+ expect.objectContaining({
317
+ method: 'POST',
318
+ headers: {
319
+ Authorization: 'Bearer test-token',
320
+ 'Content-Type': 'application/json',
321
+ },
322
+ }),
323
+ );
324
+ });
325
+
326
+ it('should throw error when status field is not found', async () => {
327
+ const project = {
328
+ id: 'project-id',
329
+ url: 'https://github.com/orgs/test-org/projects/1',
330
+ name: 'Test Project',
331
+ statuses: [],
332
+ customFieldNames: ['Priority'],
333
+ statusFieldId: null,
334
+ };
335
+
336
+ await expect(
337
+ repository.prepareStatus('New Status', project),
338
+ ).rejects.toThrow('Status field not found in project "Test Project"');
339
+ });
340
+
341
+ it('should throw error when GitHub API returns error', async () => {
342
+ const project = {
343
+ id: 'project-id',
344
+ url: 'https://github.com/orgs/test-org/projects/1',
345
+ name: 'Test Project',
346
+ statuses: ['Todo'],
347
+ customFieldNames: ['Status'],
348
+ statusFieldId: 'status-field-id',
349
+ };
350
+
351
+ mockFetch.mockResolvedValueOnce({
352
+ ok: false,
353
+ json: jest.fn().mockResolvedValue({ errors: ['API Error'] }),
354
+ });
355
+
356
+ await expect(
357
+ repository.prepareStatus('New Status', project),
358
+ ).rejects.toThrow('GitHub API error');
359
+ });
360
+
361
+ it('should throw error when response format is invalid', async () => {
362
+ const project = {
363
+ id: 'project-id',
364
+ url: 'https://github.com/orgs/test-org/projects/1',
365
+ name: 'Test Project',
366
+ statuses: ['Todo'],
367
+ customFieldNames: ['Status'],
368
+ statusFieldId: 'status-field-id',
369
+ };
370
+
371
+ mockFetch.mockResolvedValueOnce({
372
+ ok: true,
373
+ json: jest.fn().mockResolvedValue(null),
374
+ });
375
+
376
+ await expect(
377
+ repository.prepareStatus('New Status', project),
378
+ ).rejects.toThrow('Invalid API response format');
379
+ });
380
+
381
+ it('should throw error when response has GraphQL errors', async () => {
382
+ const project = {
383
+ id: 'project-id',
384
+ url: 'https://github.com/orgs/test-org/projects/1',
385
+ name: 'Test Project',
386
+ statuses: ['Todo'],
387
+ customFieldNames: ['Status'],
388
+ statusFieldId: 'status-field-id',
389
+ };
390
+
391
+ mockFetch.mockResolvedValueOnce({
392
+ ok: true,
393
+ json: jest.fn().mockResolvedValue({
394
+ errors: [{ message: 'Permission denied' }],
395
+ }),
396
+ });
397
+
398
+ await expect(
399
+ repository.prepareStatus('New Status', project),
400
+ ).rejects.toThrow('GraphQL errors');
250
401
  });
251
402
  });
252
403
  });
@@ -2,6 +2,7 @@ import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/Proj
2
2
  import { Project } from '../../domain/entities/Project';
3
3
 
4
4
  type GitHubProjectField = {
5
+ id?: string;
5
6
  name: string;
6
7
  options?: Array<{ name: string }>;
7
8
  };
@@ -80,12 +81,14 @@ export class GitHubProjectRepository implements ProjectRepository {
80
81
  fields(first: 100) {
81
82
  nodes {
82
83
  ... on ProjectV2SingleSelectField {
84
+ id
83
85
  name
84
86
  options {
85
87
  name
86
88
  }
87
89
  }
88
90
  ... on ProjectV2Field {
91
+ id
89
92
  name
90
93
  }
91
94
  }
@@ -100,12 +103,14 @@ export class GitHubProjectRepository implements ProjectRepository {
100
103
  fields(first: 100) {
101
104
  nodes {
102
105
  ... on ProjectV2SingleSelectField {
106
+ id
103
107
  name
104
108
  options {
105
109
  name
106
110
  }
107
111
  }
108
112
  ... on ProjectV2Field {
113
+ id
109
114
  name
110
115
  }
111
116
  }
@@ -157,6 +162,88 @@ export class GitHubProjectRepository implements ProjectRepository {
157
162
  name: project.title,
158
163
  statuses,
159
164
  customFieldNames: fields.map((f) => f.name),
165
+ statusFieldId: statusField?.id ?? null,
166
+ };
167
+ }
168
+
169
+ async prepareStatus(name: string, project: Project): Promise<Project> {
170
+ if (project.statuses.includes(name)) {
171
+ return project;
172
+ }
173
+
174
+ if (!project.statusFieldId) {
175
+ throw new Error(
176
+ `Status field not found in project "${project.name}". ` +
177
+ `Cannot add status "${name}".`,
178
+ );
179
+ }
180
+
181
+ const existingOptions = project.statuses.map((statusName) => ({
182
+ name: statusName,
183
+ color: 'GRAY',
184
+ description: '',
185
+ }));
186
+
187
+ const newOptions = [
188
+ ...existingOptions,
189
+ { name, color: 'GRAY', description: '' },
190
+ ];
191
+
192
+ const mutation = `
193
+ mutation($fieldId: ID!, $singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!]!) {
194
+ updateProjectV2Field(input: {
195
+ fieldId: $fieldId
196
+ singleSelectOptions: $singleSelectOptions
197
+ }) {
198
+ projectV2Field {
199
+ ... on ProjectV2SingleSelectField {
200
+ id
201
+ name
202
+ options {
203
+ name
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ `;
210
+
211
+ const response = await fetch('https://api.github.com/graphql', {
212
+ method: 'POST',
213
+ headers: {
214
+ Authorization: `Bearer ${this.token}`,
215
+ 'Content-Type': 'application/json',
216
+ },
217
+ body: JSON.stringify({
218
+ query: mutation,
219
+ variables: {
220
+ fieldId: project.statusFieldId,
221
+ singleSelectOptions: newOptions,
222
+ },
223
+ }),
224
+ });
225
+
226
+ const responseData: unknown = await response.json();
227
+
228
+ if (!isGitHubApiResponse(responseData)) {
229
+ throw new Error('Invalid API response format');
230
+ }
231
+
232
+ if (!response.ok) {
233
+ throw new Error(`GitHub API error: ${JSON.stringify(responseData)}`);
234
+ }
235
+
236
+ if (
237
+ typeof responseData === 'object' &&
238
+ responseData !== null &&
239
+ 'errors' in responseData
240
+ ) {
241
+ throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
242
+ }
243
+
244
+ return {
245
+ ...project,
246
+ statuses: [...project.statuses, name],
160
247
  };
161
248
  }
162
249
  }
@@ -4,4 +4,5 @@ export type Project = {
4
4
  name: string;
5
5
  statuses: string[];
6
6
  customFieldNames: string[];
7
+ statusFieldId: string | null;
7
8
  };
@@ -17,6 +17,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
17
17
  name: 'Test Project',
18
18
  statuses: ['Preparation', 'Awaiting Quality Check', 'Done'],
19
19
  customFieldNames: ['workspace'],
20
+ statusFieldId: 'status-field-id',
20
21
  };
21
22
 
22
23
  beforeEach(() => {
@@ -24,6 +25,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
24
25
 
25
26
  mockProjectRepository = {
26
27
  getByUrl: jest.fn(),
28
+ prepareStatus: jest.fn(),
27
29
  };
28
30
 
29
31
  mockIssueRepository = {
@@ -16,11 +16,13 @@ describe('StartPreparationUseCase', () => {
16
16
  name: 'Test Project',
17
17
  statuses: ['Awaiting Workspace', 'Preparation', 'Done'],
18
18
  customFieldNames: ['workspace'],
19
+ statusFieldId: 'status-field-id',
19
20
  };
20
21
  beforeEach(() => {
21
22
  jest.resetAllMocks();
22
23
  mockProjectRepository = {
23
24
  getByUrl: jest.fn(),
25
+ prepareStatus: jest.fn(),
24
26
  };
25
27
  mockIssueRepository = {
26
28
  getAllOpened: jest.fn(),
@@ -2,4 +2,5 @@ import { Project } from '../../entities/Project';
2
2
 
3
3
  export interface ProjectRepository {
4
4
  getByUrl(url: string): Promise<Project>;
5
+ prepareStatus(name: string, project: Project): Promise<Project>;
5
6
  }
@@ -5,5 +5,6 @@ export declare class GitHubProjectRepository implements ProjectRepository {
5
5
  constructor(token: string);
6
6
  private parseGitHubProjectUrl;
7
7
  getByUrl(url: string): Promise<Project>;
8
+ prepareStatus(name: string, project: Project): Promise<Project>;
8
9
  }
9
10
  //# sourceMappingURL=GitHubProjectRepository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GitHubProjectRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubProjectRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAgCxD,qBAAa,uBAAwB,YAAW,iBAAiB;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,MAAM;IAE1C,OAAO,CAAC,qBAAqB;IAiCvB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CA4F9C"}
1
+ {"version":3,"file":"GitHubProjectRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubProjectRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAiCxD,qBAAa,uBAAwB,YAAW,iBAAiB;IACnD,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,MAAM;IAE1C,OAAO,CAAC,qBAAqB;IAiCvB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkGvC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;CAgFtE"}
@@ -4,5 +4,6 @@ export type Project = {
4
4
  name: string;
5
5
  statuses: string[];
6
6
  customFieldNames: string[];
7
+ statusFieldId: string | null;
7
8
  };
8
9
  //# sourceMappingURL=Project.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Project.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/Project.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B,CAAC"}
1
+ {"version":3,"file":"Project.d.ts","sourceRoot":"","sources":["../../../src/domain/entities/Project.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Project } from '../../entities/Project';
2
2
  export interface ProjectRepository {
3
3
  getByUrl(url: string): Promise<Project>;
4
+ prepareStatus(name: string, project: Project): Promise<Project>;
4
5
  }
5
6
  //# sourceMappingURL=ProjectRepository.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ProjectRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC"}
1
+ {"version":3,"file":"ProjectRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/ProjectRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACjE"}