longcelot-sheet-db
Version:
Google Sheets-backed staging database adapter for Node.js with schema-first design
122 lines • 7.1 kB
TypeScript
import { SheetReadCacheConfig } from '../schema/types';
import type { StorageClient } from './types';
export interface CreateSpreadsheetOptions {
/** Place the spreadsheet inside this Drive folder ID. */
folderId?: string;
/** When set, enables supportsAllDrives for Shared Drive placement. */
sharedDriveId?: string;
}
/**
* boolean() columns use ONE_OF_LIST too (not a dedicated BOOLEAN type/condition) — see
* buildValidationRules() in src/utils/validationRules.ts for why that's deliberate.
*/
export interface ColumnValidationRule {
columnIndex: number;
type: 'ONE_OF_LIST';
values: (string | number | boolean)[];
}
export interface SheetFormattingOptions {
/** Total number of header columns — used for fill/auto-resize ranges. */
columnCount: number;
headerColor?: string;
freezeHeader?: boolean;
freezeFirstColumn?: boolean;
validations?: ColumnValidationRule[];
/** Number of existing data rows (excluding header) — bounds the validation range. Default: 0. */
dataRowCount?: number;
}
/**
* Extra rows past the current data range to pre-apply boolean/enum validation to,
* so a handful of new rows still get checkbox/dropdown UI before the next sync.
* Left unbounded (the GridRange default), validation extends to the sheet's full
* 1000-row default grid, and Sheets API reads then treat every one of those
* formatted-but-empty rows as "has content" — see FAQ.md #10.
*/
export declare const VALIDATION_ROW_BUFFER = 200;
/**
* How often CRUDOperations.create() re-checks whether the validated range needs
* extending as rows are appended between syncs (see FAQ.md #10 follow-up). Must be
* at most half of VALIDATION_ROW_BUFFER so coverage never runs out between checks:
* a check at row R extends coverage to R + VALIDATION_ROW_BUFFER; the next check at
* R + VALIDATION_CHECK_INTERVAL must still land inside that window.
*/
export declare const VALIDATION_CHECK_INTERVAL: number;
/** Converts a 0-based column index to A1 column letters (0 -> 'A', 25 -> 'Z', 26 -> 'AA'). */
export declare function columnIndexToA1Letter(index: number): string;
export declare class SheetClient implements StorageClient {
private sheets;
private drive;
private auth;
private cacheEnabled;
private cacheTtlMs;
/** getAllRows() results keyed by `${spreadsheetId}::${sheetName}`, valid until expiresAt. */
private _readCache;
/** Collapses concurrent getAllRows() calls for the same key into a single API request. */
private _inFlightReads;
constructor(credentials: {
clientId: string;
clientSecret: string;
redirectUri: string;
}, tokens: unknown, cacheConfig?: SheetReadCacheConfig);
createSpreadsheet(title: string, options?: CreateSpreadsheetOptions): Promise<string>;
findOrCreateFolder(name: string, parentId?: string, sharedDriveId?: string): Promise<string>;
uploadFile(buffer: Buffer, filename: string, mimeType: string, folderId?: string, makePublic?: boolean): Promise<string>;
deleteFile(fileId: string): Promise<void>;
addSheet(spreadsheetId: string, sheetName: string): Promise<void>;
getSheetNames(spreadsheetId: string): Promise<string[]>;
writeHeader(spreadsheetId: string, sheetName: string, headers: string[]): Promise<void>;
/**
* Applies header fill color, frozen rows/columns, auto-fit column widths, and
* boolean/enum data validation dropdowns in a single batchUpdate call.
*/
formatSheet(spreadsheetId: string, sheetName: string, options: SheetFormattingOptions): Promise<void>;
/**
* Re-applies boolean/enum validation rules bounded to `dataRowCount + VALIDATION_ROW_BUFFER`,
* without touching header color/freeze/auto-resize. Called by CRUDOperations.create() as rows
* are appended between syncs, so the validated range keeps pace with real row growth instead
* of only catching up the next time `lsdb sync` runs — see FAQ.md #10 follow-up.
*/
extendValidation(spreadsheetId: string, sheetName: string, validations: ColumnValidationRule[], dataRowCount: number): Promise<void>;
private buildValidationRequests;
/** Returns the 1-based sheet row number the new row was written to (parsed from the API's updatedRange, no extra read). */
appendRow(spreadsheetId: string, sheetName: string, values: string[]): Promise<number>;
appendRows(spreadsheetId: string, sheetName: string, rows: string[][]): Promise<void>;
/**
* Reads the full `A:ZZ` range for a tab. Every findMany()/findOne()/count()/update()/delete()
* call funnels through here, so a single request handler that touches a table more than once
* (e.g. checkUniqueness() calling findOne() per unique column) — or concurrent requests
* from different users hitting the same catalog table — used to mean one Sheets API read per
* call. That's what exhausts Google's default per-user read quota under any real concurrency.
* A short-TTL cache plus in-flight de-duplication collapses those into a single API call;
* see FAQ.md #11 for the incident and CacheConfig for tuning/disabling it.
*/
getAllRows(spreadsheetId: string, sheetName: string): Promise<string[][]>;
private _fetchAllRows;
private _cacheKey;
/** Drops the cached read (if any) for a tab. Called automatically after every write; also exposed for callers that write to a sheet outside this client (e.g. a human editing it directly) and need to force the next read to be fresh. */
invalidateCache(spreadsheetId: string, sheetName: string): void;
updateRow(spreadsheetId: string, sheetName: string, rowIndex: number, values: string[]): Promise<void>;
deleteRow(spreadsheetId: string, sheetName: string, rowIndex: number): Promise<void>;
shareWithUser(spreadsheetId: string, email: string, role?: 'reader' | 'writer'): Promise<void>;
/**
* Deletes an entire tab. Unlike the private getSheetId() used elsewhere (which falls back to
* sheetId 0 when a title isn't found — fine for formatting calls, not for a delete), this
* throws if the tab doesn't exist so callers can distinguish "already gone" from "about to
* delete the wrong tab."
*/
deleteSheet(spreadsheetId: string, sheetName: string): Promise<void>;
/**
* Deletes one or more columns from a tab in a single batchUpdate. `columnIndexes` may be
* given in any order — they're sorted descending internally so earlier deletions don't shift
* the indexes of later ones within the same batch.
*/
deleteColumns(spreadsheetId: string, sheetName: string, columnIndexes: number[]): Promise<void>;
/**
* Overwrites a single header cell in place — used for column rename so existing data rows
* are left untouched (unlike a drop + re-add, which would lose every value in that column).
*/
updateHeaderCell(spreadsheetId: string, sheetName: string, columnIndex: number, value: string): Promise<void>;
private getSheetId;
private getSheetIdStrict;
}
//# sourceMappingURL=sheetClient.d.ts.map