github-issue-tower-defence-management 1.123.2 → 1.124.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 +14 -0
- package/bin/adapter/entry-points/cli/projectConfig.js +6 -11
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +21 -36
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/githubGraphqlClient.js +99 -0
- package/bin/adapter/repositories/githubGraphqlClient.js.map +1 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -33
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +79 -142
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +29 -2
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/projectConfig.ts +6 -11
- package/src/adapter/repositories/GraphqlProjectRepository.ts +81 -77
- package/src/adapter/repositories/githubGraphqlClient.test.ts +251 -0
- package/src/adapter/repositories/githubGraphqlClient.ts +122 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +16 -33
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +62 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +208 -237
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +190 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +40 -10
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/githubGraphqlClient.d.ts +21 -0
- package/types/adapter/repositories/githubGraphqlClient.d.ts.map +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +2 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const mockPost = jest.fn();
|
|
2
|
+
|
|
3
|
+
jest.mock('ky', () => ({
|
|
4
|
+
default: {
|
|
5
|
+
post: mockPost,
|
|
6
|
+
get: jest.fn(),
|
|
7
|
+
put: jest.fn(),
|
|
8
|
+
patch: jest.fn(),
|
|
9
|
+
delete: jest.fn(),
|
|
10
|
+
extend: jest.fn(),
|
|
11
|
+
create: jest.fn(),
|
|
12
|
+
stop: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
__esModule: true,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
GITHUB_GRAPHQL_ENDPOINT,
|
|
19
|
+
RATE_LIMIT_SELECTION,
|
|
20
|
+
extractGraphqlOperationName,
|
|
21
|
+
fetchGithubGraphql,
|
|
22
|
+
injectRateLimitSelection,
|
|
23
|
+
isMutationOperation,
|
|
24
|
+
logGithubGraphqlCost,
|
|
25
|
+
postGithubGraphqlJson,
|
|
26
|
+
} from './githubGraphqlClient';
|
|
27
|
+
|
|
28
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
29
|
+
typeof value === 'object' && value !== null;
|
|
30
|
+
|
|
31
|
+
const expectRecord = (value: unknown): Record<string, unknown> => {
|
|
32
|
+
if (!isRecord(value)) {
|
|
33
|
+
throw new Error(`Expected an object but got: ${String(value)}`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getMockCallArguments = (
|
|
39
|
+
mock: jest.Mock,
|
|
40
|
+
callIndex: number,
|
|
41
|
+
): unknown[] => {
|
|
42
|
+
const call: unknown = mock.mock.calls[callIndex];
|
|
43
|
+
if (!Array.isArray(call)) {
|
|
44
|
+
throw new Error('Expected a recorded mock call');
|
|
45
|
+
}
|
|
46
|
+
return call.map((argument: unknown) => argument);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe('githubGraphqlClient', () => {
|
|
50
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.clearAllMocks();
|
|
54
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
consoleLogSpy.mockRestore();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('isMutationOperation', () => {
|
|
62
|
+
it('returns true for a mutation operation', () => {
|
|
63
|
+
expect(isMutationOperation('mutation AddItem { x }')).toBe(true);
|
|
64
|
+
expect(isMutationOperation('\n mutation { x }')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns false for query operations', () => {
|
|
68
|
+
expect(isMutationOperation('query GetItem { x }')).toBe(false);
|
|
69
|
+
expect(isMutationOperation('\n query($a: Int!) { x }')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('extractGraphqlOperationName', () => {
|
|
74
|
+
it('extracts the operation name from a named query', () => {
|
|
75
|
+
expect(
|
|
76
|
+
extractGraphqlOperationName('query GetProjectItems($id: ID!) { x }'),
|
|
77
|
+
).toBe('GetProjectItems');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('extracts the operation name from a named mutation', () => {
|
|
81
|
+
expect(
|
|
82
|
+
extractGraphqlOperationName('mutation AddIssueToProject { x }'),
|
|
83
|
+
).toBe('AddIssueToProject');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns anonymous for an unnamed query', () => {
|
|
87
|
+
expect(extractGraphqlOperationName('query($a: Int!) { x }')).toBe(
|
|
88
|
+
'anonymous',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('injectRateLimitSelection', () => {
|
|
94
|
+
it('injects the rateLimit selection before the closing brace of a query', () => {
|
|
95
|
+
const injected = injectRateLimitSelection(
|
|
96
|
+
'query GetItem($id: ID!) {\n node(id: $id) { id }\n}',
|
|
97
|
+
);
|
|
98
|
+
expect(injected).toContain(RATE_LIMIT_SELECTION);
|
|
99
|
+
expect(injected.trimEnd().endsWith('}')).toBe(true);
|
|
100
|
+
const rateLimitIndex = injected.indexOf(RATE_LIMIT_SELECTION);
|
|
101
|
+
const lastBraceIndex = injected.lastIndexOf('}');
|
|
102
|
+
expect(rateLimitIndex).toBeLessThan(lastBraceIndex);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('does not modify a mutation', () => {
|
|
106
|
+
const mutation = 'mutation AddItem { addItem { id } }';
|
|
107
|
+
expect(injectRateLimitSelection(mutation)).toBe(mutation);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('injects into an anonymous query', () => {
|
|
111
|
+
const injected = injectRateLimitSelection(
|
|
112
|
+
'\n query($a: Int!) {\n node { id }\n }\n',
|
|
113
|
+
);
|
|
114
|
+
expect(injected).toContain(RATE_LIMIT_SELECTION);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('logGithubGraphqlCost', () => {
|
|
119
|
+
it('logs one line with query name, cost and remaining', () => {
|
|
120
|
+
logGithubGraphqlCost('query GetProjectItems { x }', {
|
|
121
|
+
data: { rateLimit: { cost: 3, remaining: 4200 } },
|
|
122
|
+
});
|
|
123
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
125
|
+
'githubGraphqlClient: query=GetProjectItems cost=3 remaining=4200',
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('does not log when rateLimit is absent', () => {
|
|
130
|
+
logGithubGraphqlCost('query GetProjectItems { x }', {
|
|
131
|
+
data: { node: null },
|
|
132
|
+
});
|
|
133
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not log when the body is not an object', () => {
|
|
137
|
+
logGithubGraphqlCost('query GetProjectItems { x }', undefined);
|
|
138
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('postGithubGraphqlJson', () => {
|
|
143
|
+
it('sends the query with the injected rateLimit selection and logs the cost', async () => {
|
|
144
|
+
mockPost.mockReturnValue({
|
|
145
|
+
json: jest.fn().mockResolvedValue({
|
|
146
|
+
data: {
|
|
147
|
+
node: { id: 'x' },
|
|
148
|
+
rateLimit: { cost: 1, remaining: 4999 },
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
const response = await postGithubGraphqlJson<{
|
|
153
|
+
data: { node: { id: string } };
|
|
154
|
+
}>({
|
|
155
|
+
ghToken: 'token-a',
|
|
156
|
+
query: 'query GetItem($id: ID!) { node(id: $id) { id } }',
|
|
157
|
+
variables: { id: 'x' },
|
|
158
|
+
});
|
|
159
|
+
expect(response.data.node.id).toBe('x');
|
|
160
|
+
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
161
|
+
const call = getMockCallArguments(mockPost, 0);
|
|
162
|
+
expect(call[0]).toBe(GITHUB_GRAPHQL_ENDPOINT);
|
|
163
|
+
const options = expectRecord(call[1]);
|
|
164
|
+
const json = expectRecord(options.json);
|
|
165
|
+
const headers = expectRecord(options.headers);
|
|
166
|
+
expect(json.query).toContain(RATE_LIMIT_SELECTION);
|
|
167
|
+
expect(json.variables).toEqual({ id: 'x' });
|
|
168
|
+
expect(headers.Authorization).toBe('Bearer token-a');
|
|
169
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
170
|
+
'githubGraphqlClient: query=GetItem cost=1 remaining=4999',
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('sends a mutation unchanged and does not log', async () => {
|
|
175
|
+
mockPost.mockReturnValue({
|
|
176
|
+
json: jest.fn().mockResolvedValue({
|
|
177
|
+
data: { addItem: { id: 'x' } },
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
const mutation = 'mutation AddItem { addItem { id } }';
|
|
181
|
+
await postGithubGraphqlJson({
|
|
182
|
+
ghToken: 'token-a',
|
|
183
|
+
query: mutation,
|
|
184
|
+
});
|
|
185
|
+
const call = getMockCallArguments(mockPost, 0);
|
|
186
|
+
const options = expectRecord(call[1]);
|
|
187
|
+
const json = expectRecord(options.json);
|
|
188
|
+
expect(json.query).toBe(mutation);
|
|
189
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('fetchGithubGraphql', () => {
|
|
194
|
+
const originalFetch = global.fetch;
|
|
195
|
+
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
global.fetch = originalFetch;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('sends the query with the injected rateLimit selection and logs the cost', async () => {
|
|
201
|
+
const responseBody = {
|
|
202
|
+
data: {
|
|
203
|
+
repository: { issue: null },
|
|
204
|
+
rateLimit: { cost: 2, remaining: 4321 },
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const fetchMock = jest
|
|
208
|
+
.fn()
|
|
209
|
+
.mockResolvedValue(
|
|
210
|
+
new Response(JSON.stringify(responseBody), { status: 200 }),
|
|
211
|
+
);
|
|
212
|
+
global.fetch = fetchMock;
|
|
213
|
+
const response = await fetchGithubGraphql({
|
|
214
|
+
ghToken: 'token-b',
|
|
215
|
+
query: 'query PullRequestStatus($a: Int!) { x }',
|
|
216
|
+
variables: { a: 1 },
|
|
217
|
+
});
|
|
218
|
+
expect(response.ok).toBe(true);
|
|
219
|
+
await expect(response.json()).resolves.toEqual(responseBody);
|
|
220
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
221
|
+
const call = getMockCallArguments(fetchMock, 0);
|
|
222
|
+
expect(call[0]).toBe(GITHUB_GRAPHQL_ENDPOINT);
|
|
223
|
+
const init = expectRecord(call[1]);
|
|
224
|
+
const headers = expectRecord(init.headers);
|
|
225
|
+
expect(init.method).toBe('POST');
|
|
226
|
+
expect(headers.Authorization).toBe('Bearer token-b');
|
|
227
|
+
if (typeof init.body !== 'string') {
|
|
228
|
+
throw new Error('Expected the request body to be a string');
|
|
229
|
+
}
|
|
230
|
+
const sentBody = expectRecord(JSON.parse(init.body));
|
|
231
|
+
expect(sentBody.query).toContain(RATE_LIMIT_SELECTION);
|
|
232
|
+
expect(sentBody.variables).toEqual({ a: 1 });
|
|
233
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
234
|
+
'githubGraphqlClient: query=PullRequestStatus cost=2 remaining=4321',
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('returns the response without logging when the request failed', async () => {
|
|
239
|
+
const fetchMock = jest
|
|
240
|
+
.fn()
|
|
241
|
+
.mockResolvedValue(new Response('rate limited', { status: 403 }));
|
|
242
|
+
global.fetch = fetchMock;
|
|
243
|
+
const response = await fetchGithubGraphql({
|
|
244
|
+
ghToken: 'token-b',
|
|
245
|
+
query: 'query PullRequestStatus($a: Int!) { x }',
|
|
246
|
+
});
|
|
247
|
+
expect(response.status).toBe(403);
|
|
248
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
|
|
3
|
+
export const GITHUB_GRAPHQL_ENDPOINT = 'https://api.github.com/graphql';
|
|
4
|
+
|
|
5
|
+
export const RATE_LIMIT_SELECTION = 'rateLimit { cost remaining }';
|
|
6
|
+
|
|
7
|
+
export type GithubGraphqlRateLimit = {
|
|
8
|
+
cost: number;
|
|
9
|
+
remaining: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const isMutationOperation = (query: string): boolean =>
|
|
13
|
+
query.trimStart().startsWith('mutation');
|
|
14
|
+
|
|
15
|
+
export const extractGraphqlOperationName = (query: string): string => {
|
|
16
|
+
const match = query.match(
|
|
17
|
+
/^\s*(?:query|mutation)\s+([A-Za-z_][A-Za-z0-9_]*)/,
|
|
18
|
+
);
|
|
19
|
+
return match ? match[1] : 'anonymous';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const injectRateLimitSelection = (query: string): string => {
|
|
23
|
+
if (isMutationOperation(query)) {
|
|
24
|
+
return query;
|
|
25
|
+
}
|
|
26
|
+
const lastBraceIndex = query.lastIndexOf('}');
|
|
27
|
+
if (lastBraceIndex === -1) {
|
|
28
|
+
return query;
|
|
29
|
+
}
|
|
30
|
+
return `${query.slice(0, lastBraceIndex)} ${RATE_LIMIT_SELECTION}\n${query.slice(lastBraceIndex)}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const extractRateLimit = (
|
|
34
|
+
responseBody: unknown,
|
|
35
|
+
): GithubGraphqlRateLimit | null => {
|
|
36
|
+
if (
|
|
37
|
+
typeof responseBody !== 'object' ||
|
|
38
|
+
responseBody === null ||
|
|
39
|
+
!('data' in responseBody)
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const data: unknown = responseBody.data;
|
|
44
|
+
if (typeof data !== 'object' || data === null || !('rateLimit' in data)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const rateLimit: unknown = data.rateLimit;
|
|
48
|
+
if (
|
|
49
|
+
typeof rateLimit !== 'object' ||
|
|
50
|
+
rateLimit === null ||
|
|
51
|
+
!('cost' in rateLimit) ||
|
|
52
|
+
!('remaining' in rateLimit)
|
|
53
|
+
) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const { cost, remaining } = rateLimit;
|
|
57
|
+
if (typeof cost !== 'number' || typeof remaining !== 'number') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return { cost, remaining };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const logGithubGraphqlCost = (
|
|
64
|
+
query: string,
|
|
65
|
+
responseBody: unknown,
|
|
66
|
+
): void => {
|
|
67
|
+
const rateLimit = extractRateLimit(responseBody);
|
|
68
|
+
if (!rateLimit) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log(
|
|
72
|
+
`githubGraphqlClient: query=${extractGraphqlOperationName(query)} cost=${rateLimit.cost} remaining=${rateLimit.remaining}`,
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const postGithubGraphqlJson = async <T>(params: {
|
|
77
|
+
ghToken: string;
|
|
78
|
+
query: string;
|
|
79
|
+
variables?: Record<string, unknown>;
|
|
80
|
+
}): Promise<T> => {
|
|
81
|
+
const response = await ky
|
|
82
|
+
.post(GITHUB_GRAPHQL_ENDPOINT, {
|
|
83
|
+
json: {
|
|
84
|
+
query: injectRateLimitSelection(params.query),
|
|
85
|
+
...(params.variables !== undefined
|
|
86
|
+
? { variables: params.variables }
|
|
87
|
+
: {}),
|
|
88
|
+
},
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${params.ghToken}`,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
.json<T>();
|
|
94
|
+
logGithubGraphqlCost(params.query, response);
|
|
95
|
+
return response;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const fetchGithubGraphql = async (params: {
|
|
99
|
+
ghToken: string;
|
|
100
|
+
query: string;
|
|
101
|
+
variables?: Record<string, unknown>;
|
|
102
|
+
}): Promise<Response> => {
|
|
103
|
+
const response = await fetch(GITHUB_GRAPHQL_ENDPOINT, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${params.ghToken}`,
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
query: injectRateLimitSelection(params.query),
|
|
111
|
+
variables: params.variables,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
if (response.ok) {
|
|
115
|
+
const responseBody: unknown = await response
|
|
116
|
+
.clone()
|
|
117
|
+
.json()
|
|
118
|
+
.catch((): null => null);
|
|
119
|
+
logGithubGraphqlCost(params.query, responseBody);
|
|
120
|
+
}
|
|
121
|
+
return response;
|
|
122
|
+
};
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { LocalStorageCacheRepository } from '../LocalStorageCacheRepository';
|
|
21
21
|
import typia from 'typia';
|
|
22
22
|
import { BaseGitHubRepository } from '../BaseGitHubRepository';
|
|
23
|
+
import { fetchGithubGraphql } from '../githubGraphqlClient';
|
|
23
24
|
import { normalizeFieldName } from '../utils';
|
|
24
25
|
import { LocalStorageRepository } from '../LocalStorageRepository';
|
|
25
26
|
import { Member } from '../../../domain/entities/Member';
|
|
@@ -1160,7 +1161,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1160
1161
|
mergeStateStatus: string | null;
|
|
1161
1162
|
} | null> => {
|
|
1162
1163
|
const query = `
|
|
1163
|
-
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
1164
|
+
query PullRequestMergeability($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
1164
1165
|
repository(owner: $owner, name: $repo) {
|
|
1165
1166
|
pullRequest(number: $prNumber) {
|
|
1166
1167
|
mergeable
|
|
@@ -1183,16 +1184,10 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1183
1184
|
}
|
|
1184
1185
|
|
|
1185
1186
|
const response = await this.fetchWithRateLimitRetry(() =>
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
'Content-Type': 'application/json',
|
|
1191
|
-
},
|
|
1192
|
-
body: JSON.stringify({
|
|
1193
|
-
query,
|
|
1194
|
-
variables: { owner, repo, prNumber },
|
|
1195
|
-
}),
|
|
1187
|
+
fetchGithubGraphql({
|
|
1188
|
+
ghToken: this.ghToken,
|
|
1189
|
+
query,
|
|
1190
|
+
variables: { owner, repo, prNumber },
|
|
1196
1191
|
}),
|
|
1197
1192
|
);
|
|
1198
1193
|
|
|
@@ -1244,7 +1239,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1244
1239
|
}
|
|
1245
1240
|
|
|
1246
1241
|
const query = `
|
|
1247
|
-
query($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
|
|
1242
|
+
query IssueRelatedOpenPullRequests($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
|
|
1248
1243
|
repository(owner: $owner, name: $repo) {
|
|
1249
1244
|
issue(number: $issueNumber) {
|
|
1250
1245
|
timelineItems(first: 100, after: $after, itemTypes: [CROSS_REFERENCED_EVENT]) {
|
|
@@ -1348,16 +1343,10 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1348
1343
|
|
|
1349
1344
|
while (hasNextPage) {
|
|
1350
1345
|
const response = await this.fetchWithRateLimitRetry(() =>
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
'Content-Type': 'application/json',
|
|
1356
|
-
},
|
|
1357
|
-
body: JSON.stringify({
|
|
1358
|
-
query,
|
|
1359
|
-
variables: { owner, repo, issueNumber, after },
|
|
1360
|
-
}),
|
|
1346
|
+
fetchGithubGraphql({
|
|
1347
|
+
ghToken: this.ghToken,
|
|
1348
|
+
query,
|
|
1349
|
+
variables: { owner, repo, issueNumber, after },
|
|
1361
1350
|
}),
|
|
1362
1351
|
);
|
|
1363
1352
|
|
|
@@ -1488,7 +1477,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1488
1477
|
const { owner, repo, issueNumber: prNumber } = parsedUrl;
|
|
1489
1478
|
|
|
1490
1479
|
const query = `
|
|
1491
|
-
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
1480
|
+
query PullRequestStatus($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
1492
1481
|
repository(owner: $owner, name: $repo) {
|
|
1493
1482
|
pullRequest(number: $prNumber) {
|
|
1494
1483
|
url
|
|
@@ -1565,16 +1554,10 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1565
1554
|
`;
|
|
1566
1555
|
|
|
1567
1556
|
const response = await this.fetchWithRateLimitRetry(() =>
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
'Content-Type': 'application/json',
|
|
1573
|
-
},
|
|
1574
|
-
body: JSON.stringify({
|
|
1575
|
-
query,
|
|
1576
|
-
variables: { owner, repo, prNumber },
|
|
1577
|
-
}),
|
|
1557
|
+
fetchGithubGraphql({
|
|
1558
|
+
ghToken: this.ghToken,
|
|
1559
|
+
query,
|
|
1560
|
+
variables: { owner, repo, prNumber },
|
|
1578
1561
|
}),
|
|
1579
1562
|
);
|
|
1580
1563
|
|
|
@@ -22,6 +22,8 @@ import { HTTPError } from 'ky';
|
|
|
22
22
|
import {
|
|
23
23
|
GraphqlProjectItemRepository,
|
|
24
24
|
PAGINATION_DELAY_MS,
|
|
25
|
+
PROJECT_ITEM_ASSIGNEES_FIRST,
|
|
26
|
+
PROJECT_ITEM_LABELS_FIRST,
|
|
25
27
|
RATE_LIMIT_MAX_RETRIES,
|
|
26
28
|
callWithRateLimitRetry,
|
|
27
29
|
} from './GraphqlProjectItemRepository';
|
|
@@ -160,6 +162,66 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
160
162
|
mockPost.mockReset();
|
|
161
163
|
});
|
|
162
164
|
|
|
165
|
+
it('requests the reduced labels and assignees selection sizes', async () => {
|
|
166
|
+
const repository = new GraphqlProjectItemRepository(
|
|
167
|
+
new LocalStorageRepository(),
|
|
168
|
+
'dummy-token',
|
|
169
|
+
);
|
|
170
|
+
mockPost.mockReturnValueOnce(makePageResponse(false, 'cursor-1', 1));
|
|
171
|
+
|
|
172
|
+
await repository.fetchProjectItems('test-project-id');
|
|
173
|
+
|
|
174
|
+
expect(PROJECT_ITEM_LABELS_FIRST).toBe(20);
|
|
175
|
+
expect(PROJECT_ITEM_ASSIGNEES_FIRST).toBe(10);
|
|
176
|
+
const sentQuery = extractRequestedQueryFromMockCall(
|
|
177
|
+
mockPost.mock.calls[0],
|
|
178
|
+
);
|
|
179
|
+
expect(sentQuery).toContain(
|
|
180
|
+
`labels(first: ${PROJECT_ITEM_LABELS_FIRST})`,
|
|
181
|
+
);
|
|
182
|
+
expect(sentQuery).toContain(
|
|
183
|
+
`assignees(first: ${PROJECT_ITEM_ASSIGNEES_FIRST})`,
|
|
184
|
+
);
|
|
185
|
+
expect(sentQuery).not.toContain('labels(first: 100)');
|
|
186
|
+
expect(sentQuery).not.toContain('assignees(first: 20)');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('logs the rateLimit cost of the request in one line', async () => {
|
|
190
|
+
const repository = new GraphqlProjectItemRepository(
|
|
191
|
+
new LocalStorageRepository(),
|
|
192
|
+
'dummy-token',
|
|
193
|
+
);
|
|
194
|
+
mockPost.mockReturnValueOnce(
|
|
195
|
+
mockJsonResponse({
|
|
196
|
+
data: {
|
|
197
|
+
node: {
|
|
198
|
+
items: {
|
|
199
|
+
totalCount: 0,
|
|
200
|
+
pageInfo: {
|
|
201
|
+
endCursor: 'cursor-1',
|
|
202
|
+
startCursor: 'cursor-start',
|
|
203
|
+
hasNextPage: false,
|
|
204
|
+
},
|
|
205
|
+
nodes: [],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
rateLimit: { cost: 3, remaining: 4200 },
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
const consoleLogSpy = jest
|
|
213
|
+
.spyOn(console, 'log')
|
|
214
|
+
.mockImplementation(() => {});
|
|
215
|
+
try {
|
|
216
|
+
await repository.fetchProjectItems('test-project-id');
|
|
217
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
218
|
+
'githubGraphqlClient: query=GetProjectItems cost=3 remaining=4200',
|
|
219
|
+
);
|
|
220
|
+
} finally {
|
|
221
|
+
consoleLogSpy.mockRestore();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
163
225
|
it('should sleep between paginated requests to avoid 403', async () => {
|
|
164
226
|
const localStorageRepository = new LocalStorageRepository();
|
|
165
227
|
const repository = new GraphqlProjectItemRepository(
|