github-issue-tower-defence-management 1.1.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 (221) hide show
  1. package/.env.example +6 -0
  2. package/.eslintrc.cjs +55 -0
  3. package/.github/CODEOWNERS +2 -0
  4. package/.github/workflows/assign-all-cards-to-owner.yml +14 -0
  5. package/.github/workflows/commit-lint.yml +54 -0
  6. package/.github/workflows/configs/commitlint.config.js +27 -0
  7. package/.github/workflows/create-pr.yml +64 -0
  8. package/.github/workflows/format.yml +25 -0
  9. package/.github/workflows/publish.yml +47 -0
  10. package/.github/workflows/test.yml +45 -0
  11. package/.github/workflows/umino-project.yml +181 -0
  12. package/.prettierignore +22 -0
  13. package/.prettierrc +5 -0
  14. package/CHANGELOG.md +49 -0
  15. package/CONTRIBUTING.md +107 -0
  16. package/README.md +108 -0
  17. package/bin/adapter/entry-points/cli/index.js +26 -0
  18. package/bin/adapter/entry-points/cli/index.js.map +1 -0
  19. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +142 -0
  20. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -0
  21. package/bin/adapter/repositories/AxiosSlackRepository.js +124 -0
  22. package/bin/adapter/repositories/AxiosSlackRepository.js.map +1 -0
  23. package/bin/adapter/repositories/BaseGitHubRepository.js +136 -0
  24. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -0
  25. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +123 -0
  26. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -0
  27. package/bin/adapter/repositories/GraphqlProjectRepository.js +167 -0
  28. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -0
  29. package/bin/adapter/repositories/LocalStorageCacheRepository.js +46 -0
  30. package/bin/adapter/repositories/LocalStorageCacheRepository.js.map +1 -0
  31. package/bin/adapter/repositories/LocalStorageRepository.js +30 -0
  32. package/bin/adapter/repositories/LocalStorageRepository.js.map +1 -0
  33. package/bin/adapter/repositories/SystemDateRepository.js +23 -0
  34. package/bin/adapter/repositories/SystemDateRepository.js.map +1 -0
  35. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +148 -0
  36. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -0
  37. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js +48 -0
  38. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js.map +1 -0
  39. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +120 -0
  40. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +1 -0
  41. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +485 -0
  42. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -0
  43. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +114 -0
  44. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +1 -0
  45. package/bin/adapter/repositories/issue/RestIssueRepository.js +79 -0
  46. package/bin/adapter/repositories/issue/RestIssueRepository.js.map +1 -0
  47. package/bin/adapter/repositories/issue/issueTimelineUtils.js +38 -0
  48. package/bin/adapter/repositories/issue/issueTimelineUtils.js.map +1 -0
  49. package/bin/adapter/repositories/utils.js +45 -0
  50. package/bin/adapter/repositories/utils.js.map +1 -0
  51. package/bin/domain/entities/Issue.js +3 -0
  52. package/bin/domain/entities/Issue.js.map +1 -0
  53. package/bin/domain/entities/Member.js +3 -0
  54. package/bin/domain/entities/Member.js.map +1 -0
  55. package/bin/domain/entities/Project.js +3 -0
  56. package/bin/domain/entities/Project.js.map +1 -0
  57. package/bin/domain/entities/ProjectField.js +3 -0
  58. package/bin/domain/entities/ProjectField.js.map +1 -0
  59. package/bin/domain/entities/ProjectFieldSingleSelect.js +3 -0
  60. package/bin/domain/entities/ProjectFieldSingleSelect.js.map +1 -0
  61. package/bin/domain/entities/ProjectFieldSingleSelectOption.js +3 -0
  62. package/bin/domain/entities/ProjectFieldSingleSelectOption.js.map +1 -0
  63. package/bin/domain/entities/WorkingTime.js +3 -0
  64. package/bin/domain/entities/WorkingTime.js.map +1 -0
  65. package/bin/domain/usecases/ActionAnnouncementUseCase.js +46 -0
  66. package/bin/domain/usecases/ActionAnnouncementUseCase.js.map +1 -0
  67. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js +116 -0
  68. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js.map +1 -0
  69. package/bin/domain/usecases/ClearNextActionHourUseCase.js +38 -0
  70. package/bin/domain/usecases/ClearNextActionHourUseCase.js.map +1 -0
  71. package/bin/domain/usecases/GenerateWorkingTimeReportUseCase.js +180 -0
  72. package/bin/domain/usecases/GenerateWorkingTimeReportUseCase.js.map +1 -0
  73. package/bin/domain/usecases/HandleScheduledEventUseCase.js +122 -0
  74. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -0
  75. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +35 -0
  76. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -0
  77. package/bin/domain/usecases/adapter-interfaces/DateRepository.js +3 -0
  78. package/bin/domain/usecases/adapter-interfaces/DateRepository.js.map +1 -0
  79. package/bin/domain/usecases/adapter-interfaces/IssueRepository.js +3 -0
  80. package/bin/domain/usecases/adapter-interfaces/IssueRepository.js.map +1 -0
  81. package/bin/domain/usecases/adapter-interfaces/ProjectRepository.js +3 -0
  82. package/bin/domain/usecases/adapter-interfaces/ProjectRepository.js.map +1 -0
  83. package/bin/domain/usecases/adapter-interfaces/SlackRepository.js +3 -0
  84. package/bin/domain/usecases/adapter-interfaces/SlackRepository.js.map +1 -0
  85. package/bin/domain/usecases/adapter-interfaces/SpreadsheetRepository.js +3 -0
  86. package/bin/domain/usecases/adapter-interfaces/SpreadsheetRepository.js.map +1 -0
  87. package/bin/index.js +13 -0
  88. package/bin/index.js.map +1 -0
  89. package/commitlint.config.js +6 -0
  90. package/jest.config.js +19 -0
  91. package/package.json +80 -0
  92. package/renovate.json +37 -0
  93. package/src/adapter/entry-points/cli/index.test.ts +20 -0
  94. package/src/adapter/entry-points/cli/index.ts +36 -0
  95. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +95 -0
  96. package/src/adapter/repositories/AxiosSlackRepository.test.ts +119 -0
  97. package/src/adapter/repositories/AxiosSlackRepository.ts +184 -0
  98. package/src/adapter/repositories/BaseGitHubRepository.test.ts +95 -0
  99. package/src/adapter/repositories/BaseGitHubRepository.ts +172 -0
  100. package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +124 -0
  101. package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +151 -0
  102. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +46 -0
  103. package/src/adapter/repositories/GraphqlProjectRepository.ts +236 -0
  104. package/src/adapter/repositories/LocalStorageCacheRepository.test.ts +146 -0
  105. package/src/adapter/repositories/LocalStorageCacheRepository.ts +53 -0
  106. package/src/adapter/repositories/LocalStorageRepository.integration.test.ts +142 -0
  107. package/src/adapter/repositories/LocalStorageRepository.test.ts +161 -0
  108. package/src/adapter/repositories/LocalStorageRepository.ts +21 -0
  109. package/src/adapter/repositories/SystemDateRepository.ts +20 -0
  110. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +158 -0
  111. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -0
  112. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +26 -0
  113. package/src/adapter/repositories/issue/ApiV3IssueRepository.ts +59 -0
  114. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +6610 -0
  115. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +127 -0
  116. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +49 -0
  117. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +745 -0
  118. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +71 -0
  119. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +263 -0
  120. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +29 -0
  121. package/src/adapter/repositories/issue/RestIssueRepository.ts +105 -0
  122. package/src/adapter/repositories/issue/issueTimelineUtils.test.ts +79 -0
  123. package/src/adapter/repositories/issue/issueTimelineUtils.ts +52 -0
  124. package/src/adapter/repositories/utils.test.ts +40 -0
  125. package/src/adapter/repositories/utils.ts +50 -0
  126. package/src/domain/entities/Issue.ts +23 -0
  127. package/src/domain/entities/Member.ts +3 -0
  128. package/src/domain/entities/Project.ts +29 -0
  129. package/src/domain/entities/ProjectField.ts +3 -0
  130. package/src/domain/entities/ProjectFieldSingleSelect.ts +8 -0
  131. package/src/domain/entities/ProjectFieldSingleSelectOption.ts +8 -0
  132. package/src/domain/entities/WorkingTime.ts +8 -0
  133. package/src/domain/usecases/ActionAnnouncementUseCase.ts +76 -0
  134. package/src/domain/usecases/AnalyzeProblemByIssueUseCase.ts +209 -0
  135. package/src/domain/usecases/ClearNextActionHourUseCase.ts +51 -0
  136. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.test.ts +382 -0
  137. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.ts +284 -0
  138. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +58 -0
  139. package/src/domain/usecases/HandleScheduledEventUseCase.ts +209 -0
  140. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +46 -0
  141. package/src/domain/usecases/adapter-interfaces/DateRepository.ts +5 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +44 -0
  143. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
  144. package/src/domain/usecases/adapter-interfaces/SlackRepository.ts +20 -0
  145. package/src/domain/usecases/adapter-interfaces/SpreadsheetRepository.ts +18 -0
  146. package/src/index.test.ts +8 -0
  147. package/src/index.ts +7 -0
  148. package/tsconfig.build.json +11 -0
  149. package/tsconfig.json +22 -0
  150. package/types/adapter/entry-points/cli/index.d.ts +3 -0
  151. package/types/adapter/entry-points/cli/index.d.ts.map +1 -0
  152. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts +11 -0
  153. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -0
  154. package/types/adapter/repositories/AxiosSlackRepository.d.ts +13 -0
  155. package/types/adapter/repositories/AxiosSlackRepository.d.ts.map +1 -0
  156. package/types/adapter/repositories/BaseGitHubRepository.d.ts +32 -0
  157. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -0
  158. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +13 -0
  159. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -0
  160. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +13 -0
  161. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -0
  162. package/types/adapter/repositories/LocalStorageCacheRepository.d.ts +12 -0
  163. package/types/adapter/repositories/LocalStorageCacheRepository.d.ts.map +1 -0
  164. package/types/adapter/repositories/LocalStorageRepository.d.ts +7 -0
  165. package/types/adapter/repositories/LocalStorageRepository.d.ts.map +1 -0
  166. package/types/adapter/repositories/SystemDateRepository.d.ts +7 -0
  167. package/types/adapter/repositories/SystemDateRepository.d.ts.map +1 -0
  168. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +38 -0
  169. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -0
  170. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts +17 -0
  171. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts.map +1 -0
  172. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +31 -0
  173. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +1 -0
  174. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +39 -0
  175. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -0
  176. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +83 -0
  177. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +1 -0
  178. package/types/adapter/repositories/issue/RestIssueRepository.d.ts +16 -0
  179. package/types/adapter/repositories/issue/RestIssueRepository.d.ts.map +1 -0
  180. package/types/adapter/repositories/issue/issueTimelineUtils.d.ts +12 -0
  181. package/types/adapter/repositories/issue/issueTimelineUtils.d.ts.map +1 -0
  182. package/types/adapter/repositories/utils.d.ts +4 -0
  183. package/types/adapter/repositories/utils.d.ts.map +1 -0
  184. package/types/domain/entities/Issue.d.ts +24 -0
  185. package/types/domain/entities/Issue.d.ts.map +1 -0
  186. package/types/domain/entities/Member.d.ts +4 -0
  187. package/types/domain/entities/Member.d.ts.map +1 -0
  188. package/types/domain/entities/Project.d.ts +29 -0
  189. package/types/domain/entities/Project.d.ts.map +1 -0
  190. package/types/domain/entities/ProjectField.d.ts +3 -0
  191. package/types/domain/entities/ProjectField.d.ts.map +1 -0
  192. package/types/domain/entities/ProjectFieldSingleSelect.d.ts +8 -0
  193. package/types/domain/entities/ProjectFieldSingleSelect.d.ts.map +1 -0
  194. package/types/domain/entities/ProjectFieldSingleSelectOption.d.ts +8 -0
  195. package/types/domain/entities/ProjectFieldSingleSelectOption.d.ts.map +1 -0
  196. package/types/domain/entities/WorkingTime.d.ts +8 -0
  197. package/types/domain/entities/WorkingTime.d.ts.map +1 -0
  198. package/types/domain/usecases/ActionAnnouncementUseCase.d.ts +17 -0
  199. package/types/domain/usecases/ActionAnnouncementUseCase.d.ts.map +1 -0
  200. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts +26 -0
  201. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts.map +1 -0
  202. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts +14 -0
  203. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts.map +1 -0
  204. package/types/domain/usecases/GenerateWorkingTimeReportUseCase.d.ts +50 -0
  205. package/types/domain/usecases/GenerateWorkingTimeReportUseCase.d.ts.map +1 -0
  206. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +63 -0
  207. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -0
  208. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +14 -0
  209. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -0
  210. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts +6 -0
  211. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts.map +1 -0
  212. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +23 -0
  213. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -0
  214. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +6 -0
  215. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -0
  216. package/types/domain/usecases/adapter-interfaces/SlackRepository.d.ts +9 -0
  217. package/types/domain/usecases/adapter-interfaces/SlackRepository.d.ts.map +1 -0
  218. package/types/domain/usecases/adapter-interfaces/SpreadsheetRepository.d.ts +6 -0
  219. package/types/domain/usecases/adapter-interfaces/SpreadsheetRepository.d.ts.map +1 -0
  220. package/types/index.d.ts +10 -0
  221. package/types/index.d.ts.map +1 -0
@@ -0,0 +1,124 @@
1
+ import { GoogleSpreadsheetRepository } from './GoogleSpreadsheetRepository';
2
+ import { describe, test, expect } from '@jest/globals';
3
+ import { LocalStorageRepository } from './LocalStorageRepository';
4
+
5
+ describe('GoogleSpreadsheetRepository', () => {
6
+ const localStorageRepository = new LocalStorageRepository();
7
+ const repository = new GoogleSpreadsheetRepository(localStorageRepository);
8
+ const spreadsheetUrl =
9
+ 'https://docs.google.com/spreadsheets/d/1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM/edit?gid=0#gid=0';
10
+
11
+ describe('getSpreadsheetId', () => {
12
+ const testCases: [string, string][] = [
13
+ [
14
+ 'https://docs.google.com/spreadsheets/d/1234567890abcdef/edit#gid=0',
15
+ '1234567890abcdef',
16
+ ],
17
+ [
18
+ 'https://docs.google.com/spreadsheets/d/abcdef1234567890/edit',
19
+ 'abcdef1234567890',
20
+ ],
21
+ ];
22
+
23
+ test.each(testCases)(
24
+ 'given %s returns %s',
25
+ (input: string, expected: string) => {
26
+ expect(repository.getSpreadsheetId(input)).toBe(expected);
27
+ },
28
+ );
29
+ });
30
+
31
+ describe('getSheet', () => {
32
+ const testCases: [string, string, string[][] | null][] = [
33
+ ['SheetUndefined', 'Undefined Sheet', null],
34
+ // ['SheetEmpty', 'Empty Sheet', []],
35
+ ['SheetSingleCell', 'Single Cell', [['test']]],
36
+ [
37
+ 'SheetMultipleRows',
38
+ 'Multiple Rows',
39
+ [
40
+ ['1', '2'],
41
+ ['3', '4'],
42
+ ],
43
+ ],
44
+ ];
45
+
46
+ test.each(testCases)(
47
+ 'gets sheet %s with %s',
48
+ async (sheetName: string, _: string, expected: string[][] | null) => {
49
+ const result = await repository.getSheet(spreadsheetUrl, sheetName);
50
+ expect(result).toEqual(expected);
51
+ },
52
+ );
53
+
54
+ test('returns null for non-existent sheet', async () => {
55
+ const result = await repository.getSheet(
56
+ spreadsheetUrl,
57
+ 'NonExistentSheet',
58
+ );
59
+ expect(result).toBeNull();
60
+ });
61
+ });
62
+
63
+ describe('updateCell', () => {
64
+ const testCases: [string, number, number, string][] = [
65
+ ['Sheet1', 0, 0, 'First Value'],
66
+ ['Sheet1', 0, 0, 'Updated Value'],
67
+ ['Sheet1', 1, 1, '123'],
68
+ ['Sheet1', 2, 2, 'Test'],
69
+ ];
70
+
71
+ test.each(testCases)(
72
+ 'updates cell in sheet %s at row %d col %d with value %s',
73
+ async (sheetName: string, row: number, col: number, value: string) => {
74
+ await repository.updateCell(spreadsheetUrl, sheetName, row, col, value);
75
+ const result = await repository.getSheet(spreadsheetUrl, sheetName);
76
+ if (!result) {
77
+ throw new Error('Sheet not found');
78
+ }
79
+ expect(result[row][col]).toBe(value);
80
+ },
81
+ );
82
+ });
83
+
84
+ describe('appendSheetValues', () => {
85
+ const testCases: [string[][]][] = [
86
+ [[['Single Row']]],
87
+ [[['Multiple', 'Columns']]],
88
+ [
89
+ [
90
+ ['Row1Col1', 'Row1Col2'],
91
+ ['Row2Col1', 'Row2Col2'],
92
+ ],
93
+ ],
94
+ ];
95
+
96
+ test.each(testCases)(
97
+ 'appends values %j to sheet',
98
+ async (values: string[][]) => {
99
+ const sheetName = 'AppendTest';
100
+ const initialSheet = await repository.getSheet(
101
+ spreadsheetUrl,
102
+ sheetName,
103
+ );
104
+ const initialLength = initialSheet ? initialSheet.length : 0;
105
+
106
+ await repository.appendSheetValues(spreadsheetUrl, sheetName, values);
107
+
108
+ const updatedSheet = await repository.getSheet(
109
+ spreadsheetUrl,
110
+ sheetName,
111
+ );
112
+ expect(updatedSheet).not.toBeNull();
113
+ if (!updatedSheet) {
114
+ throw new Error('Sheet not found');
115
+ }
116
+ expect(updatedSheet.length).toBe(initialLength + values.length);
117
+
118
+ for (let i = 0; i < values.length; i++) {
119
+ expect(updatedSheet[initialLength + i]).toEqual(values[i]);
120
+ }
121
+ },
122
+ );
123
+ });
124
+ });
@@ -0,0 +1,151 @@
1
+ import { SpreadsheetRepository } from '../../domain/usecases/adapter-interfaces/SpreadsheetRepository';
2
+ import { google } from 'googleapis';
3
+ import { LocalStorageRepository } from './LocalStorageRepository';
4
+ import dotenv from 'dotenv';
5
+ dotenv.config();
6
+
7
+ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
8
+ keyFile = './tmp/service-account-key.json';
9
+
10
+ constructor(
11
+ readonly localStorageRepository: LocalStorageRepository,
12
+ serviceAccountKey: string = process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
13
+ 'dummy',
14
+ ) {
15
+ this.localStorageRepository.write(this.keyFile, serviceAccountKey);
16
+ }
17
+
18
+ getSpreadsheetId = (spreadsheetUrl: string): string => {
19
+ const url = new URL(spreadsheetUrl);
20
+ return url.pathname.split('/')[3];
21
+ };
22
+ getSheet = async (
23
+ spreadsheetUrl: string,
24
+ sheetName: string,
25
+ ): Promise<string[][] | null> => {
26
+ const auth = new google.auth.GoogleAuth({
27
+ keyFile: this.keyFile,
28
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
29
+ });
30
+ const sheets = google.sheets({ version: 'v4', auth });
31
+ const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
32
+ const responseSheet = await sheets.spreadsheets.get({
33
+ spreadsheetId,
34
+ });
35
+ if (responseSheet.status !== 200) {
36
+ throw new Error(
37
+ `Failed to get sheet: ${responseSheet.status}. ${JSON.stringify(responseSheet.data)}`,
38
+ );
39
+ }
40
+ const sheet = responseSheet.data.sheets?.find(
41
+ (s) => s.properties?.title === sheetName,
42
+ );
43
+ if (!sheet) {
44
+ return null;
45
+ }
46
+ const response = await sheets.spreadsheets.values.get({
47
+ spreadsheetId,
48
+ range: sheetName,
49
+ });
50
+ if (response.status !== 200) {
51
+ throw new Error(
52
+ `Failed to get sheet: ${response.status}. ${JSON.stringify(response.data)}`,
53
+ );
54
+ }
55
+ if (!response.data.values) {
56
+ return null;
57
+ }
58
+ return response.data.values.map((row) => row.map((cell) => String(cell)));
59
+ };
60
+ updateCell = async (
61
+ spreadsheetUrl: string,
62
+ sheetName: string,
63
+ row: number,
64
+ column: number,
65
+ value: string,
66
+ ): Promise<void> => {
67
+ const auth = new google.auth.GoogleAuth({
68
+ keyFile: this.keyFile,
69
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
70
+ });
71
+ const sheets = google.sheets({ version: 'v4', auth });
72
+ const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
73
+ await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
74
+ const response = await sheets.spreadsheets.values.update({
75
+ spreadsheetId,
76
+ range: `${sheetName}!${String.fromCharCode(65 + column)}${row + 1}`,
77
+ valueInputOption: 'RAW',
78
+ requestBody: {
79
+ values: [[value]],
80
+ },
81
+ });
82
+ if (response.status !== 200) {
83
+ throw new Error(
84
+ `Failed to update cell: ${response.status}. ${JSON.stringify(response.data)}`,
85
+ );
86
+ }
87
+ };
88
+ createNewSheetIfNotExists = async (
89
+ spreadsheetUrl: string,
90
+ sheetName: string,
91
+ ): Promise<void> => {
92
+ const auth = new google.auth.GoogleAuth({
93
+ keyFile: this.keyFile,
94
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
95
+ });
96
+ const sheets = google.sheets({ version: 'v4', auth });
97
+ const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
98
+ const sheet = await this.getSheet(spreadsheetUrl, sheetName);
99
+ if (sheet !== null) {
100
+ return;
101
+ }
102
+ const response = await sheets.spreadsheets.batchUpdate({
103
+ spreadsheetId,
104
+ requestBody: {
105
+ requests: [
106
+ {
107
+ addSheet: {
108
+ properties: {
109
+ title: sheetName,
110
+ },
111
+ },
112
+ },
113
+ ],
114
+ },
115
+ });
116
+ if (response.status !== 200) {
117
+ throw new Error(
118
+ `Failed to create sheet: ${response.status}. ${JSON.stringify(response.data)}`,
119
+ );
120
+ }
121
+ };
122
+
123
+ appendSheetValues = async (
124
+ spreadsheetUrl: string,
125
+ sheetName: string,
126
+ values: string[][],
127
+ ): Promise<void> => {
128
+ const auth = new google.auth.GoogleAuth({
129
+ keyFile: this.keyFile,
130
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
131
+ });
132
+ const sheets = google.sheets({ version: 'v4', auth });
133
+ const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
134
+ await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
135
+ const sheet = await this.getSheet(spreadsheetUrl, sheetName);
136
+ const range = `${sheetName}!A${sheet ? sheet.length + 1 : 1}:A`;
137
+ const response = await sheets.spreadsheets.values.append({
138
+ spreadsheetId,
139
+ range: range,
140
+ valueInputOption: 'RAW',
141
+ requestBody: {
142
+ values,
143
+ },
144
+ });
145
+ if (response.status !== 200) {
146
+ throw new Error(
147
+ `Failed to append values: ${response.status}. ${JSON.stringify(response.data)}`,
148
+ );
149
+ }
150
+ };
151
+ }
@@ -0,0 +1,46 @@
1
+ import { GraphqlProjectRepository } from './GraphqlProjectRepository';
2
+
3
+ describe('GraphqlProjectRepository', () => {
4
+ let repository: GraphqlProjectRepository;
5
+ const token = process.env.GH_TOKEN;
6
+ const login = 'HiromiShikata';
7
+ const projectUrl = `https://github.com/users/HiromiShikata/projects/49`;
8
+ const projectNumber = 49;
9
+ const projectId = 'PVT_kwHOAGJHa84AFhgF';
10
+
11
+ beforeEach(() => {
12
+ repository = new GraphqlProjectRepository(token);
13
+ });
14
+
15
+ describe('fetchProjectId', () => {
16
+ it('should fetch project ID using GraphQL API', async () => {
17
+ const response = await repository.fetchProjectId(login, projectNumber);
18
+
19
+ expect(response).toEqual(projectId);
20
+ });
21
+ });
22
+
23
+ describe('findProjectIdByUrl', () => {
24
+ it('should extract project ID from URL and fetch it', async () => {
25
+ const response = await repository.findProjectIdByUrl(projectUrl);
26
+ expect(response).toEqual(projectId);
27
+ });
28
+ });
29
+
30
+ describe('getProject', () => {
31
+ it('should retrieve project details', async () => {
32
+ const project = await repository.getProject(projectId);
33
+ expect(project).toEqual({
34
+ id: 'PVT_kwHOAGJHa84AFhgF',
35
+ name: 'V2 project on owner for testing',
36
+ nextActionDate: {
37
+ fieldId: 'PVTF_lAHOAGJHa84AFhgFzgVlnK4',
38
+ name: 'NextActionDate',
39
+ },
40
+ nextActionHour: null,
41
+ remainingEstimationMinutes: null,
42
+ story: null,
43
+ });
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,236 @@
1
+ import axios from 'axios';
2
+ import { BaseGitHubRepository } from './BaseGitHubRepository';
3
+ import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/ProjectRepository';
4
+ import { Project } from '../../domain/entities/Project';
5
+ import { normalizeFieldName } from './utils';
6
+
7
+ export class GraphqlProjectRepository
8
+ extends BaseGitHubRepository
9
+ implements ProjectRepository
10
+ {
11
+ extractProjectFromUrl = (
12
+ projectUrl: string,
13
+ ): {
14
+ owner: string;
15
+ projectNumber: number;
16
+ } => {
17
+ const url = new URL(projectUrl);
18
+ const path = url.pathname.split('/');
19
+ const owner = path[2];
20
+ const projectNumber = parseInt(path[4], 10);
21
+ return { owner, projectNumber };
22
+ };
23
+ fetchProjectId = async (
24
+ login: string,
25
+ projectNumber: number,
26
+ ): Promise<string> => {
27
+ const graphqlQuery = {
28
+ query: `query GetProjectID($login: String!, $number: Int!) {
29
+ organization(login: $login) {
30
+ projectV2(number: $number) {
31
+ id
32
+ }
33
+ }
34
+ user(login: $login){
35
+ projectV2(number: $number){
36
+ id
37
+ }
38
+ }
39
+ }`,
40
+ variables: {
41
+ login: login,
42
+ number: projectNumber,
43
+ },
44
+ };
45
+
46
+ const response = await axios<{
47
+ data: {
48
+ organization: {
49
+ projectV2: {
50
+ id: string;
51
+ };
52
+ };
53
+ user: {
54
+ projectV2: {
55
+ id: string;
56
+ };
57
+ };
58
+ };
59
+ }>({
60
+ url: 'https://api.github.com/graphql',
61
+ method: 'post',
62
+ headers: {
63
+ Authorization: `Bearer ${this.ghToken}`,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ data: JSON.stringify(graphqlQuery),
67
+ });
68
+
69
+ const projectId =
70
+ response.data.data.organization?.projectV2?.id ||
71
+ response.data.data.user?.projectV2?.id;
72
+ if (!projectId) {
73
+ throw new Error('projectId is not found');
74
+ }
75
+ return projectId;
76
+ };
77
+ findProjectIdByUrl = async (
78
+ projectUrl: string,
79
+ ): Promise<Project['id'] | null> => {
80
+ const { owner, projectNumber } = this.extractProjectFromUrl(projectUrl);
81
+ return await this.fetchProjectId(owner, projectNumber);
82
+ };
83
+ getProject = async (projectId: Project['id']): Promise<Project | null> => {
84
+ const query = `query GetProjectV2($projectId: ID!) {
85
+ node(id: $projectId) {
86
+ ... on ProjectV2 {
87
+ id
88
+ title
89
+ shortDescription
90
+ public
91
+ closed
92
+ createdAt
93
+ updatedAt
94
+ number
95
+ url
96
+ fields(first: 100) {
97
+ nodes {
98
+ ... on ProjectV2Field {
99
+ id
100
+ name
101
+ dataType
102
+ }
103
+ ... on ProjectV2IterationField {
104
+ id
105
+ name
106
+ dataType
107
+ configuration {
108
+ iterations {
109
+ startDate
110
+ duration
111
+ title
112
+ }
113
+ }
114
+ }
115
+ ... on ProjectV2SingleSelectField {
116
+ id
117
+ name
118
+ dataType
119
+ options {
120
+ id
121
+ name
122
+ description
123
+ color
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ `;
133
+ const variables = {
134
+ projectId: projectId,
135
+ };
136
+ const response = await axios.post<{
137
+ data: {
138
+ node: {
139
+ id: string;
140
+ title: string;
141
+ shortDescription: string;
142
+ public: boolean;
143
+ closed: boolean;
144
+ createdAt: string;
145
+ updatedAt: string;
146
+ number: number;
147
+ url: string;
148
+ fields: {
149
+ nodes: {
150
+ id: string;
151
+ name: string;
152
+ dataType: string;
153
+ configuration: {
154
+ iterations: {
155
+ startDate: string;
156
+ duration: string;
157
+ title: string;
158
+ }[];
159
+ };
160
+ options: {
161
+ id: string;
162
+ name: string;
163
+ }[];
164
+ }[];
165
+ };
166
+ };
167
+ };
168
+ }>(
169
+ 'https://api.github.com/graphql',
170
+ {
171
+ query,
172
+ variables,
173
+ },
174
+ {
175
+ headers: {
176
+ Authorization: `Bearer ${this.ghToken}`,
177
+ 'Content-Type': 'application/json',
178
+ },
179
+ },
180
+ );
181
+ const project = response.data.data.node;
182
+ if (!project) {
183
+ return null;
184
+ }
185
+ const nextActionDate = project.fields.nodes.find(
186
+ (field) => normalizeFieldName(field.name) === 'nextactiondate',
187
+ );
188
+ const nextActionHour = project.fields.nodes.find(
189
+ (field) => normalizeFieldName(field.name) === 'nextactionhour',
190
+ );
191
+ const story = project.fields.nodes.find(
192
+ (field) => normalizeFieldName(field.name) === 'story',
193
+ );
194
+ const workflowManagementStory = story?.options.find((option) =>
195
+ normalizeFieldName(option.name).includes('workflowmanagement'),
196
+ );
197
+ const remainignEstimationMinutes = project.fields.nodes.find(
198
+ (field) =>
199
+ normalizeFieldName(field.name) === 'remainingestimationminutes',
200
+ );
201
+ return {
202
+ id: project.id,
203
+ name: project.title,
204
+ nextActionDate: nextActionDate
205
+ ? {
206
+ name: nextActionDate.name,
207
+ fieldId: nextActionDate.id,
208
+ }
209
+ : null,
210
+ nextActionHour: nextActionHour
211
+ ? {
212
+ name: nextActionHour.name,
213
+ fieldId: nextActionHour.id,
214
+ }
215
+ : null,
216
+ story:
217
+ story && workflowManagementStory
218
+ ? {
219
+ name: story.name,
220
+ fieldId: story.id,
221
+ stories: story.options.map((option) => ({
222
+ id: option.id,
223
+ name: option.name,
224
+ })),
225
+ workflowManagementStory,
226
+ }
227
+ : null,
228
+ remainingEstimationMinutes: remainignEstimationMinutes
229
+ ? {
230
+ name: remainignEstimationMinutes.name,
231
+ fieldId: remainignEstimationMinutes.id,
232
+ }
233
+ : null,
234
+ };
235
+ };
236
+ }
@@ -0,0 +1,146 @@
1
+ import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
2
+ import { LocalStorageRepository } from './LocalStorageRepository';
3
+
4
+ describe('LocalStorageCacheRepository', () => {
5
+ let localStorageRepository: jest.Mocked<LocalStorageRepository>;
6
+ let repository: LocalStorageCacheRepository;
7
+ let now: Date;
8
+
9
+ beforeEach(() => {
10
+ now = new Date('2024-01-01T00:00:00.000Z');
11
+ jest.useFakeTimers();
12
+ jest.setSystemTime(now);
13
+
14
+ localStorageRepository = {
15
+ listFiles: jest.fn(),
16
+ read: jest.fn(),
17
+ write: jest.fn(),
18
+ mkdir: jest.fn(),
19
+ };
20
+ repository = new LocalStorageCacheRepository(localStorageRepository);
21
+ });
22
+
23
+ afterEach(() => {
24
+ jest.useRealTimers();
25
+ });
26
+
27
+ describe('getLatest', () => {
28
+ type GetLatestParams = Parameters<LocalStorageCacheRepository['getLatest']>;
29
+ type GetLatestReturn = ReturnType<LocalStorageCacheRepository['getLatest']>;
30
+
31
+ interface TestCase {
32
+ name: string;
33
+ key: GetLatestParams[0];
34
+ files: string[];
35
+ fileContent: string | null;
36
+ expected: Awaited<GetLatestReturn>;
37
+ }
38
+
39
+ const testCases: TestCase[] = [
40
+ {
41
+ name: 'returns null when no files exist',
42
+ key: 'test-key',
43
+ files: [],
44
+ fileContent: null,
45
+ expected: null,
46
+ },
47
+ {
48
+ name: 'returns null when file content is empty',
49
+ key: 'test-key',
50
+ files: ['2024-01-01T00:00:00.000Z'],
51
+ fileContent: null,
52
+ expected: null,
53
+ },
54
+ {
55
+ name: 'returns null when content is invalid JSON',
56
+ key: 'test-key',
57
+ files: ['2024-01-01T00:00:00.000Z'],
58
+ fileContent: 'invalid-json',
59
+ expected: null,
60
+ },
61
+ {
62
+ name: 'returns value when valid JSON exists',
63
+ key: 'test-key',
64
+ files: ['2024-01-01T00:00:00.000Z'],
65
+ fileContent: '{"test": "value"}',
66
+ expected: {
67
+ value: { test: 'value' },
68
+ timestamp: new Date('2024-01-01T00:00:00.000Z'),
69
+ },
70
+ },
71
+ ];
72
+
73
+ test.each(testCases)(
74
+ '$name',
75
+ async ({ key, files, fileContent, expected }) => {
76
+ localStorageRepository.listFiles.mockReturnValue(files);
77
+ localStorageRepository.read.mockReturnValue(fileContent);
78
+
79
+ const result = await repository.getLatest(key);
80
+
81
+ expect(result).toEqual(expected);
82
+ },
83
+ );
84
+ });
85
+
86
+ describe('set', () => {
87
+ type SetParams = Parameters<LocalStorageCacheRepository['set']>;
88
+
89
+ interface TestCase {
90
+ name: string;
91
+ key: SetParams[0];
92
+ value: SetParams[1];
93
+ expectedDirPath: string;
94
+ expectedFilePath: string;
95
+ expectedContent: string;
96
+ }
97
+
98
+ const testCases: TestCase[] = [
99
+ {
100
+ name: 'stores string value',
101
+ key: 'test-key',
102
+ value: 'test-value',
103
+ expectedDirPath: './tmp/cache/test-key',
104
+ expectedFilePath: './tmp/cache/test-key/2024-01-01T00:00:00.000Z.json',
105
+ expectedContent: '"test-value"',
106
+ },
107
+ {
108
+ name: 'stores object value',
109
+ key: 'test-key',
110
+ value: { test: 'value' },
111
+ expectedDirPath: './tmp/cache/test-key',
112
+ expectedFilePath: './tmp/cache/test-key/2024-01-01T00:00:00.000Z.json',
113
+ expectedContent: '{"test":"value"}',
114
+ },
115
+ {
116
+ name: 'stores array value',
117
+ key: 'test-key',
118
+ value: [1, 2, 3],
119
+ expectedDirPath: './tmp/cache/test-key',
120
+ expectedFilePath: './tmp/cache/test-key/2024-01-01T00:00:00.000Z.json',
121
+ expectedContent: '[1,2,3]',
122
+ },
123
+ ];
124
+
125
+ test.each(testCases)(
126
+ '$name',
127
+ async ({
128
+ key,
129
+ value,
130
+ expectedDirPath,
131
+ expectedFilePath,
132
+ expectedContent,
133
+ }) => {
134
+ await repository.set(key, value);
135
+
136
+ expect(localStorageRepository.mkdir).toHaveBeenCalledWith(
137
+ expectedDirPath,
138
+ );
139
+ expect(localStorageRepository.write).toHaveBeenCalledWith(
140
+ expectedFilePath,
141
+ expectedContent,
142
+ );
143
+ },
144
+ );
145
+ });
146
+ });