262 lines
7.1 KiB
TypeScript
262 lines
7.1 KiB
TypeScript
|
import { Types as T } from "@lingdocs/inflect";
|
||
|
import * as FT from "../../website/src/types/functions-types";
|
||
|
import { standardizeEntry } from "@lingdocs/inflect";
|
||
|
import type { sheets_v4 } from "@googleapis/sheets";
|
||
|
import {
|
||
|
dictionaryEntryBooleanFields,
|
||
|
dictionaryEntryNumberFields,
|
||
|
dictionaryEntryTextFields,
|
||
|
simplifyPhonetics,
|
||
|
standardizePashto,
|
||
|
} from "@lingdocs/inflect";
|
||
|
|
||
|
const validFields = [
|
||
|
...dictionaryEntryTextFields,
|
||
|
...dictionaryEntryBooleanFields,
|
||
|
...dictionaryEntryNumberFields,
|
||
|
];
|
||
|
|
||
|
export type Sheets = {
|
||
|
spreadsheetId: string;
|
||
|
spreadsheets: sheets_v4.Resource$Spreadsheets;
|
||
|
};
|
||
|
|
||
|
async function getTsIndex(sheets: Sheets): Promise<number[]> {
|
||
|
const values = await getRange(sheets, "A2:A");
|
||
|
return values.map((r) => parseInt(r[0]));
|
||
|
}
|
||
|
|
||
|
async function getFirstEmptyRow(sheets: Sheets): Promise<number> {
|
||
|
const values = await getRange(sheets, "A2:A");
|
||
|
return values.length + 2;
|
||
|
}
|
||
|
|
||
|
export async function getEntriesFromSheet({
|
||
|
spreadsheets,
|
||
|
spreadsheetId,
|
||
|
}: Sheets): Promise<T.DictionaryEntry[]> {
|
||
|
const keyInfo = await getKeyInfo({ spreadsheets, spreadsheetId });
|
||
|
const { data } = await spreadsheets.values.get({
|
||
|
spreadsheetId,
|
||
|
range: `A2:${keyInfo.lastCol}`,
|
||
|
});
|
||
|
if (!data.values) {
|
||
|
throw new Error("data not found");
|
||
|
}
|
||
|
function processRow(row: string[]) {
|
||
|
// TODO: optimize this
|
||
|
const processedRow = row.flatMap<
|
||
|
[keyof T.DictionaryEntry, string | boolean | number]
|
||
|
>((x, i) => {
|
||
|
if (x === "") {
|
||
|
return [];
|
||
|
}
|
||
|
const k = keyInfo.keyRow[i];
|
||
|
// @ts-expect-error
|
||
|
if (dictionaryEntryNumberFields.includes(k)) {
|
||
|
return [[k, parseInt(x)]];
|
||
|
}
|
||
|
// @ts-expect-error
|
||
|
if (dictionaryEntryBooleanFields.includes(k)) {
|
||
|
return [[k, x.toLowerCase() === "true"]];
|
||
|
}
|
||
|
return [[k, k.endsWith("p") ? standardizePashto(x.trim()) : x.trim()]];
|
||
|
});
|
||
|
return processedRow;
|
||
|
}
|
||
|
const entries = data.values.map(processRow).map((pr) => {
|
||
|
return Object.fromEntries(pr) as T.DictionaryEntry;
|
||
|
});
|
||
|
entries.sort((a, b) => a.p.localeCompare(b.p, "ps"));
|
||
|
const entriesLength = entries.length;
|
||
|
// add index and g
|
||
|
for (let i = 0; i < entriesLength; i++) {
|
||
|
entries[i].i = i;
|
||
|
entries[i].g = simplifyPhonetics(entries[i].f);
|
||
|
}
|
||
|
return entries;
|
||
|
}
|
||
|
|
||
|
export async function updateDictionaryEntries(
|
||
|
{ spreadsheets, spreadsheetId }: Sheets,
|
||
|
edits: FT.EntryEdit[]
|
||
|
) {
|
||
|
if (edits.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
const entries = edits.map((e) => e.entry);
|
||
|
const tsIndex = await getTsIndex({ spreadsheets, spreadsheetId });
|
||
|
const { keyRow, lastCol } = await getKeyInfo({ spreadsheets, spreadsheetId });
|
||
|
function entryToRowArray(e: T.DictionaryEntry): any[] {
|
||
|
return keyRow.slice(1).map((k) => e[k] || "");
|
||
|
}
|
||
|
const data = entries.flatMap((entry) => {
|
||
|
const rowNum = getRowNumFromTs(tsIndex, entry.ts);
|
||
|
if (rowNum === undefined) {
|
||
|
console.error(`couldn't find ${entry.ts} ${JSON.stringify(entry)}`);
|
||
|
return [];
|
||
|
}
|
||
|
const values = [entryToRowArray(entry)];
|
||
|
return [
|
||
|
{
|
||
|
q: `B${rowNum}:${lastCol}${rowNum}`,
|
||
|
values,
|
||
|
},
|
||
|
];
|
||
|
});
|
||
|
await spreadsheets.values.batchUpdate({
|
||
|
spreadsheetId,
|
||
|
requestBody: {
|
||
|
data,
|
||
|
valueInputOption: "RAW",
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
export async function addDictionaryEntries(
|
||
|
{ spreadsheets, spreadsheetId }: Sheets,
|
||
|
additions: FT.NewEntry[]
|
||
|
) {
|
||
|
if (additions.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
const entries = additions.map((x) => standardizeEntry(x.entry));
|
||
|
const endRow = await getFirstEmptyRow({ spreadsheets, spreadsheetId });
|
||
|
const { keyRow, lastCol } = await getKeyInfo({ spreadsheets, spreadsheetId });
|
||
|
const ts = Date.now();
|
||
|
function entryToRowArray(e: T.DictionaryEntry): any[] {
|
||
|
return keyRow.slice(1).map((k) => e[k] || "");
|
||
|
}
|
||
|
const values = entries.map((entry, i) => [ts + i, ...entryToRowArray(entry)]);
|
||
|
await spreadsheets.values.batchUpdate({
|
||
|
spreadsheetId,
|
||
|
requestBody: {
|
||
|
data: [
|
||
|
{
|
||
|
range: `A${endRow}:${lastCol}${endRow + (values.length - 1)}`,
|
||
|
values,
|
||
|
},
|
||
|
],
|
||
|
valueInputOption: "RAW",
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
export async function updateDictionaryFields(
|
||
|
{ spreadsheets, spreadsheetId }: Sheets,
|
||
|
edits: { ts: number; col: keyof T.DictionaryEntry; val: any }[]
|
||
|
) {
|
||
|
const tsIndex = await getTsIndex({ spreadsheets, spreadsheetId });
|
||
|
const { colMap } = await getKeyInfo({ spreadsheets, spreadsheetId });
|
||
|
const data = edits.flatMap((edit) => {
|
||
|
const rowNum = getRowNumFromTs(tsIndex, edit.ts);
|
||
|
if (rowNum === undefined) {
|
||
|
console.error(`couldn't find ${edit.ts} ${JSON.stringify(edit)}`);
|
||
|
return [];
|
||
|
}
|
||
|
const col = colMap[edit.col];
|
||
|
return [
|
||
|
{
|
||
|
range: `${col}${rowNum}:${col}${rowNum}`,
|
||
|
values: [[edit.val]],
|
||
|
},
|
||
|
];
|
||
|
});
|
||
|
await spreadsheets.values.batchUpdate({
|
||
|
spreadsheetId,
|
||
|
requestBody: {
|
||
|
data,
|
||
|
valueInputOption: "RAW",
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
export async function deleteEntry(
|
||
|
{ spreadsheets, spreadsheetId }: Sheets,
|
||
|
sheetId: number,
|
||
|
ed: FT.EntryDeletion
|
||
|
) {
|
||
|
const tsIndex = await getTsIndex({ spreadsheets, spreadsheetId });
|
||
|
const row = getRowNumFromTs(tsIndex, ed.ts);
|
||
|
if (!row) {
|
||
|
console.error(`${ed.ts} not found to do delete`);
|
||
|
return;
|
||
|
}
|
||
|
const requests = [
|
||
|
{
|
||
|
deleteDimension: {
|
||
|
range: {
|
||
|
sheetId,
|
||
|
dimension: "ROWS",
|
||
|
startIndex: row - 1,
|
||
|
endIndex: row,
|
||
|
},
|
||
|
},
|
||
|
},
|
||
|
];
|
||
|
await spreadsheets.batchUpdate({
|
||
|
spreadsheetId,
|
||
|
requestBody: {
|
||
|
requests,
|
||
|
includeSpreadsheetInResponse: false,
|
||
|
responseRanges: [],
|
||
|
},
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function getRowNumFromTs(tsIndex: number[], ts: number): number | undefined {
|
||
|
const res = tsIndex.findIndex((x) => x === ts);
|
||
|
if (res === -1) {
|
||
|
return undefined;
|
||
|
}
|
||
|
return res + 2;
|
||
|
}
|
||
|
|
||
|
async function getKeyInfo(sheets: Sheets): Promise<{
|
||
|
colMap: Record<keyof T.DictionaryEntry, string>;
|
||
|
colMapN: Record<keyof T.DictionaryEntry, number>;
|
||
|
keyRow: (keyof T.DictionaryEntry)[];
|
||
|
lastCol: string;
|
||
|
}> {
|
||
|
const headVals = await getRange(sheets, "A1:1");
|
||
|
const headRow: string[] = headVals[0];
|
||
|
const colMap: Record<any, string> = {};
|
||
|
const colMapN: Record<any, number> = {};
|
||
|
headRow.forEach((c, i) => {
|
||
|
if (validFields.every((v) => c !== v)) {
|
||
|
throw new Error(`Invalid spreadsheet field ${c}`);
|
||
|
}
|
||
|
colMap[c] = getColumnLetters(i);
|
||
|
colMapN[c] = i;
|
||
|
});
|
||
|
return {
|
||
|
colMap: colMap as Record<keyof T.DictionaryEntry, string>,
|
||
|
colMapN: colMapN as Record<keyof T.DictionaryEntry, number>,
|
||
|
keyRow: headRow as (keyof T.DictionaryEntry)[],
|
||
|
lastCol: getColumnLetters(headRow.length - 1),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
async function getRange(
|
||
|
{ spreadsheets, spreadsheetId }: Sheets,
|
||
|
range: string
|
||
|
): Promise<any[][]> {
|
||
|
const { data } = await spreadsheets.values.get({
|
||
|
spreadsheetId,
|
||
|
range,
|
||
|
});
|
||
|
if (!data.values) {
|
||
|
throw new Error("data not found");
|
||
|
}
|
||
|
return data.values;
|
||
|
}
|
||
|
|
||
|
function getColumnLetters(num: number) {
|
||
|
let letters = "";
|
||
|
while (num >= 0) {
|
||
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num % 26] + letters;
|
||
|
num = Math.floor(num / 26) - 1;
|
||
|
}
|
||
|
return letters;
|
||
|
}
|