feat: Start refactoring code base to be testable

Implement a spreadsheet service abstraction, GAS integration, and Jest testing setup.
This commit is contained in:
Ben Miller
2025-12-25 03:52:16 -07:00
parent 85cdfe1443
commit 3c6130778e
10 changed files with 5060 additions and 57 deletions

View 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);
}
}
}