UNPKG

longcelot-sheet-db

Version:

Google Sheets-backed staging database adapter for Node.js with schema-first design

122 lines 7.1 kB
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