Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 102 additions & 271 deletions dist/bundle.global.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/bundle.global.js.map

Large diffs are not rendered by default.

373 changes: 102 additions & 271 deletions dist/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/bundle.js.map

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions dist/types/utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GQueryRow } from "./types";
export declare function parseRows(headers: string[], values: any[][]): GQueryRow[];
export declare function fetchSheetData(spreadsheetId: string, sheetName: string): {
headers: string[];
rows: GQueryRow[];
};
30 changes: 2 additions & 28 deletions src/delete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GQueryTableFactory } from "./index";
import { callHandler } from "./ratelimit";
import { fetchSheetData } from "./utils";

export function deleteInternal(gqueryTableFactory: GQueryTableFactory): {
deletedRows: number;
Expand All @@ -10,35 +11,8 @@ export function deleteInternal(gqueryTableFactory: GQueryTableFactory): {
const sheet = gqueryTableFactory.gQueryTable.sheet;
const sheetId = sheet.getSheetId();

// Fetch current data from the sheet
const response = callHandler(() =>
Sheets.Spreadsheets.Values.get(spreadsheetId, sheetName)
);
const values = response.values || [];
const { rows } = fetchSheetData(spreadsheetId, sheetName);

if (values.length <= 1) {
// Only header row or empty sheet
return { deletedRows: 0 };
}

// Extract headers and rows
const headers = values[0];
const rows: Record<string, any>[] = values.slice(1).map((row, rowIndex) => {
const obj: Record<string, any> = {
__meta: {
rowNum: rowIndex + 2, // +2 because we're starting from index 0 and row 1 is headers
colLength: row.length,
},
};

headers.forEach((header: string, i: number) => {
obj[header] = i < row.length ? row[i] : "";
});

return obj;
});

// If no filter option, nothing to delete
if (!gqueryTableFactory.filterOption || rows.length === 0) {
return { deletedRows: 0 };
}
Expand Down
210 changes: 37 additions & 173 deletions src/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DateTimeRenderOption,
GQueryRow,
} from "./types";
import { parseRows } from "./utils";

export function getManyInternal(
gquery: GQuery,
Expand All @@ -19,62 +20,15 @@ export function getManyInternal(
return {};
}

// Set default options if not provided
const valueRenderOption =
options?.valueRenderOption || ValueRenderOption.FORMATTED_VALUE;
const dateTimeRenderOption =
options?.dateTimeRenderOption || DateTimeRenderOption.FORMATTED_STRING;

const result: { [sheetName: string]: GQueryResult } = {};
const headersMap: { [sheetName: string]: string[] } = {};

// Step 1: Get headers for each sheet (row 1)
for (const sheetName of sheetNames) {
try {
const headerResponse = callHandler(() =>
Sheets.Spreadsheets.Values.get(
gquery.spreadsheetId,
`${sheetName}!1:1`,
{
valueRenderOption: valueRenderOption,
dateTimeRenderOption: dateTimeRenderOption,
}
)
);

if (
!headerResponse ||
!headerResponse.values ||
headerResponse.values.length === 0
) {
// Handle empty sheet or sheet with no headers
result[sheetName] = { headers: [], rows: [] };
continue;
}

headersMap[sheetName] = headerResponse.values[0].map((header) =>
String(header)
);
} catch (e) {
console.error(`Error fetching headers for sheet ${sheetName}:`, e);
result[sheetName] = { headers: [], rows: [] };
}
}

// Step 2: Get data for sheets that have headers
const sheetsToFetch = Object.keys(headersMap).filter(
(sheet) => headersMap[sheet].length > 0
);

if (sheetsToFetch.length === 0) {
return result;
}

// Also fetch metadata for each sheet to determine data types
let sheetMetadata: { [sheetName: string]: { [header: string]: string } } = {};

try {
// Get spreadsheet metadata including sheet tables if available
const metadataResponse = callHandler(() =>
Sheets.Spreadsheets.get(gquery.spreadsheetId, {
fields: "sheets(properties(title),tables.columnProperties)",
Expand All @@ -84,17 +38,14 @@ export function getManyInternal(
if (metadataResponse && metadataResponse.sheets) {
metadataResponse.sheets.forEach((sheet) => {
const sheetName = sheet.properties?.title;
if (!sheetName || !sheetsToFetch.includes(sheetName)) return;
if (!sheetName || !sheetNames.includes(sheetName)) return;

// @ts-expect-error: TypeScript may not recognize the tables property
// @ts-expect-error: tables may not be typed
if (sheet.tables && sheet.tables.length > 0) {
// Use the first table definition for column properties
// @ts-expect-error: TypeScript may not recognize the tables property
// @ts-expect-error
const table = sheet.tables[0];
if (table.columnProperties) {
sheetMetadata[sheetName] = {};

// For each column property, store its data type
Object.keys(table.columnProperties).forEach((column) => {
const dataType = table.columnProperties[column].dataType;
if (dataType) {
Expand All @@ -107,170 +58,83 @@ export function getManyInternal(
}
} catch (e) {
console.error("Error fetching metadata:", e);
// Continue without metadata - types won't be converted
}

// Batch get data for all sheets (just use the sheet name as the range)
const dataRanges = sheetsToFetch.map((sheet) => `${sheet}`);
const dataResponse = callHandler(() =>
Sheets.Spreadsheets.Values.batchGet(gquery.spreadsheetId, {
ranges: dataRanges,
valueRenderOption: valueRenderOption,
dateTimeRenderOption: dateTimeRenderOption,
ranges: sheetNames.map((s) => `${s}`),
valueRenderOption,
dateTimeRenderOption,
})
);

if (!dataResponse || !dataResponse.valueRanges) {
// Return just the headers if we couldn't get any data
sheetsToFetch.forEach((sheet) => {
result[sheet] = {
headers: headersMap[sheet],
rows: [],
};
sheetNames.forEach((sheet) => {
result[sheet] = { headers: [], rows: [] };
});
return result;
}

// Process each value range from the batch response
dataResponse.valueRanges.forEach((valueRange, index) => {
const sheetName = sheetsToFetch[index];
const headers = headersMap[sheetName];
const sheetName = sheetNames[index];

if (!valueRange.values || valueRange.values.length === 0) {
// Sheet exists but has no data rows
result[sheetName] = { headers, rows: [] };
result[sheetName] = { headers: [], rows: [] };
return;
}

const rows: GQueryRow[] = [];
const headers = valueRange.values[0].map((h) => String(h));
let rows = parseRows(headers, valueRange.values.slice(1));
const columnTypes = sheetMetadata[sheetName] || {};

// Process data rows
valueRange.values.forEach((rowData, rowIndex) => {
const row: GQueryRow = {
__meta: {
rowNum: rowIndex + 2, // +2 because we're starting from index 0 and row 1 is headers
colLength: rowData.length,
},
};

// First initialize all header fields to empty strings
rows = rows.map((row) => {
const newRow: GQueryRow = { __meta: row.__meta };
headers.forEach((header) => {
row[header] = "";
});

// Map each column value to its corresponding header
for (let j = 0; j < Math.min(rowData.length, headers.length); j++) {
const header = headers[j];
let value = rowData[j];

if (value === null || value === undefined) {
continue; // Skip processing but keep the empty string initialized earlier
}

// Apply type conversions based on metadata if available
if (columnTypes[header] && value !== "") {
const dataType = columnTypes[header];

if (dataType === "BOOLEAN") {
// Convert to boolean
if (typeof value === "string") {
value = value.toLowerCase() === "true";
}
} else if (dataType === "DATE_TIME") {
// Convert to Date object
try {
let value = row[header];
if (value !== undefined && value !== null && value !== "") {
if (columnTypes[header]) {
const dataType = columnTypes[header];
if (dataType === "BOOLEAN") {
if (typeof value === "string") {
value = value.toLowerCase() === "true";
}
} else if (dataType === "DATE_TIME") {
const dateValue = new Date(value);
if (!isNaN(dateValue.getTime())) {
value = dateValue;
}
} catch (e) {
// Keep original value if conversion fails
}
} else if (dataType === "NUMBER") {
// Convert to number
const numValue = Number(value);
if (!isNaN(numValue)) {
value = numValue;
} else if (dataType === "NUMBER") {
const numValue = Number(value);
if (!isNaN(numValue)) {
value = numValue;
}
}
}
} else {
// Try automatic type inference for common patterns
if (typeof value === "string") {
// Auto-detect booleans
} else if (typeof value === "string") {
if (
value.toLowerCase() === "true" ||
value.toLowerCase() === "false"
) {
value = value.toLowerCase() === "true";
}
// Auto-detect dates (simple pattern for dates like MM/DD/YYYY, etc.)
else if (
} else if (
/^\d{1,2}\/\d{1,2}\/\d{4}(\s\d{1,2}:\d{1,2}(:\d{1,2})?)?$/.test(
value
)
) {
try {
const dateValue = new Date(value);
if (!isNaN(dateValue.getTime())) {
value = dateValue;
}
} catch (e) {
// Keep as string if conversion fails
const dateValue = new Date(value);
if (!isNaN(dateValue.getTime())) {
value = dateValue;
}
}
}
}

row[header] = value;
}

rows.push(row);
newRow[header] = value;
});
return newRow;
});

result[sheetName] = { headers, rows };
});

// Make sure all sheets in headersMap have an entry in result
sheetsToFetch.forEach((sheet) => {
if (!result[sheet]) {
result[sheet] = {
headers: headersMap[sheet],
rows: [],
};
}
});

// Convert data types based on metadata if available
if (Object.keys(sheetMetadata).length > 0) {
Object.keys(result).forEach((sheetName) => {
const sheetResult = result[sheetName];
const metadata = sheetMetadata[sheetName];

if (sheetResult && sheetResult.rows && metadata) {
sheetResult.rows = sheetResult.rows.map((row) => {
const newRow = { ...row };

Object.keys(metadata).forEach((column) => {
const dataType = metadata[column];

// Convert based on data type
if (dataType === "NUMBER") {
newRow[column] = Number(row[column]);
} else if (dataType === "BOOLEAN") {
newRow[column] = row[column] === "TRUE";
} else if (dataType === "DATE" || dataType === "DATETIME") {
newRow[column] = new Date(row[column]);
}
// Add more conversions as needed
});

return newRow;
});
}
});
}

return result;
}

Expand Down
Loading