github-issue-tower-defence-management 1.52.0 → 1.52.1

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.52.1](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.52.0...v1.52.1) (2026-05-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **core:** harden fetchProjectId for user-owned projects, errors-only responses, and memoize project IDs ([4a7d641](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/4a7d641ab4c57f335d4f87266a4446b348904030))
7
+
1
8
  # [1.52.0](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.51.0...v1.52.0) (2026-05-22)
2
9
 
3
10
 
@@ -7,9 +7,12 @@ exports.GraphqlProjectRepository = void 0;
7
7
  const ky_1 = __importDefault(require("ky"));
8
8
  const BaseGitHubRepository_1 = require("./BaseGitHubRepository");
9
9
  const utils_1 = require("./utils");
10
+ const ONE_HOUR_MS = 60 * 60 * 1000;
10
11
  class GraphqlProjectRepository extends BaseGitHubRepository_1.BaseGitHubRepository {
11
12
  constructor() {
12
13
  super(...arguments);
14
+ this.projectIdCache = new Map();
15
+ this.fetchProjectIdFailedAt = new Map();
13
16
  this.extractProjectFromUrl = (projectUrl) => {
14
17
  const url = new URL(projectUrl);
15
18
  const path = url.pathname.split('/');
@@ -18,6 +21,15 @@ class GraphqlProjectRepository extends BaseGitHubRepository_1.BaseGitHubReposito
18
21
  return { owner, projectNumber };
19
22
  };
20
23
  this.fetchProjectId = async (login, projectNumber) => {
24
+ const cacheKey = `${login}:${projectNumber}`;
25
+ const cached = this.projectIdCache.get(cacheKey);
26
+ if (cached) {
27
+ return cached;
28
+ }
29
+ const failedAt = this.fetchProjectIdFailedAt.get(cacheKey);
30
+ if (failedAt !== undefined && Date.now() - failedAt < ONE_HOUR_MS) {
31
+ throw new Error(`fetchProjectId for ${login}/${projectNumber} is in backoff after a recent failure`);
32
+ }
21
33
  const graphqlQuery = {
22
34
  query: `query GetProjectID($login: String!, $number: Int!) {
23
35
  organization(login: $login) {
@@ -38,15 +50,23 @@ class GraphqlProjectRepository extends BaseGitHubRepository_1.BaseGitHubReposito
38
50
  number: projectNumber,
39
51
  },
40
52
  };
41
- const response = await ky_1.default
42
- .post('https://api.github.com/graphql', {
43
- json: graphqlQuery,
44
- headers: {
45
- Authorization: `Bearer ${this.ghToken}`,
46
- },
47
- })
48
- .json();
53
+ let response;
54
+ try {
55
+ response = await ky_1.default
56
+ .post('https://api.github.com/graphql', {
57
+ json: graphqlQuery,
58
+ headers: {
59
+ Authorization: `Bearer ${this.ghToken}`,
60
+ },
61
+ })
62
+ .json();
63
+ }
64
+ catch (error) {
65
+ this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
66
+ throw new Error(`fetchProjectId network error for ${login}/${projectNumber}: ${String(error)}`);
67
+ }
49
68
  if (!response.data) {
69
+ this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
50
70
  const errorMessages = response.errors
51
71
  ? response.errors.map((e) => e.message).join('; ')
52
72
  : 'no data field in response';
@@ -55,8 +75,10 @@ class GraphqlProjectRepository extends BaseGitHubRepository_1.BaseGitHubReposito
55
75
  const projectId = response.data.organization?.projectV2?.id ||
56
76
  response.data.user?.projectV2?.id;
57
77
  if (!projectId) {
58
- throw new Error('projectId is not found');
78
+ this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
79
+ throw new Error(`fetchProjectId: project not found for ${login}/${projectNumber}`);
59
80
  }
81
+ this.projectIdCache.set(cacheKey, projectId);
60
82
  return projectId;
61
83
  };
62
84
  this.findProjectIdByUrl = async (projectUrl) => {
@@ -1 +1 @@
1
- {"version":3,"file":"GraphqlProjectRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GraphqlProjectRepository.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,iEAA8D;AAG9D,mCAA6C;AAE7C,MAAa,wBACX,SAAQ,2CAAoB;IAD9B;;QAYE,0BAAqB,GAAG,CACtB,UAAkB,EAIlB,EAAE;YACF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;YAChC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACrC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YACtB,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5C,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;QAClC,CAAC,CAAC;QACF,mBAAc,GAAG,KAAK,EACpB,KAAa,EACb,aAAqB,EACJ,EAAE;YACnB,MAAM,YAAY,GAAG;gBACnB,KAAK,EAAE;;;;;;;;;;;;;EAaX;gBACI,SAAS,EAAE;oBACT,KAAK,EAAE,KAAK;oBACZ,MAAM,EAAE,aAAa;iBACtB;aACF,CAAC;YAEF,MAAM,QAAQ,GAAG,MAAM,YAAE;iBACtB,IAAI,CAAC,gCAAgC,EAAE;gBACtC,IAAI,EAAE,YAAY;gBAClB,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,EAAE;iBACxC;aACF,CAAC;iBACD,IAAI,EAgBD,CAAC;YAEP,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnB,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM;oBACnC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;oBAClD,CAAC,CAAC,2BAA2B,CAAC;gBAChC,MAAM,IAAI,KAAK,CACb,2DAA2D,aAAa,EAAE,CAC3E,CAAC;YACJ,CAAC;YACD,MAAM,SAAS,GACb,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,EAAE,EAAE;gBACzC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC;YACpC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;YAC5C,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC,CAAC;QACF,uBAAkB,GAAG,KAAK,EACxB,UAAkB,EACa,EAAE;YACjC,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC;YACxE,OAAO,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACzD,CAAC,CAAC;QACF,eAAU,GAAG,KAAK,EAAE,SAAwB,EAA2B,EAAE;YACvE,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDjB,CAAC;YACE,MAAM,SAAS,GAAG;gBAChB,SAAS,EAAE,SAAS;aACrB,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,YAAE;iBACtB,IAAI,CAAC,gCAAgC,EAAE;gBACtC,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;gBAC1B,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,EAAE;iBACxC;aACF,CAAC;iBACD,IAAI,EAqCD,CAAC;YACP,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnB,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM;oBACnC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;oBAClD,CAAC,CAAC,2BAA2B,CAAC;gBAChC,MAAM,IAAI,KAAK,CACb,uDAAuD,aAAa,EAAE,CACvE,CAAC;YACJ,CAAC;YACD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC9C,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,KAAK,gBAAgB,CAC/D,CAAC;YACF,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC9C,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,KAAK,gBAAgB,CAC/D,CAAC;YACF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CACtC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CACvD,CAAC;YACF,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;YAC/C,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CACrC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,CACtD,CAAC;YACF,MAAM,uBAAuB,GAAG,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAC7D,IAAA,0BAAkB,EAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAC/D,CAAC;YACF,MAAM,0BAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC1D,CAAC,KAAK,EAAE,EAAE,CACR,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,KAAK,4BAA4B,CAClE,CAAC;YACF,MAAM,gCAAgC,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAChE,CAAC,KAAK,EAAE,EAAE,CACR,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CACvC,kCAAkC,CACnC,CACJ,CAAC;YACF,MAAM,iCAAiC,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CACjE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,0BAAkB,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,gBAAgB,CAAC,CACvE,CAAC;YACF,MAAM,yBAAyB,GAAG,CAAC,KAAa,EAAwB,EAAE;gBACxE,QAAQ,KAAK,EAAE,CAAC;oBACd,KAAK,KAAK,CAAC;oBACX,KAAK,QAAQ,CAAC;oBACd,KAAK,OAAO,CAAC;oBACb,KAAK,MAAM,CAAC;oBACZ,KAAK,QAAQ,CAAC;oBACd,KAAK,MAAM;wBACT,OAAO,KAAK,CAAC;oBACf;wBACE,OAAO,MAAM,CAAC;gBAClB,CAAC;YACH,CAAC,CAAC;YACF,OAAO;gBACL,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,IAAI,EAAE,OAAO,CAAC,KAAK;gBACnB,MAAM,EAAE;oBACN,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,OAAO,EAAE,MAAM,CAAC,EAAE;oBAClB,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;wBACxC,EAAE,EAAE,MAAM,CAAC,EAAE;wBACb,IAAI,EAAE,MAAM,CAAC,IAAI;wBACjB,KAAK,EAAE,yBAAyB,CAAC,MAAM,CAAC,KAAK,CAAC;wBAC9C,WAAW,EAAE,MAAM,CAAC,WAAW;qBAChC,CAAC,CAAC;iBACJ;gBACD,cAAc,EAAE,cAAc;oBAC5B,CAAC,CAAC;wBACE,IAAI,EAAE,cAAc,CAAC,IAAI;wBACzB,OAAO,EAAE,cAAc,CAAC,EAAE;qBAC3B;oBACH,CAAC,CAAC,IAAI;gBACR,cAAc,EAAE,cAAc;oBAC5B,CAAC,CAAC;wBACE,IAAI,EAAE,cAAc,CAAC,IAAI;wBACzB,OAAO,EAAE,cAAc,CAAC,EAAE;qBAC3B;oBACH,CAAC,CAAC,IAAI;gBACR,KAAK,EACH,KAAK,IAAI,uBAAuB;oBAC9B,CAAC,CAAC;wBACE,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,OAAO,EAAE,KAAK,CAAC,EAAE;wBACjB,UAAU,EAAE,KAAK,CAAC,UAAU;wBAC5B,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;4BACtC,EAAE,EAAE,MAAM,CAAC,EAAE;4BACb,IAAI,EAAE,MAAM,CAAC,IAAI;4BACjB,KAAK,EAAE,yBAAyB,CAAC,MAAM,CAAC,KAAK,CAAC;4BAC9C,WAAW,EAAE,MAAM,CAAC,WAAW;yBAChC,CAAC,CAAC;wBACH,uBAAuB;qBACxB;oBACH,CAAC,CAAC,IAAI;gBACV,0BAA0B,EAAE,0BAA0B;oBACpD,CAAC,CAAC;wBACE,IAAI,EAAE,0BAA0B,CAAC,IAAI;wBACrC,OAAO,EAAE,0BAA0B,CAAC,EAAE;qBACvC;oBACH,CAAC,CAAC,IAAI;gBACR,gCAAgC,EAAE,gCAAgC;oBAChE,CAAC,CAAC;wBACE,IAAI,EAAE,gCAAgC,CAAC,IAAI;wBAC3C,OAAO,EAAE,gCAAgC,CAAC,EAAE;qBAC7C;oBACH,CAAC,CAAC,IAAI;gBACR,iCAAiC,EAAE,iCAAiC;oBAClE,CAAC,CAAC;wBACE,IAAI,EAAE,iCAAiC,CAAC,IAAI;wBAC5C,OAAO,EAAE,iCAAiC,CAAC,EAAE;qBAC9C;oBACH,CAAC,CAAC,IAAI;aACT,CAAC;QACJ,CAAC,CAAC;QACF,aAAQ,GAAG,KAAK,EAAE,GAAW,EAAoB,EAAE;YACjD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACrD,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;YACvD,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YACjD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,6BAA6B,SAAS,EAAE,CAAC,CAAC;YAC5D,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC;QACF,oBAAe,GAAG,KAAK,EACrB,OAAgB,EAChB,YAEI,EACoB,EAAE;YAC1B,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBACnB,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;YAChD,CAAC;YACD,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;EAgBnB,CAAC;YACC,MAAM,SAAS,GAAG;gBAChB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO;gBAC9B,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;oBAC/D,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC9B,IAAI;oBACJ,KAAK;oBACL,WAAW;iBACZ,CAAC,CAAC;aACJ,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,YAAE;iBACtB,IAAI,CAAC,gCAAgC,EAAE;gBACtC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE;gBACpC,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,EAAE;iBACxC;aACF,CAAC;iBACD,IAAI,EAQD,CAAC;YACP,OAAO,QAAQ,CAAC,IAAI,CAAC,oBAAoB,CAAC,cAAc,CAAC,OAAO,CAAC;QACnE,CAAC,CAAC;QACF,qBAAgB,GAAG,KAAK,EACtB,OAAgB,EAChB,aAEI,EACoB,EAAE;YAC1B,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;EAgBnB,CAAC;YACC,MAAM,SAAS,GAAG;gBAChB,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;gBAC/B,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;oBAChE,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC9B,IAAI;oBACJ,KAAK;oBACL,WAAW;iBACZ,CAAC,CAAC;aACJ,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,YAAE;iBACtB,IAAI,CAAC,gCAAgC,EAAE;gBACtC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE;gBACpC,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,EAAE;iBACxC;aACF,CAAC;iBACD,IAAI,EAQD,CAAC;YACP,OAAO,QAAQ,CAAC,IAAI,CAAC,oBAAoB,CAAC,cAAc,CAAC,OAAO,CAAC;QACnE,CAAC,CAAC;IACJ,CAAC;CAAA;AA9aD,4DA8aC"}
1
+ {"version":3,"file":"GraphqlProjectRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GraphqlProjectRepository.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.52.0",
3
+ "version": "1.52.1",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -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 { GraphqlProjectRepository } from './GraphqlProjectRepository';
18
+ import { LocalStorageRepository } from './LocalStorageRepository';
19
+
20
+ const mockJsonResponse = <T>(data: T) => ({
21
+ json: jest.fn().mockResolvedValue(data),
22
+ });
23
+
24
+ describe('GraphqlProjectRepository.fetchProjectId', () => {
25
+ const localStorageRepository = new LocalStorageRepository();
26
+ let repository: GraphqlProjectRepository;
27
+
28
+ beforeEach(() => {
29
+ jest.useFakeTimers();
30
+ mockPost.mockReset();
31
+ repository = new GraphqlProjectRepository(localStorageRepository, '');
32
+ });
33
+
34
+ afterEach(() => {
35
+ jest.useRealTimers();
36
+ });
37
+
38
+ describe('user-owned project', () => {
39
+ it('should resolve project ID from user owner when organization returns null', async () => {
40
+ mockPost.mockReturnValueOnce(
41
+ mockJsonResponse({
42
+ data: {
43
+ organization: null,
44
+ user: {
45
+ projectV2: {
46
+ id: 'PVT_user123',
47
+ databaseId: 999,
48
+ },
49
+ },
50
+ },
51
+ }),
52
+ );
53
+
54
+ const result = await repository.fetchProjectId('some-user', 1);
55
+
56
+ expect(result).toBe('PVT_user123');
57
+ });
58
+
59
+ it('should resolve project ID from organization owner when user returns null', async () => {
60
+ mockPost.mockReturnValueOnce(
61
+ mockJsonResponse({
62
+ data: {
63
+ organization: {
64
+ projectV2: {
65
+ id: 'PVT_org456',
66
+ databaseId: 111,
67
+ },
68
+ },
69
+ user: null,
70
+ },
71
+ }),
72
+ );
73
+
74
+ const result = await repository.fetchProjectId('some-org', 2);
75
+
76
+ expect(result).toBe('PVT_org456');
77
+ });
78
+ });
79
+
80
+ describe('memoization', () => {
81
+ it('should return cached project ID on subsequent calls without re-fetching', async () => {
82
+ mockPost.mockReturnValueOnce(
83
+ mockJsonResponse({
84
+ data: {
85
+ organization: null,
86
+ user: {
87
+ projectV2: {
88
+ id: 'PVT_cached',
89
+ databaseId: 777,
90
+ },
91
+ },
92
+ },
93
+ }),
94
+ );
95
+
96
+ const first = await repository.fetchProjectId('owner', 10);
97
+ const second = await repository.fetchProjectId('owner', 10);
98
+
99
+ expect(first).toBe('PVT_cached');
100
+ expect(second).toBe('PVT_cached');
101
+ expect(mockPost).toHaveBeenCalledTimes(1);
102
+ });
103
+
104
+ it('should use separate cache entries for different owner+projectNumber combinations', async () => {
105
+ mockPost
106
+ .mockReturnValueOnce(
107
+ mockJsonResponse({
108
+ data: {
109
+ organization: null,
110
+ user: { projectV2: { id: 'PVT_A', databaseId: 1 } },
111
+ },
112
+ }),
113
+ )
114
+ .mockReturnValueOnce(
115
+ mockJsonResponse({
116
+ data: {
117
+ organization: null,
118
+ user: { projectV2: { id: 'PVT_B', databaseId: 2 } },
119
+ },
120
+ }),
121
+ );
122
+
123
+ const resultA = await repository.fetchProjectId('ownerA', 1);
124
+ const resultB = await repository.fetchProjectId('ownerB', 1);
125
+
126
+ expect(resultA).toBe('PVT_A');
127
+ expect(resultB).toBe('PVT_B');
128
+ expect(mockPost).toHaveBeenCalledTimes(2);
129
+ });
130
+ });
131
+
132
+ describe('errors-only response hardening', () => {
133
+ it('should throw a clear error when response has no data field', async () => {
134
+ mockPost.mockReturnValueOnce(
135
+ mockJsonResponse({
136
+ errors: [{ message: 'Could not resolve to a User' }],
137
+ }),
138
+ );
139
+
140
+ await expect(repository.fetchProjectId('bad-owner', 1)).rejects.toThrow(
141
+ 'Could not resolve to a User',
142
+ );
143
+ });
144
+
145
+ it('should throw a clear error when data is null', async () => {
146
+ mockPost.mockReturnValueOnce(
147
+ mockJsonResponse({
148
+ data: null,
149
+ errors: [{ message: 'secondary rate limit' }],
150
+ }),
151
+ );
152
+
153
+ await expect(
154
+ repository.fetchProjectId('rate-limited', 1),
155
+ ).rejects.toThrow('secondary rate limit');
156
+ });
157
+
158
+ it('should throw a clear error when data has no project in either org or user', async () => {
159
+ mockPost.mockReturnValueOnce(
160
+ mockJsonResponse({
161
+ data: {
162
+ organization: null,
163
+ user: null,
164
+ },
165
+ }),
166
+ );
167
+
168
+ await expect(
169
+ repository.fetchProjectId('no-project-owner', 99),
170
+ ).rejects.toThrow('project not found');
171
+ });
172
+
173
+ it('should throw a clear error when network call fails', async () => {
174
+ mockPost.mockReturnValueOnce({
175
+ json: jest.fn().mockRejectedValue(new Error('network failure')),
176
+ });
177
+
178
+ await expect(repository.fetchProjectId('owner', 1)).rejects.toThrow(
179
+ 'network failure',
180
+ );
181
+ });
182
+ });
183
+
184
+ describe('backoff after failure', () => {
185
+ it('should not re-call GraphQL within 1 hour after a failure', async () => {
186
+ mockPost.mockReturnValueOnce(
187
+ mockJsonResponse({
188
+ errors: [{ message: 'auth failure' }],
189
+ }),
190
+ );
191
+
192
+ await expect(repository.fetchProjectId('owner', 5)).rejects.toThrow();
193
+
194
+ await expect(repository.fetchProjectId('owner', 5)).rejects.toThrow(
195
+ 'backoff',
196
+ );
197
+
198
+ expect(mockPost).toHaveBeenCalledTimes(1);
199
+ });
200
+
201
+ it('should retry after 1 hour backoff has elapsed', async () => {
202
+ mockPost
203
+ .mockReturnValueOnce(
204
+ mockJsonResponse({
205
+ errors: [{ message: 'temporary error' }],
206
+ }),
207
+ )
208
+ .mockReturnValueOnce(
209
+ mockJsonResponse({
210
+ data: {
211
+ organization: null,
212
+ user: { projectV2: { id: 'PVT_recovered', databaseId: 42 } },
213
+ },
214
+ }),
215
+ );
216
+
217
+ await expect(repository.fetchProjectId('owner', 7)).rejects.toThrow();
218
+
219
+ jest.advanceTimersByTime(60 * 60 * 1000 + 1);
220
+
221
+ const result = await repository.fetchProjectId('owner', 7);
222
+
223
+ expect(result).toBe('PVT_recovered');
224
+ expect(mockPost).toHaveBeenCalledTimes(2);
225
+ });
226
+
227
+ it('should apply backoff independently per owner+projectNumber', async () => {
228
+ mockPost
229
+ .mockReturnValueOnce(
230
+ mockJsonResponse({
231
+ errors: [{ message: 'error for owner A' }],
232
+ }),
233
+ )
234
+ .mockReturnValueOnce(
235
+ mockJsonResponse({
236
+ data: {
237
+ organization: null,
238
+ user: { projectV2: { id: 'PVT_B', databaseId: 2 } },
239
+ },
240
+ }),
241
+ );
242
+
243
+ await expect(repository.fetchProjectId('ownerA', 1)).rejects.toThrow();
244
+
245
+ const resultB = await repository.fetchProjectId('ownerB', 1);
246
+
247
+ expect(resultB).toBe('PVT_B');
248
+ expect(mockPost).toHaveBeenCalledTimes(2);
249
+ });
250
+ });
251
+ });
@@ -4,6 +4,8 @@ import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/Proj
4
4
  import { FieldOption, Project } from '../../domain/entities/Project';
5
5
  import { normalizeFieldName } from './utils';
6
6
 
7
+ const ONE_HOUR_MS = 60 * 60 * 1000;
8
+
7
9
  export class GraphqlProjectRepository
8
10
  extends BaseGitHubRepository
9
11
  implements
@@ -16,6 +18,9 @@ export class GraphqlProjectRepository
16
18
  | 'updateStatusList'
17
19
  >
18
20
  {
21
+ private readonly projectIdCache = new Map<string, string>();
22
+ private readonly fetchProjectIdFailedAt = new Map<string, number>();
23
+
19
24
  extractProjectFromUrl = (
20
25
  projectUrl: string,
21
26
  ): {
@@ -32,6 +37,17 @@ export class GraphqlProjectRepository
32
37
  login: string,
33
38
  projectNumber: number,
34
39
  ): Promise<string> => {
40
+ const cacheKey = `${login}:${projectNumber}`;
41
+ const cached = this.projectIdCache.get(cacheKey);
42
+ if (cached) {
43
+ return cached;
44
+ }
45
+ const failedAt = this.fetchProjectIdFailedAt.get(cacheKey);
46
+ if (failedAt !== undefined && Date.now() - failedAt < ONE_HOUR_MS) {
47
+ throw new Error(
48
+ `fetchProjectId for ${login}/${projectNumber} is in backoff after a recent failure`,
49
+ );
50
+ }
35
51
  const graphqlQuery = {
36
52
  query: `query GetProjectID($login: String!, $number: Int!) {
37
53
  organization(login: $login) {
@@ -53,32 +69,41 @@ export class GraphqlProjectRepository
53
69
  },
54
70
  };
55
71
 
56
- const response = await ky
57
- .post('https://api.github.com/graphql', {
58
- json: graphqlQuery,
59
- headers: {
60
- Authorization: `Bearer ${this.ghToken}`,
61
- },
62
- })
63
- .json<{
64
- data?: {
65
- organization: {
66
- projectV2: {
67
- id: string;
68
- databaseId: number;
69
- };
70
- };
71
- user: {
72
- projectV2: {
73
- id: string;
74
- databaseId: number;
75
- };
76
- };
77
- };
78
- errors?: { message: string }[];
79
- }>();
72
+ let response: {
73
+ data?: {
74
+ organization?: {
75
+ projectV2?: {
76
+ id: string;
77
+ databaseId: number;
78
+ } | null;
79
+ } | null;
80
+ user?: {
81
+ projectV2?: {
82
+ id: string;
83
+ databaseId: number;
84
+ } | null;
85
+ } | null;
86
+ } | null;
87
+ errors?: { message: string }[];
88
+ };
89
+ try {
90
+ response = await ky
91
+ .post('https://api.github.com/graphql', {
92
+ json: graphqlQuery,
93
+ headers: {
94
+ Authorization: `Bearer ${this.ghToken}`,
95
+ },
96
+ })
97
+ .json();
98
+ } catch (error) {
99
+ this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
100
+ throw new Error(
101
+ `fetchProjectId network error for ${login}/${projectNumber}: ${String(error)}`,
102
+ );
103
+ }
80
104
 
81
105
  if (!response.data) {
106
+ this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
82
107
  const errorMessages = response.errors
83
108
  ? response.errors.map((e) => e.message).join('; ')
84
109
  : 'no data field in response';
@@ -90,8 +115,12 @@ export class GraphqlProjectRepository
90
115
  response.data.organization?.projectV2?.id ||
91
116
  response.data.user?.projectV2?.id;
92
117
  if (!projectId) {
93
- throw new Error('projectId is not found');
118
+ this.fetchProjectIdFailedAt.set(cacheKey, Date.now());
119
+ throw new Error(
120
+ `fetchProjectId: project not found for ${login}/${projectNumber}`,
121
+ );
94
122
  }
123
+ this.projectIdCache.set(cacheKey, projectId);
95
124
  return projectId;
96
125
  };
97
126
  findProjectIdByUrl = async (
@@ -2,6 +2,8 @@ import { BaseGitHubRepository } from './BaseGitHubRepository';
2
2
  import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/ProjectRepository';
3
3
  import { FieldOption, Project } from '../../domain/entities/Project';
4
4
  export declare class GraphqlProjectRepository extends BaseGitHubRepository implements Pick<ProjectRepository, 'getProject' | 'findProjectIdByUrl' | 'getByUrl' | 'updateStoryList' | 'updateStatusList'> {
5
+ private readonly projectIdCache;
6
+ private readonly fetchProjectIdFailedAt;
5
7
  extractProjectFromUrl: (projectUrl: string) => {
6
8
  owner: string;
7
9
  projectNumber: number;
@@ -1 +1 @@
1
- {"version":3,"file":"GraphqlProjectRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GraphqlProjectRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAGrE,qBAAa,wBACX,SAAQ,oBACR,YACE,IAAI,CACF,iBAAiB,EACf,YAAY,GACZ,oBAAoB,GACpB,UAAU,GACV,iBAAiB,GACjB,kBAAkB,CACrB;IAEH,qBAAqB,GACnB,YAAY,MAAM,KACjB;QACD,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;KACvB,CAMC;IACF,cAAc,GACZ,OAAO,MAAM,EACb,eAAe,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,CA8DhB;IACF,kBAAkB,GAChB,YAAY,MAAM,KACjB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAG9B;IACF,UAAU,GAAU,WAAW,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA2NpE;IACF,QAAQ,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC,CAU9C;IACF,eAAe,GACb,SAAS,OAAO,EAChB,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QACvC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC,EAAE,KACH,OAAO,CAAC,WAAW,EAAE,CAAC,CA+CvB;IACF,gBAAgB,GACd,SAAS,OAAO,EAChB,eAAe,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QACxC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC,EAAE,KACH,OAAO,CAAC,WAAW,EAAE,CAAC,CA4CvB;CACH"}
1
+ {"version":3,"file":"GraphqlProjectRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GraphqlProjectRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAKrE,qBAAa,wBACX,SAAQ,oBACR,YACE,IAAI,CACF,iBAAiB,EACf,YAAY,GACZ,oBAAoB,GACpB,UAAU,GACV,iBAAiB,GACjB,kBAAkB,CACrB;IAEH,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA6B;IAEpE,qBAAqB,GACnB,YAAY,MAAM,KACjB;QACD,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,MAAM,CAAC;KACvB,CAMC;IACF,cAAc,GACZ,OAAO,MAAM,EACb,eAAe,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,CAsFhB;IACF,kBAAkB,GAChB,YAAY,MAAM,KACjB,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAG9B;IACF,UAAU,GAAU,WAAW,OAAO,CAAC,IAAI,CAAC,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA2NpE;IACF,QAAQ,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC,CAU9C;IACF,eAAe,GACb,SAAS,OAAO,EAChB,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QACvC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC,EAAE,KACH,OAAO,CAAC,WAAW,EAAE,CAAC,CA+CvB;IACF,gBAAgB,GACd,SAAS,OAAO,EAChB,eAAe,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;QACxC,EAAE,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;KAC9B,CAAC,EAAE,KACH,OAAO,CAAC,WAAW,EAAE,CAAC,CA4CvB;CACH"}