github-issue-tower-defence-management 1.42.3 → 1.42.4
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 +8 -0
- package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +25 -21
- package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/GoogleSpreadsheetRepository.integration.test.ts +120 -0
- package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +273 -71
- package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +82 -20
- package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +67 -1
- package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [1.42.4](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.42.3...v1.42.4) (2026-05-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **core:** mock Google Sheets API in tests to prevent quota exceeded CI failures ([12fb71a](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/12fb71a696ad3f0b5a5248ec42ae02f677b276b2)), closes [#379](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/379)
|
|
7
|
+
* **test:** mock Google Sheets API to prevent quota exceeded flakiness ([94266bf](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/94266bf83792313e3fe2e1be474500adc3f65fd1)), closes [#373](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/373) [#379](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/379)
|
|
8
|
+
|
|
1
9
|
## [1.42.3](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.42.2...v1.42.3) (2026-05-11)
|
|
2
10
|
|
|
3
11
|
|
|
@@ -9,7 +9,7 @@ const dotenv_1 = __importDefault(require("dotenv"));
|
|
|
9
9
|
dotenv_1.default.config();
|
|
10
10
|
class GoogleSpreadsheetRepository {
|
|
11
11
|
constructor(localStorageRepository, serviceAccountKey = process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
|
|
12
|
-
'dummy') {
|
|
12
|
+
'dummy', sheetsClientFactory) {
|
|
13
13
|
this.localStorageRepository = localStorageRepository;
|
|
14
14
|
this.keyFile = './tmp/service-account-key.json';
|
|
15
15
|
this.getSpreadsheetId = (spreadsheetUrl) => {
|
|
@@ -17,11 +17,7 @@ class GoogleSpreadsheetRepository {
|
|
|
17
17
|
return url.pathname.split('/')[3];
|
|
18
18
|
};
|
|
19
19
|
this.getSheet = async (spreadsheetUrl, sheetName) => {
|
|
20
|
-
const
|
|
21
|
-
keyFile: this.keyFile,
|
|
22
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
23
|
-
});
|
|
24
|
-
const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
|
|
20
|
+
const sheets = this.sheetsClient;
|
|
25
21
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
26
22
|
const responseSheet = await sheets.spreadsheets.get({
|
|
27
23
|
spreadsheetId,
|
|
@@ -46,11 +42,7 @@ class GoogleSpreadsheetRepository {
|
|
|
46
42
|
return response.data.values.map((row) => row.map((cell) => String(cell)));
|
|
47
43
|
};
|
|
48
44
|
this.updateCell = async (spreadsheetUrl, sheetName, row, column, value) => {
|
|
49
|
-
const
|
|
50
|
-
keyFile: this.keyFile,
|
|
51
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
52
|
-
});
|
|
53
|
-
const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
|
|
45
|
+
const sheets = this.sheetsClient;
|
|
54
46
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
55
47
|
await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
|
|
56
48
|
const response = await sheets.spreadsheets.values.update({
|
|
@@ -66,11 +58,7 @@ class GoogleSpreadsheetRepository {
|
|
|
66
58
|
}
|
|
67
59
|
};
|
|
68
60
|
this.createNewSheetIfNotExists = async (spreadsheetUrl, sheetName) => {
|
|
69
|
-
const
|
|
70
|
-
keyFile: this.keyFile,
|
|
71
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
72
|
-
});
|
|
73
|
-
const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
|
|
61
|
+
const sheets = this.sheetsClient;
|
|
74
62
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
75
63
|
const sheet = await this.getSheet(spreadsheetUrl, sheetName);
|
|
76
64
|
if (sheet !== null) {
|
|
@@ -95,11 +83,7 @@ class GoogleSpreadsheetRepository {
|
|
|
95
83
|
}
|
|
96
84
|
};
|
|
97
85
|
this.appendSheetValues = async (spreadsheetUrl, sheetName, values) => {
|
|
98
|
-
const
|
|
99
|
-
keyFile: this.keyFile,
|
|
100
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
101
|
-
});
|
|
102
|
-
const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
|
|
86
|
+
const sheets = this.sheetsClient;
|
|
103
87
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
104
88
|
await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
|
|
105
89
|
const sheet = await this.getSheet(spreadsheetUrl, sheetName);
|
|
@@ -117,6 +101,26 @@ class GoogleSpreadsheetRepository {
|
|
|
117
101
|
}
|
|
118
102
|
};
|
|
119
103
|
this.localStorageRepository.write(this.keyFile, serviceAccountKey);
|
|
104
|
+
this.sheetsClient = sheetsClientFactory
|
|
105
|
+
? sheetsClientFactory()
|
|
106
|
+
: (() => {
|
|
107
|
+
const auth = new googleapis_1.google.auth.GoogleAuth({
|
|
108
|
+
keyFile: this.keyFile,
|
|
109
|
+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
110
|
+
});
|
|
111
|
+
const googleSheets = googleapis_1.google.sheets({ version: 'v4', auth });
|
|
112
|
+
return {
|
|
113
|
+
spreadsheets: {
|
|
114
|
+
get: (params) => googleSheets.spreadsheets.get(params),
|
|
115
|
+
values: {
|
|
116
|
+
get: (params) => googleSheets.spreadsheets.values.get(params),
|
|
117
|
+
update: (params) => googleSheets.spreadsheets.values.update(params),
|
|
118
|
+
append: (params) => googleSheets.spreadsheets.values.append(params),
|
|
119
|
+
},
|
|
120
|
+
batchUpdate: (params) => googleSheets.spreadsheets.batchUpdate(params),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
})();
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
exports.GoogleSpreadsheetRepository = GoogleSpreadsheetRepository;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoogleSpreadsheetRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":";;;;;;AACA,2CAAoC;AAEpC,oDAA4B;AAC5B,gBAAM,CAAC,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"GoogleSpreadsheetRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import { GoogleSpreadsheetRepository } from './GoogleSpreadsheetRepository';
|
|
3
|
+
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
4
|
+
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
const GOOGLE_SERVICE_ACCOUNT_KEY = process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
|
|
8
|
+
|
|
9
|
+
if (!GOOGLE_SERVICE_ACCOUNT_KEY) {
|
|
10
|
+
throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('GoogleSpreadsheetRepository integration tests', () => {
|
|
14
|
+
jest.setTimeout(60 * 1000);
|
|
15
|
+
jest.retryTimes(3, { logErrorsBeforeRetry: true });
|
|
16
|
+
|
|
17
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
18
|
+
const spreadsheetUrl =
|
|
19
|
+
'https://docs.google.com/spreadsheets/d/1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM/edit?gid=0#gid=0';
|
|
20
|
+
const repository = new GoogleSpreadsheetRepository(
|
|
21
|
+
localStorageRepository,
|
|
22
|
+
GOOGLE_SERVICE_ACCOUNT_KEY,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('getSheet', () => {
|
|
30
|
+
const testCases: [string, string[][] | null][] = [
|
|
31
|
+
['SheetUndefined', null],
|
|
32
|
+
['SheetSingleCell', [['test']]],
|
|
33
|
+
[
|
|
34
|
+
'SheetMultipleRows',
|
|
35
|
+
[
|
|
36
|
+
['1', '2'],
|
|
37
|
+
['3', '4'],
|
|
38
|
+
],
|
|
39
|
+
],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
test.each(testCases)(
|
|
43
|
+
'gets sheet %s',
|
|
44
|
+
async (sheetName: string, expected: string[][] | null) => {
|
|
45
|
+
const result = await repository.getSheet(spreadsheetUrl, sheetName);
|
|
46
|
+
expect(result).toEqual(expected);
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
test('returns null for non-existent sheet', async () => {
|
|
51
|
+
const result = await repository.getSheet(
|
|
52
|
+
spreadsheetUrl,
|
|
53
|
+
'NonExistentSheet',
|
|
54
|
+
);
|
|
55
|
+
expect(result).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('updateCell', () => {
|
|
60
|
+
const testCases: [string, number, number, string][] = [
|
|
61
|
+
['Sheet1', 0, 0, 'First Value'],
|
|
62
|
+
['Sheet1', 0, 0, 'Updated Value'],
|
|
63
|
+
['Sheet1', 1, 1, '123'],
|
|
64
|
+
['Sheet1', 2, 2, 'Test'],
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
test.each(testCases)(
|
|
68
|
+
'updates cell in sheet %s at row %d col %d with value %s',
|
|
69
|
+
async (sheetName: string, row: number, col: number, value: string) => {
|
|
70
|
+
await repository.updateCell(spreadsheetUrl, sheetName, row, col, value);
|
|
71
|
+
const result = await repository.getSheet(spreadsheetUrl, sheetName);
|
|
72
|
+
if (!result) {
|
|
73
|
+
throw new Error('Sheet not found');
|
|
74
|
+
}
|
|
75
|
+
expect(result[row][col]).toBe(value);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('appendSheetValues', () => {
|
|
81
|
+
const testCases: [string[][]][] = [
|
|
82
|
+
[[['Single Row']]],
|
|
83
|
+
[[['Multiple', 'Columns']]],
|
|
84
|
+
[
|
|
85
|
+
[
|
|
86
|
+
['Row1Col1', 'Row1Col2'],
|
|
87
|
+
['Row2Col1', 'Row2Col2'],
|
|
88
|
+
],
|
|
89
|
+
],
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
test.each(testCases)(
|
|
93
|
+
'appends values %j to sheet',
|
|
94
|
+
async (values: string[][]) => {
|
|
95
|
+
const sheetName = 'AppendTest';
|
|
96
|
+
const initialSheet = await repository.getSheet(
|
|
97
|
+
spreadsheetUrl,
|
|
98
|
+
sheetName,
|
|
99
|
+
);
|
|
100
|
+
const initialLength = initialSheet ? initialSheet.length : 0;
|
|
101
|
+
|
|
102
|
+
await repository.appendSheetValues(spreadsheetUrl, sheetName, values);
|
|
103
|
+
|
|
104
|
+
const updatedSheet = await repository.getSheet(
|
|
105
|
+
spreadsheetUrl,
|
|
106
|
+
sheetName,
|
|
107
|
+
);
|
|
108
|
+
expect(updatedSheet).not.toBeNull();
|
|
109
|
+
if (!updatedSheet) {
|
|
110
|
+
throw new Error('Sheet not found');
|
|
111
|
+
}
|
|
112
|
+
expect(updatedSheet.length).toBe(initialLength + values.length);
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < values.length; i++) {
|
|
115
|
+
expect(updatedSheet[initialLength + i]).toEqual(values[i]);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -1,13 +1,56 @@
|
|
|
1
1
|
import { GoogleSpreadsheetRepository } from './GoogleSpreadsheetRepository';
|
|
2
|
-
import { describe, test, expect } from '@jest/globals';
|
|
2
|
+
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
|
|
3
3
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
4
4
|
|
|
5
|
+
type SpreadsheetGetResponse = {
|
|
6
|
+
status: number;
|
|
7
|
+
data: {
|
|
8
|
+
sheets?: Array<{
|
|
9
|
+
properties?: { title?: string | null } | null;
|
|
10
|
+
}> | null;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ValuesGetResponse = {
|
|
15
|
+
status: number;
|
|
16
|
+
data: { values?: unknown[][] | null };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SimpleResponse = { status: number; data: unknown };
|
|
20
|
+
|
|
5
21
|
describe('GoogleSpreadsheetRepository', () => {
|
|
6
22
|
const localStorageRepository = new LocalStorageRepository();
|
|
7
|
-
const repository = new GoogleSpreadsheetRepository(localStorageRepository);
|
|
8
23
|
const spreadsheetUrl =
|
|
9
24
|
'https://docs.google.com/spreadsheets/d/1N_3y0y46v5tHbra5YSm6PldflcsF1bkfeWDdQ3MRuXM/edit?gid=0#gid=0';
|
|
10
25
|
|
|
26
|
+
const mockSpreadsheetsGet = jest.fn<() => Promise<SpreadsheetGetResponse>>();
|
|
27
|
+
const mockSpreadsheetsValuesGet = jest.fn<() => Promise<ValuesGetResponse>>();
|
|
28
|
+
const mockSpreadsheetsValuesUpdate = jest.fn<() => Promise<SimpleResponse>>();
|
|
29
|
+
const mockSpreadsheetsValuesAppend = jest.fn<() => Promise<SimpleResponse>>();
|
|
30
|
+
const mockSpreadsheetsBatchUpdate = jest.fn<() => Promise<SimpleResponse>>();
|
|
31
|
+
|
|
32
|
+
const mockSheetsClient = {
|
|
33
|
+
spreadsheets: {
|
|
34
|
+
get: mockSpreadsheetsGet,
|
|
35
|
+
values: {
|
|
36
|
+
get: mockSpreadsheetsValuesGet,
|
|
37
|
+
update: mockSpreadsheetsValuesUpdate,
|
|
38
|
+
append: mockSpreadsheetsValuesAppend,
|
|
39
|
+
},
|
|
40
|
+
batchUpdate: mockSpreadsheetsBatchUpdate,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const repository = new GoogleSpreadsheetRepository(
|
|
45
|
+
localStorageRepository,
|
|
46
|
+
'dummy-service-account-key',
|
|
47
|
+
() => mockSheetsClient,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
11
54
|
describe('getSpreadsheetId', () => {
|
|
12
55
|
const testCases: [string, string][] = [
|
|
13
56
|
[
|
|
@@ -29,96 +72,255 @@ describe('GoogleSpreadsheetRepository', () => {
|
|
|
29
72
|
});
|
|
30
73
|
|
|
31
74
|
describe('getSheet', () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'SheetMultipleRows',
|
|
38
|
-
'Multiple Rows',
|
|
39
|
-
[
|
|
40
|
-
['1', '2'],
|
|
41
|
-
['3', '4'],
|
|
42
|
-
],
|
|
43
|
-
],
|
|
44
|
-
];
|
|
75
|
+
test('returns null when sheet is not in spreadsheet', async () => {
|
|
76
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
77
|
+
status: 200,
|
|
78
|
+
data: { sheets: [] },
|
|
79
|
+
});
|
|
45
80
|
|
|
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
81
|
const result = await repository.getSheet(
|
|
56
82
|
spreadsheetUrl,
|
|
57
83
|
'NonExistentSheet',
|
|
58
84
|
);
|
|
85
|
+
|
|
86
|
+
expect(result).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('returns null when sheet name does not match', async () => {
|
|
90
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
91
|
+
status: 200,
|
|
92
|
+
data: {
|
|
93
|
+
sheets: [{ properties: { title: 'OtherSheet' } }],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await repository.getSheet(
|
|
98
|
+
spreadsheetUrl,
|
|
99
|
+
'SheetUndefined',
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('returns single cell value', async () => {
|
|
106
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
107
|
+
status: 200,
|
|
108
|
+
data: {
|
|
109
|
+
sheets: [{ properties: { title: 'SheetSingleCell' } }],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
113
|
+
status: 200,
|
|
114
|
+
data: { values: [['test']] },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const result = await repository.getSheet(
|
|
118
|
+
spreadsheetUrl,
|
|
119
|
+
'SheetSingleCell',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(result).toEqual([['test']]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('returns multiple rows', async () => {
|
|
126
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
127
|
+
status: 200,
|
|
128
|
+
data: {
|
|
129
|
+
sheets: [{ properties: { title: 'SheetMultipleRows' } }],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
133
|
+
status: 200,
|
|
134
|
+
data: {
|
|
135
|
+
values: [
|
|
136
|
+
['1', '2'],
|
|
137
|
+
['3', '4'],
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await repository.getSheet(
|
|
143
|
+
spreadsheetUrl,
|
|
144
|
+
'SheetMultipleRows',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(result).toEqual([
|
|
148
|
+
['1', '2'],
|
|
149
|
+
['3', '4'],
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('returns null when sheet has no values', async () => {
|
|
154
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
155
|
+
status: 200,
|
|
156
|
+
data: {
|
|
157
|
+
sheets: [{ properties: { title: 'EmptySheet' } }],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
161
|
+
status: 200,
|
|
162
|
+
data: {},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const result = await repository.getSheet(spreadsheetUrl, 'EmptySheet');
|
|
166
|
+
|
|
59
167
|
expect(result).toBeNull();
|
|
60
168
|
});
|
|
169
|
+
|
|
170
|
+
test('converts non-string cell values to strings', async () => {
|
|
171
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
172
|
+
status: 200,
|
|
173
|
+
data: {
|
|
174
|
+
sheets: [{ properties: { title: 'MixedTypes' } }],
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
178
|
+
status: 200,
|
|
179
|
+
data: { values: [[1, true, 'text']] },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = await repository.getSheet(spreadsheetUrl, 'MixedTypes');
|
|
183
|
+
|
|
184
|
+
expect(result).toEqual([['1', 'true', 'text']]);
|
|
185
|
+
});
|
|
61
186
|
});
|
|
62
187
|
|
|
63
188
|
describe('updateCell', () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
191
|
+
status: 200,
|
|
192
|
+
data: {
|
|
193
|
+
sheets: [{ properties: { title: 'Sheet1' } }],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
197
|
+
status: 200,
|
|
198
|
+
data: { values: [['existing']] },
|
|
199
|
+
});
|
|
200
|
+
mockSpreadsheetsValuesUpdate.mockResolvedValue({
|
|
201
|
+
status: 200,
|
|
202
|
+
data: {},
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const testCases: [string, number, number, string, string][] = [
|
|
207
|
+
['Sheet1', 0, 0, 'First Value', 'Sheet1!A1'],
|
|
208
|
+
['Sheet1', 0, 0, 'Updated Value', 'Sheet1!A1'],
|
|
209
|
+
['Sheet1', 1, 1, '123', 'Sheet1!B2'],
|
|
210
|
+
['Sheet1', 2, 2, 'Test', 'Sheet1!C3'],
|
|
69
211
|
];
|
|
70
212
|
|
|
71
213
|
test.each(testCases)(
|
|
72
|
-
'updates cell in sheet %s at row %d col %d with value %s',
|
|
73
|
-
async (sheetName
|
|
214
|
+
'updates cell in sheet %s at row %d col %d with value %s using range %s',
|
|
215
|
+
async (sheetName, row, col, value, expectedRange) => {
|
|
74
216
|
await repository.updateCell(spreadsheetUrl, sheetName, row, col, value);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
217
|
+
|
|
218
|
+
expect(mockSpreadsheetsValuesUpdate).toHaveBeenCalledWith(
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
range: expectedRange,
|
|
221
|
+
valueInputOption: 'RAW',
|
|
222
|
+
requestBody: { values: [[value]] },
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
80
225
|
},
|
|
81
226
|
);
|
|
82
227
|
});
|
|
83
228
|
|
|
84
229
|
describe('appendSheetValues', () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
230
|
+
test('appends to existing sheet starting after last row', async () => {
|
|
231
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
232
|
+
status: 200,
|
|
233
|
+
data: {
|
|
234
|
+
sheets: [{ properties: { title: 'AppendTest' } }],
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
238
|
+
status: 200,
|
|
239
|
+
data: { values: [['Row1'], ['Row2']] },
|
|
240
|
+
});
|
|
241
|
+
mockSpreadsheetsValuesAppend.mockResolvedValue({
|
|
242
|
+
status: 200,
|
|
243
|
+
data: {},
|
|
244
|
+
});
|
|
95
245
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const sheetName = 'AppendTest';
|
|
100
|
-
const initialSheet = await repository.getSheet(
|
|
101
|
-
spreadsheetUrl,
|
|
102
|
-
sheetName,
|
|
103
|
-
);
|
|
104
|
-
const initialLength = initialSheet ? initialSheet.length : 0;
|
|
246
|
+
await repository.appendSheetValues(spreadsheetUrl, 'AppendTest', [
|
|
247
|
+
['NewRow'],
|
|
248
|
+
]);
|
|
105
249
|
|
|
106
|
-
|
|
250
|
+
expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
|
|
251
|
+
expect.objectContaining({
|
|
252
|
+
range: 'AppendTest!A3:A',
|
|
253
|
+
valueInputOption: 'RAW',
|
|
254
|
+
requestBody: { values: [['NewRow']] },
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
107
258
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
259
|
+
test('appends multiple rows with correct values', async () => {
|
|
260
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
261
|
+
status: 200,
|
|
262
|
+
data: {
|
|
263
|
+
sheets: [{ properties: { title: 'AppendTest' } }],
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
267
|
+
status: 200,
|
|
268
|
+
data: { values: [['Existing']] },
|
|
269
|
+
});
|
|
270
|
+
mockSpreadsheetsValuesAppend.mockResolvedValue({
|
|
271
|
+
status: 200,
|
|
272
|
+
data: {},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const newValues = [
|
|
276
|
+
['Row1Col1', 'Row1Col2'],
|
|
277
|
+
['Row2Col1', 'Row2Col2'],
|
|
278
|
+
];
|
|
279
|
+
await repository.appendSheetValues(
|
|
280
|
+
spreadsheetUrl,
|
|
281
|
+
'AppendTest',
|
|
282
|
+
newValues,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
|
|
286
|
+
expect.objectContaining({
|
|
287
|
+
requestBody: { values: newValues },
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('creates new sheet via batchUpdate when sheet does not exist', async () => {
|
|
293
|
+
mockSpreadsheetsGet.mockResolvedValue({
|
|
294
|
+
status: 200,
|
|
295
|
+
data: { sheets: [] },
|
|
296
|
+
});
|
|
297
|
+
mockSpreadsheetsBatchUpdate.mockResolvedValue({
|
|
298
|
+
status: 200,
|
|
299
|
+
data: {},
|
|
300
|
+
});
|
|
301
|
+
mockSpreadsheetsValuesGet.mockResolvedValue({
|
|
302
|
+
status: 200,
|
|
303
|
+
data: {},
|
|
304
|
+
});
|
|
305
|
+
mockSpreadsheetsValuesAppend.mockResolvedValue({
|
|
306
|
+
status: 200,
|
|
307
|
+
data: {},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await repository.appendSheetValues(spreadsheetUrl, 'NewSheet', [['Row']]);
|
|
311
|
+
|
|
312
|
+
expect(mockSpreadsheetsBatchUpdate).toHaveBeenCalledWith(
|
|
313
|
+
expect.objectContaining({
|
|
314
|
+
requestBody: {
|
|
315
|
+
requests: [{ addSheet: { properties: { title: 'NewSheet' } } }],
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
expect(mockSpreadsheetsValuesAppend).toHaveBeenCalledWith(
|
|
320
|
+
expect.objectContaining({
|
|
321
|
+
range: 'NewSheet!A1:A',
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
});
|
|
123
325
|
});
|
|
124
326
|
});
|
|
@@ -4,15 +4,93 @@ import { LocalStorageRepository } from './LocalStorageRepository';
|
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
dotenv.config();
|
|
6
6
|
|
|
7
|
+
interface SheetsApiClient {
|
|
8
|
+
spreadsheets: {
|
|
9
|
+
get(params: { spreadsheetId: string }): Promise<{
|
|
10
|
+
status: number;
|
|
11
|
+
data: {
|
|
12
|
+
sheets?: Array<{
|
|
13
|
+
properties?: { title?: string | null } | null;
|
|
14
|
+
}> | null;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
17
|
+
values: {
|
|
18
|
+
get(params: { spreadsheetId: string; range: string }): Promise<{
|
|
19
|
+
status: number;
|
|
20
|
+
data: { values?: unknown[][] | null };
|
|
21
|
+
}>;
|
|
22
|
+
update(params: {
|
|
23
|
+
spreadsheetId: string;
|
|
24
|
+
range: string;
|
|
25
|
+
valueInputOption: string;
|
|
26
|
+
requestBody: { values: string[][] };
|
|
27
|
+
}): Promise<{ status: number; data: unknown }>;
|
|
28
|
+
append(params: {
|
|
29
|
+
spreadsheetId: string;
|
|
30
|
+
range: string;
|
|
31
|
+
valueInputOption: string;
|
|
32
|
+
requestBody: { values: string[][] };
|
|
33
|
+
}): Promise<{ status: number; data: unknown }>;
|
|
34
|
+
};
|
|
35
|
+
batchUpdate(params: {
|
|
36
|
+
spreadsheetId: string;
|
|
37
|
+
requestBody: {
|
|
38
|
+
requests: Array<{ addSheet?: { properties?: { title?: string } } }>;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<{ status: number; data: unknown }>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
7
44
|
export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
8
45
|
keyFile = './tmp/service-account-key.json';
|
|
46
|
+
private readonly sheetsClient: SheetsApiClient;
|
|
9
47
|
|
|
10
48
|
constructor(
|
|
11
49
|
readonly localStorageRepository: LocalStorageRepository,
|
|
12
50
|
serviceAccountKey: string = process.env.GOOGLE_SERVICE_ACCOUNT_KEY ||
|
|
13
51
|
'dummy',
|
|
52
|
+
sheetsClientFactory?: () => SheetsApiClient,
|
|
14
53
|
) {
|
|
15
54
|
this.localStorageRepository.write(this.keyFile, serviceAccountKey);
|
|
55
|
+
this.sheetsClient = sheetsClientFactory
|
|
56
|
+
? sheetsClientFactory()
|
|
57
|
+
: (() => {
|
|
58
|
+
const auth = new google.auth.GoogleAuth({
|
|
59
|
+
keyFile: this.keyFile,
|
|
60
|
+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
61
|
+
});
|
|
62
|
+
const googleSheets = google.sheets({ version: 'v4', auth });
|
|
63
|
+
return {
|
|
64
|
+
spreadsheets: {
|
|
65
|
+
get: (params: { spreadsheetId: string }) =>
|
|
66
|
+
googleSheets.spreadsheets.get(params),
|
|
67
|
+
values: {
|
|
68
|
+
get: (params: { spreadsheetId: string; range: string }) =>
|
|
69
|
+
googleSheets.spreadsheets.values.get(params),
|
|
70
|
+
update: (params: {
|
|
71
|
+
spreadsheetId: string;
|
|
72
|
+
range: string;
|
|
73
|
+
valueInputOption: string;
|
|
74
|
+
requestBody: { values: string[][] };
|
|
75
|
+
}) => googleSheets.spreadsheets.values.update(params),
|
|
76
|
+
append: (params: {
|
|
77
|
+
spreadsheetId: string;
|
|
78
|
+
range: string;
|
|
79
|
+
valueInputOption: string;
|
|
80
|
+
requestBody: { values: string[][] };
|
|
81
|
+
}) => googleSheets.spreadsheets.values.append(params),
|
|
82
|
+
},
|
|
83
|
+
batchUpdate: (params: {
|
|
84
|
+
spreadsheetId: string;
|
|
85
|
+
requestBody: {
|
|
86
|
+
requests: Array<{
|
|
87
|
+
addSheet?: { properties?: { title?: string } };
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
}) => googleSheets.spreadsheets.batchUpdate(params),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
})();
|
|
16
94
|
}
|
|
17
95
|
|
|
18
96
|
getSpreadsheetId = (spreadsheetUrl: string): string => {
|
|
@@ -23,11 +101,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
23
101
|
spreadsheetUrl: string,
|
|
24
102
|
sheetName: string,
|
|
25
103
|
): Promise<string[][] | null> => {
|
|
26
|
-
const
|
|
27
|
-
keyFile: this.keyFile,
|
|
28
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
29
|
-
});
|
|
30
|
-
const sheets = google.sheets({ version: 'v4', auth });
|
|
104
|
+
const sheets = this.sheetsClient;
|
|
31
105
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
32
106
|
const responseSheet = await sheets.spreadsheets.get({
|
|
33
107
|
spreadsheetId,
|
|
@@ -64,11 +138,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
64
138
|
column: number,
|
|
65
139
|
value: string,
|
|
66
140
|
): Promise<void> => {
|
|
67
|
-
const
|
|
68
|
-
keyFile: this.keyFile,
|
|
69
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
70
|
-
});
|
|
71
|
-
const sheets = google.sheets({ version: 'v4', auth });
|
|
141
|
+
const sheets = this.sheetsClient;
|
|
72
142
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
73
143
|
await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
|
|
74
144
|
const response = await sheets.spreadsheets.values.update({
|
|
@@ -89,11 +159,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
89
159
|
spreadsheetUrl: string,
|
|
90
160
|
sheetName: string,
|
|
91
161
|
): Promise<void> => {
|
|
92
|
-
const
|
|
93
|
-
keyFile: this.keyFile,
|
|
94
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
95
|
-
});
|
|
96
|
-
const sheets = google.sheets({ version: 'v4', auth });
|
|
162
|
+
const sheets = this.sheetsClient;
|
|
97
163
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
98
164
|
const sheet = await this.getSheet(spreadsheetUrl, sheetName);
|
|
99
165
|
if (sheet !== null) {
|
|
@@ -125,11 +191,7 @@ export class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
|
125
191
|
sheetName: string,
|
|
126
192
|
values: string[][],
|
|
127
193
|
): Promise<void> => {
|
|
128
|
-
const
|
|
129
|
-
keyFile: this.keyFile,
|
|
130
|
-
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
131
|
-
});
|
|
132
|
-
const sheets = google.sheets({ version: 'v4', auth });
|
|
194
|
+
const sheets = this.sheetsClient;
|
|
133
195
|
const spreadsheetId = this.getSpreadsheetId(spreadsheetUrl);
|
|
134
196
|
await this.createNewSheetIfNotExists(spreadsheetUrl, sheetName);
|
|
135
197
|
const sheet = await this.getSheet(spreadsheetUrl, sheetName);
|
|
@@ -1,13 +1,79 @@
|
|
|
1
1
|
import { SpreadsheetRepository } from '../../domain/usecases/adapter-interfaces/SpreadsheetRepository';
|
|
2
2
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
3
|
+
interface SheetsApiClient {
|
|
4
|
+
spreadsheets: {
|
|
5
|
+
get(params: {
|
|
6
|
+
spreadsheetId: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
status: number;
|
|
9
|
+
data: {
|
|
10
|
+
sheets?: Array<{
|
|
11
|
+
properties?: {
|
|
12
|
+
title?: string | null;
|
|
13
|
+
} | null;
|
|
14
|
+
}> | null;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
17
|
+
values: {
|
|
18
|
+
get(params: {
|
|
19
|
+
spreadsheetId: string;
|
|
20
|
+
range: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
status: number;
|
|
23
|
+
data: {
|
|
24
|
+
values?: unknown[][] | null;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
update(params: {
|
|
28
|
+
spreadsheetId: string;
|
|
29
|
+
range: string;
|
|
30
|
+
valueInputOption: string;
|
|
31
|
+
requestBody: {
|
|
32
|
+
values: string[][];
|
|
33
|
+
};
|
|
34
|
+
}): Promise<{
|
|
35
|
+
status: number;
|
|
36
|
+
data: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
append(params: {
|
|
39
|
+
spreadsheetId: string;
|
|
40
|
+
range: string;
|
|
41
|
+
valueInputOption: string;
|
|
42
|
+
requestBody: {
|
|
43
|
+
values: string[][];
|
|
44
|
+
};
|
|
45
|
+
}): Promise<{
|
|
46
|
+
status: number;
|
|
47
|
+
data: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
batchUpdate(params: {
|
|
51
|
+
spreadsheetId: string;
|
|
52
|
+
requestBody: {
|
|
53
|
+
requests: Array<{
|
|
54
|
+
addSheet?: {
|
|
55
|
+
properties?: {
|
|
56
|
+
title?: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
}): Promise<{
|
|
62
|
+
status: number;
|
|
63
|
+
data: unknown;
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
3
67
|
export declare class GoogleSpreadsheetRepository implements SpreadsheetRepository {
|
|
4
68
|
readonly localStorageRepository: LocalStorageRepository;
|
|
5
69
|
keyFile: string;
|
|
6
|
-
|
|
70
|
+
private readonly sheetsClient;
|
|
71
|
+
constructor(localStorageRepository: LocalStorageRepository, serviceAccountKey?: string, sheetsClientFactory?: () => SheetsApiClient);
|
|
7
72
|
getSpreadsheetId: (spreadsheetUrl: string) => string;
|
|
8
73
|
getSheet: (spreadsheetUrl: string, sheetName: string) => Promise<string[][] | null>;
|
|
9
74
|
updateCell: (spreadsheetUrl: string, sheetName: string, row: number, column: number, value: string) => Promise<void>;
|
|
10
75
|
createNewSheetIfNotExists: (spreadsheetUrl: string, sheetName: string) => Promise<void>;
|
|
11
76
|
appendSheetValues: (spreadsheetUrl: string, sheetName: string, values: string[][]) => Promise<void>;
|
|
12
77
|
}
|
|
78
|
+
export {};
|
|
13
79
|
//# sourceMappingURL=GoogleSpreadsheetRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GoogleSpreadsheetRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,gEAAgE,CAAC;AAEvG,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAIlE,qBAAa,2BAA4B,YAAW,qBAAqB;
|
|
1
|
+
{"version":3,"file":"GoogleSpreadsheetRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GoogleSpreadsheetRepository.ts"],"names":[],"mappings":"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"}
|