diff --git a/package.json b/package.json index 8c1f4a6..dddba56 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/jest": "^29.5.0", "@types/lodash": "^4.14.202", "@types/prompt-sync": "^4.2.3", + "@types/uuid": "^10.0.0", "dotenv": "^16.3.1", "esbuild": "^0.19.7", "jest": "^29.5.0", @@ -35,6 +36,7 @@ "dependencies": { "cross-fetch": "^3.1.5", "lodash": "^4.17.21", - "prompt-sync": "^4.2.0" + "prompt-sync": "^4.2.0", + "uuid": "^10.0.0" } } diff --git a/src/App.ts b/src/App.ts index 1d6addb..67aefda 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,6 +1,6 @@ import { Glide } from "./Glide"; import { Table } from "./Table"; -import type { TableProps, ColumnSchema, AppProps, IDName, AppManifest } from "./types"; +import type { TableProps, ColumnSchema, AppProps, IDName, AppManifest, Row, RowID } from "./types"; import fetch from "cross-fetch"; diff --git a/src/BigTable.ts b/src/BigTable.ts new file mode 100644 index 0000000..ddd5c3a --- /dev/null +++ b/src/BigTable.ts @@ -0,0 +1,176 @@ +import { QueryBuilder } from "./QueryBuilder"; +import { v4 as uuidv4 } from "uuid"; +import type { + TableProps, + Row, + ColumnSchema, + RowID, + FullRow, + Query, + ToSQL, + RowIdentifiable, + NullableRow, + NullableFullRow, + APITableSchema, +} from "./types"; +import { MAX_MUTATIONS } from "./constants"; +import { throwError } from "./common"; +import { Glide } from "./Glide"; +import { mapChunks } from "./Table"; +import { Stash } from "./Stash"; + +/** + * Class to interact with the Glide API v2 with functionalities reserved for Big Tables. + */ +export class BigTable { + private displayNameToName: Record, string>; + + /** + * @returns The table id. + */ + public get id(): string { + return this.props.table; + } + + /** + * @returns The display name + */ + public get name() { + return this.props.name; + } + constructor(private props: Omit, "app">, private glide: Glide) { + const { columns } = props; + this.displayNameToName = Object.fromEntries( + Object.entries(columns).map(([displayName, value]) => + typeof value !== "string" && typeof value.name === "string" + ? [displayName, value.name /* internal name */] + : [displayName, displayName] + ) + ) as Record; + this.displayNameToName["$rowID"] = "$rowID"; + } + + private renameOutgoing(rows: NullableRow[]): NullableRow[] { + const rename = this.displayNameToName; + return rows.map( + row => + Object.fromEntries( + Object.entries(row).map(([key, value]) => [ + rename[key] ?? key, + // null is sent as an empty string + value === null ? "" : value, + ]) + ) as NullableRow + ); + } + + /** + * Add a row to the table. + * + * @param row A row to add. + */ + public async add(row: Row): Promise; + + /** + * Adds rows to the table. + * + * @param rows An array of rows to add to the table. + */ + public async add(rows: Row[]): Promise; + + async add(rowOrRows: Row | Row[]): Promise { + const { table } = this.props; + + const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows]; + const renamedRows = this.renameOutgoing(rows); + + const addedIds = await mapChunks(renamedRows, MAX_MUTATIONS, async chunk => { + const response = await this.glide.post(`/tables/${table}/rows`, chunk); + await throwError(response); + + const { + data: { rowIDs }, + } = await response.json(); + return rowIDs; + }); + + const rowIDs = addedIds.flat(); + return Array.isArray(rowOrRows) ? rowIDs : rowIDs[0]; + } + + /** + * Creates a new Stash object for the BigTable. + * + * @returns The newly created Stash object. + */ + createStash(): Stash { + // const stashId: string = "20240215-job32"; + // const stashId: string = Math.random().toString(36).substring(2, 15); + // TODO: use a better stash id (for now using uuid v4 because no other stashId seems to work) + const stashId: string = uuidv4(); + return new Stash({ stashId, bigTable: this }, this.glide); + } + + /** + * Adds a Stash to the BigTable. + * + * @param stash The Stash to add. + * @returns A promise that resolves to an array of row IDs if successful, or undefined. + */ + async addStash(stash: Stash): Promise { + const response = await this.glide.post(`/tables/${this.id}/rows`, { + $stashID: stash.stashId, + }); + await throwError(response); + + const { + data: { rowIDs }, + } = await response.json(); + return rowIDs; + } + + /** + * Overwrites a row or rows in the BigTable. + * + * @param rowOrRows The row or rows to overwrite. + * @returns A promise that resolves to the row ID or an array of row IDs if successful, or undefined. + */ + async overwrite(rowOrRows: Row | Row[]): Promise { + const { table } = this.props; + + const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows]; + const renamedRows = this.renameOutgoing(rows); + + const addedIds = await mapChunks(renamedRows, MAX_MUTATIONS, async chunk => { + // TODO see if the chunk should be in the "rows" key in the docs + const response = await this.glide.put(`/tables/${table}/`, chunk); + await throwError(response); + + const { + data: { rowIDs }, + } = await response.json(); + return rowIDs; + }); + + const rowIDs = addedIds.flat(); + return Array.isArray(rowOrRows) ? rowIDs : rowIDs[0]; + } + + /** + * Overwrites a Stash in the BigTable. + * + * @param stash The Stash to overwrite. + * @returns A promise that resolves to an array of row IDs if successful, or undefined. + */ + async overwriteStash(stash: Stash): Promise { + const response = await this.glide.post(`/tables/${this.id}`, { + $stashID: stash.stashId, + }); + await throwError(response); + + const { + data: { rowIDs }, + } = await response.json(); + return rowIDs; + } +} diff --git a/src/Glide.ts b/src/Glide.ts index 791f430..3b5b67e 100644 --- a/src/Glide.ts +++ b/src/Glide.ts @@ -1,7 +1,18 @@ import { App } from "./App"; +import { BigTable } from "./BigTable"; +import { Stash } from "./Stash"; import { Table } from "./Table"; import { defaultEndpoint, defaultEndpointREST } from "./constants"; -import type { TableProps, ColumnSchema, AppProps, IDName, GlideProps, Tokened } from "./types"; +import type { + TableProps, + ColumnSchema, + AppProps, + IDName, + GlideProps, + Tokened, + Row, + RowID, +} from "./types"; import fetch from "cross-fetch"; export class Glide { @@ -58,6 +69,10 @@ export class Glide { return this.api(r, { method: "POST", body: JSON.stringify(body) }); } + public put(r: string, body: any) { + return this.api(r, { method: "PUT", body: JSON.stringify(body) }); + } + public with(props: Partial = {}) { return new Glide({ ...this.props, ...props }); } @@ -111,4 +126,70 @@ export class Glide { const apps = await this.getApps(props); return apps?.find(a => a.name === name); } + + /** + * Retrieves all big tables. + * + * @param props An optional object containing a token. + * @param props.token An optional token for authentication. + * @returns A promise that resolves to an array of tables if successful, or undefined. + */ + public async getBigTables(props: Tokened = {}): Promise { + const response = await this.with(props).get(`/tables`); + if (response.status !== 200) return undefined; + const { data: tables }: { data: IDName[] } = await response.json(); + console.log(tables); + return tables.map(t => this.bigTable({ table: t.id, name: t.name, columns: {}, ...props })); + } + + /** + * This function creates a new Table object with the provided properties. + * + * @param props The properties to create the table with. + * @returns The newly created table. + */ + public bigTable(props: Omit, "app">) { + return new BigTable(props, this.with(props)); + } + + public async addBigTable(props: { + name: string; + schema: T; + rows: Row; + }) { + const result = await this.post("/tables", props); + if (result.status != 200) return undefined; + const { data }: { data: { tableId: string; rowIDs: RowID[] } } = await result.json(); + return { + table: this.bigTable({ + columns: props.schema, + table: data.tableId, + name: props.name, + token: this.props.token, + }), + }; + } + public async addBigTableStash(props: { + name: string; + schema: T; + stash: Stash; + }) { + const result = await this.post("/tables", { + name: props.name, + schema: props.schema, + rows: { + $stashID: props.stash.stashId, + }, + }); + if (result.status != 200) return undefined; + const { data }: { data: { tableId: string; rowIDs: RowID[] } } = await result.json(); + return { + table: this.bigTable({ + columns: props.schema, + table: data.tableId, + name: props.name, + token: this.props.token, + }), + }; + } } diff --git a/src/Stash.ts b/src/Stash.ts new file mode 100644 index 0000000..a920f9d --- /dev/null +++ b/src/Stash.ts @@ -0,0 +1,36 @@ +import { BigTable } from "./BigTable"; +import { throwError } from "./common"; +import { Glide } from "./Glide"; +import { ColumnSchema, Row } from "./types"; + +type StashProps = { + stashId: string; + bigTable: BigTable; +}; + +export class Stash { + public get stashId(): string { + return this.props.stashId; + } + + indexOfLastAdd = 0; + + getSerial(): string { + return `${this.indexOfLastAdd++}`; + } + + constructor(private props: StashProps, private glide: Glide) {} + + public async add(rows: Row[]) { + const serial = this.getSerial(); + const url = `/stashes/${this.props.stashId}/${serial}`; + console.log(url); + const response = await this.glide.post(`/stashes/${this.props.stashId}/${serial}`, rows); + if (response.status !== 200) { + const text = await response.text(); + console.log(text); + throw new Error(`Error adding to stash: ${text}`); + } + await throwError(response); + } +} diff --git a/src/Table.ts b/src/Table.ts index 836723f..01d6ffe 100644 --- a/src/Table.ts +++ b/src/Table.ts @@ -21,7 +21,7 @@ import { Glide } from "./Glide"; */ export type RowOf> = T extends Table ? FullRow : never; -async function mapChunks( +export async function mapChunks( array: TItem[], chunkSize: number, work: (chunk: TItem[]) => Promise diff --git a/src/constants.ts b/src/constants.ts index ab274e4..9341c61 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ export const defaultEndpoint = "https://api.glideapp.io/api/function"; -export const defaultEndpointREST = "https://functions.prod.internal.glideapps.com/api"; +export const defaultEndpointREST = "https://api.glideapps.com/"; export const MAX_MUTATIONS = 500;