feat: Start refactoring code base to be testable
Implement a spreadsheet service abstraction, GAS integration, and Jest testing setup.
This commit is contained in:
85
src/services/GASSpreadsheetService.ts
Normal file
85
src/services/GASSpreadsheetService.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
|
||||
|
||||
export class GASSpreadsheetService implements ISpreadsheetService {
|
||||
private getSheet(sheetName: string): GoogleAppsScript.Spreadsheet.Sheet {
|
||||
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet '${sheetName}' not found`);
|
||||
}
|
||||
return sheet;
|
||||
}
|
||||
|
||||
private getColumnIndex(sheet: GoogleAppsScript.Spreadsheet.Sheet, columnName: string): number {
|
||||
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
||||
return headers.indexOf(columnName);
|
||||
}
|
||||
|
||||
getHeaders(sheetName: string): string[] {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
return sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
||||
}
|
||||
|
||||
getRowData(sheetName: string, row: number): any[] {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const lastCol = sheet.getLastColumn();
|
||||
// getRange(row, column, numRows, numColumns)
|
||||
return sheet.getRange(row, 1, 1, lastCol).getValues()[0];
|
||||
}
|
||||
|
||||
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex === -1) return undefined;
|
||||
|
||||
// Get all values in the column. Note: calling getValues() on a large sheet might be slow,
|
||||
// but this matches the previous implementation's performance characteristics more or less.
|
||||
// Ideally we would cache this or use a more efficient find.
|
||||
// offset(1, colIndex) to skip header, but actually getColumnValuesByName usually gets everything including header or handles it.
|
||||
// Original implementation: getColumnValuesByName gets range from row 2.
|
||||
|
||||
const data = sheet.getRange(2, colIndex + 1, sheet.getLastRow() - 1, 1).getValues();
|
||||
const flatData = data.map(r => r[0]);
|
||||
const index = flatData.indexOf(value);
|
||||
|
||||
if (index === -1) return undefined;
|
||||
return index + 2; // +1 for 0-based index, +1 for header row
|
||||
}
|
||||
|
||||
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
sheet.getRange(row, colIndex + 1).setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
return sheet.getRange(row, colIndex + 1).getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
return sheet.getRange(row, colIndex + 1).getRichTextValue().getLinkUrl();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
const richText = SpreadsheetApp.newRichTextValue()
|
||||
.setText(displayText)
|
||||
.setLinkUrl(url)
|
||||
.build();
|
||||
sheet.getRange(row, colIndex + 1).setRichTextValue(richText);
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/services/MockSpreadsheetService.ts
Normal file
113
src/services/MockSpreadsheetService.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
|
||||
|
||||
export class MockSpreadsheetService implements ISpreadsheetService {
|
||||
// Store data as a map of sheetName -> array of rows (arrays of values)
|
||||
// Row 0 is headers.
|
||||
private sheets: Map<string, any[][]> = new Map();
|
||||
|
||||
constructor(initialData?: { [sheetName: string]: any[][] }) {
|
||||
if (initialData) {
|
||||
for (const [name, rows] of Object.entries(initialData)) {
|
||||
this.sheets.set(name, JSON.parse(JSON.stringify(rows))); // Deep copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSheetData(sheetName: string, data: any[][]) {
|
||||
this.sheets.set(sheetName, data);
|
||||
}
|
||||
|
||||
private getSheet(sheetName: string): any[][] {
|
||||
if (!this.sheets.has(sheetName)) {
|
||||
// Create empty sheet with no headers if accessed but not defined?
|
||||
// Or throw error to mimic GAS?
|
||||
// Let's return empty array or throw.
|
||||
throw new Error(`Sheet '${sheetName}' not found in mock`);
|
||||
}
|
||||
return this.sheets.get(sheetName)!;
|
||||
}
|
||||
|
||||
getHeaders(sheetName: string): string[] {
|
||||
const data = this.getSheet(sheetName);
|
||||
if (data.length === 0) return [];
|
||||
return data[0] as string[];
|
||||
}
|
||||
|
||||
getRowData(sheetName: string, row: number): any[] {
|
||||
const data = this.getSheet(sheetName);
|
||||
// Row is 1-based index
|
||||
if (row > data.length || row < 1) {
|
||||
throw new Error(`Row ${row} out of bounds`);
|
||||
}
|
||||
return data[row - 1]; // Convert to 0-based
|
||||
}
|
||||
|
||||
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
|
||||
const data = this.getSheet(sheetName);
|
||||
if (data.length < 2) return undefined; // Only headers or empty
|
||||
|
||||
const headers = data[0];
|
||||
const colIndex = headers.indexOf(columnName);
|
||||
if (colIndex === -1) return undefined;
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i][colIndex] === value) {
|
||||
return i + 1; // Convert 0-based index to 1-based row number
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
|
||||
const data = this.getSheet(sheetName);
|
||||
const headers = data[0];
|
||||
const colIndex = headers.indexOf(columnName);
|
||||
|
||||
if (colIndex === -1) {
|
||||
throw new Error(`Column '${columnName}' not found`);
|
||||
}
|
||||
|
||||
// Ensure row exists, extend if necessary (basic behavior)
|
||||
while (data.length < row) {
|
||||
data.push(new Array(headers.length).fill(""));
|
||||
}
|
||||
|
||||
data[row - 1][colIndex] = value;
|
||||
}
|
||||
|
||||
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
|
||||
const data = this.getSheet(sheetName);
|
||||
const headers = data[0];
|
||||
const colIndex = headers.indexOf(columnName);
|
||||
|
||||
if (colIndex === -1) return null;
|
||||
if (row > data.length) return null;
|
||||
|
||||
return data[row - 1][colIndex];
|
||||
}
|
||||
|
||||
// Helper to store links: key = "sheetName:row:colIndex", value = url
|
||||
private links: Map<string, string> = new Map();
|
||||
|
||||
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
|
||||
const data = this.getSheet(sheetName);
|
||||
const colIndex = data[0].indexOf(columnName);
|
||||
if (colIndex === -1) return null;
|
||||
|
||||
const key = `${sheetName}:${row}:${colIndex}`;
|
||||
return this.links.get(key) || null;
|
||||
}
|
||||
|
||||
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
|
||||
// Set text value
|
||||
this.setCellValueByColumnName(sheetName, row, columnName, displayText);
|
||||
|
||||
// Set link
|
||||
const data = this.getSheet(sheetName);
|
||||
const colIndex = data[0].indexOf(columnName);
|
||||
if (colIndex !== -1) {
|
||||
const key = `${sheetName}:${row}:${colIndex}`;
|
||||
this.links.set(key, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user