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 +7 -0
- package/bin/adapter/repositories/GitHubProjectRepository.js +71 -0
- package/bin/adapter/repositories/GitHubProjectRepository.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/GitHubIssueRepository.test.ts +2 -0
- package/src/adapter/repositories/GitHubProjectRepository.test.ts +152 -1
- package/src/adapter/repositories/GitHubProjectRepository.ts +87 -0
- package/src/domain/entities/Project.ts +1 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +2 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +2 -0
- package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +1 -0
- package/types/adapter/repositories/GitHubProjectRepository.d.ts +1 -0
- package/types/adapter/repositories/GitHubProjectRepository.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +1 -0
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
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":";;;
|
|
1
|
+
{"version":3,"file":"GitHubProjectRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubProjectRepository.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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
|
}
|
|
@@ -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(),
|
|
@@ -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;
|
|
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"}
|
|
@@ -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;
|
|
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 +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;
|
|
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"}
|