From 0df11ad8efec99c6b3cf2e7bc5efc6ef77adc5f5 Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 21:12:38 -0800 Subject: [PATCH 1/8] Support new schema endpoint --- package-lock.json | 13 +++++++++++++ package.json | 1 + src/index.ts | 20 ++++++++++++++++++++ src/rest.ts | 20 ++++++++++++++++++++ src/table.test.ts | 9 +++++++++ 5 files changed, 63 insertions(+) create mode 100644 src/rest.ts diff --git a/package-lock.json b/package-lock.json index d1c6814..cbe15dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@swc/core": "^1.3.44", "@swc/jest": "^0.2.24", "@types/jest": "^29.5.0", + "dotenv": "^16.3.1", "jest": "^29.5.0", "prettier": "^2.8.7", "semver": "^7.3.8", @@ -2200,6 +2201,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.348", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.348.tgz", diff --git a/package.json b/package.json index d15a98b..ffb9dfe 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@swc/core": "^1.3.44", "@swc/jest": "^0.2.24", "@types/jest": "^29.5.0", + "dotenv": "^16.3.1", "jest": "^29.5.0", "prettier": "^2.8.7", "semver": "^7.3.8", diff --git a/src/index.ts b/src/index.ts index 8eb06d4..852b70d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { Client, makeClient } from "./rest"; import type { TableProps, Row, ColumnSchema, RowID, FullRow, AppProps } from "./types"; import fetch from "cross-fetch"; @@ -18,6 +19,8 @@ const defaultEndpoint = "https://api.glideapp.io/api/function"; class Table { private props: TableProps; + private client: Client; + public get app(): string { return this.props.app; } @@ -31,6 +34,9 @@ class Table { token: process.env.GLIDE_TOKEN, ...props, }; + this.client = makeClient({ + token: process.env.GLIDE_TOKEN!, + }); } private renameOutgoing(rows: Row[]): Row[] { @@ -159,6 +165,20 @@ class Table { await this.deleteRows([row]); } + public async getSchema(): Promise<{ + data: { columns: Array<{ id: string; name: string; type: { kind: string } }> }; + }> { + const { app, table } = this.props; + + const response = await this.client.get(`/apps/${app}/tables/${table}/schema`); + + if (response.status !== 200) { + throw new Error(`Failed to get schema: ${response.status} ${response.statusText}`); + } + + return await response.json(); + } + public async getRows(): Promise[]> { const { token, app, table } = this.props; diff --git a/src/rest.ts b/src/rest.ts new file mode 100644 index 0000000..8ed048b --- /dev/null +++ b/src/rest.ts @@ -0,0 +1,20 @@ +import fetch from "cross-fetch"; + +export function makeClient({ token }: { token: string }) { + return { + get(route: string, r: RequestInit = {}) { + return fetch(`https://functions.prod.internal.glideapps.com/api${route}`, { + method: "GET", + ...r, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + Accept: "application/json", + ...r.headers, + }, + }); + }, + }; +} + +export type Client = ReturnType; diff --git a/src/table.test.ts b/src/table.test.ts index 036653b..3ab4cb0 100644 --- a/src/table.test.ts +++ b/src/table.test.ts @@ -1,3 +1,5 @@ +require("dotenv").config(); + import * as glide from "."; import type { RowOf } from "."; @@ -101,4 +103,11 @@ describe("table", () => { const renamed = await inventory.getRow(rowID); expect(renamed?.Item).toBe("Renamed"); }); + + it("can get schema", async () => { + const { + data: { columns }, + } = await inventory.getSchema(); + expect(columns).toBeTruthy(); + }); }); From 117dde96eb23a3a21f0d4ec05d1104728f9f0b5d Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 21:15:40 -0800 Subject: [PATCH 2/8] README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7bdc038..39be5fc 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ await inventory.setRow(rowID, { // Delete a row await inventory.deleteRow(rowID); + +// Get table schema info (columns and their types) +const schema = await inventory.getSchema(); ``` ### Staging From e65f525a05d6ec8f9cc5e6e30509cb50ad53ec4c Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 21:39:00 -0800 Subject: [PATCH 3/8] Get tables, including by name --- src/index.ts | 27 ++++++++++++++++++++++++++- src/table.test.ts | 11 +++++++++++ src/types.ts | 3 ++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 852b70d..93cd022 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,10 @@ class Table { return this.props.table; } + public get name() { + return this.props.name; + } + constructor(props: TableProps) { this.props = { token: process.env.GLIDE_TOKEN, @@ -209,7 +213,28 @@ class Table { } class App { - constructor(private props: AppProps) {} + private client: Client; + + constructor(private props: AppProps) { + this.client = makeClient({ + token: process.env.GLIDE_TOKEN!, + }); + } + + public async getTableNamed(name: string) { + const tables = await this.getTables(); + return tables?.find(t => t.name === name); + } + + public async getTables() { + const { id } = this.props; + const result = await this.client.get(`/apps/${id}/tables`); + + if (result.status !== 200) return undefined; + + const { data: tables }: { data: Array<{ id: string; name: string }> } = await result.json(); + return tables.map(t => this.table({ table: t.id, name: t.name, columns: {} })); + } public table(props: Omit, "app">) { return new Table({ diff --git a/src/table.test.ts b/src/table.test.ts index 3ab4cb0..bc431b9 100644 --- a/src/table.test.ts +++ b/src/table.test.ts @@ -110,4 +110,15 @@ describe("table", () => { } = await inventory.getSchema(); expect(columns).toBeTruthy(); }); + + it.only("can get tables", async () => { + const tables = await app.getTables(); + expect(tables).toBeDefined(); + expect(tables?.length).toBeGreaterThan(0); + }); + + it.only("can get a table by name", async () => { + const table = await app.getTableNamed("Inv - Inventory"); + expect(table).toBeDefined(); + }); }); diff --git a/src/types.ts b/src/types.ts index a1eea74..f43272b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,9 +47,10 @@ export type FullRow = Pretty< } & Row >; -export interface TableProps { +export interface TableProps { token?: string; endpoint?: string; + name?: string; app: string; table: string; From 7e76eb201ae40cbd6a6e8e86b57c7ef163524301 Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 21:41:36 -0800 Subject: [PATCH 4/8] README --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39be5fc..66fde39 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ # Glide Tables Client -## Usage +## Apps + +```ts +import * as glide from "@glideapps/tables"; + +const myApp = glide.app({ + token: process.env.GLIDE_TOKEN, + id: "bAFxpGXU1bHiBgUMcDgn", +}); + +// Get all tables +const tables = await myApp.getTables(); + +// Get a table by name +const users = await myApp.getTableNamed("Users); +``` + +## Tables ```ts import * as glide from "@glideapps/tables"; From ece40ef322232f6897ba515de758bd2d8ee710ac Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 21:42:13 -0800 Subject: [PATCH 5/8] Tests --- src/table.test.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/table.test.ts b/src/table.test.ts index bc431b9..03b89a1 100644 --- a/src/table.test.ts +++ b/src/table.test.ts @@ -27,6 +27,19 @@ const inventoryStaging = glide.table({ }, }); +describe("app", () => { + it("can get tables", async () => { + const tables = await app.getTables(); + expect(tables).toBeDefined(); + expect(tables?.length).toBeGreaterThan(0); + }); + + it("can get a table by name", async () => { + const table = await app.getTableNamed("Inv - Inventory"); + expect(table).toBeDefined(); + }); +}); + describe("table", () => { jest.setTimeout(60_000); @@ -110,15 +123,4 @@ describe("table", () => { } = await inventory.getSchema(); expect(columns).toBeTruthy(); }); - - it.only("can get tables", async () => { - const tables = await app.getTables(); - expect(tables).toBeDefined(); - expect(tables?.length).toBeGreaterThan(0); - }); - - it.only("can get a table by name", async () => { - const table = await app.getTableNamed("Inv - Inventory"); - expect(table).toBeDefined(); - }); }); From 8d5b5f45f9bf9785ed4771c306b1ac3a41efaf77 Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 22:01:18 -0800 Subject: [PATCH 6/8] Get apps by name as well --- README.md | 18 +++++++++++++----- src/index.ts | 29 +++++++++++++++++++++++++++-- src/rest.ts | 2 +- src/table.test.ts | 15 ++++++++++++++- src/types.ts | 1 + 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 66fde39..9bc209b 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,28 @@ # Glide Tables Client +## Authorization + +Set `GLIDE_TOKEN` environment variable to your Glide token, or pass the token inline as props. + ## Apps ```ts import * as glide from "@glideapps/tables"; -const myApp = glide.app({ - token: process.env.GLIDE_TOKEN, - id: "bAFxpGXU1bHiBgUMcDgn", -}); +// Create an app with its ID +const myApp = glide.app("bAFxpGXU1bHiBgUMcDgn"); + +// Or get by name +const myApp = await glide.getAppNamed("Employee Directory"); // Get all tables const tables = await myApp.getTables(); // Get a table by name -const users = await myApp.getTableNamed("Users); +const users = await myApp.getTableNamed("Users"); + +// List all apps +const apps = await glide.getApps(); ``` ## Tables diff --git a/src/index.ts b/src/index.ts index 93cd022..189ac67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import fetch from "cross-fetch"; type RowIdentifiable = RowID | FullRow; +type IDName = { id: string; name: string }; + function rowID(row: RowIdentifiable): RowID { return typeof row === "string" ? row : row.$rowID; } @@ -215,6 +217,10 @@ class Table { class App { private client: Client; + public get name() { + return this.props.name; + } + constructor(private props: AppProps) { this.client = makeClient({ token: process.env.GLIDE_TOKEN!, @@ -232,7 +238,7 @@ class App { if (result.status !== 200) return undefined; - const { data: tables }: { data: Array<{ id: string; name: string }> } = await result.json(); + const { data: tables }: { data: IDName[] } = await result.json(); return tables.map(t => this.table({ table: t.id, name: t.name, columns: {} })); } @@ -246,10 +252,29 @@ class App { } } -export function app(props: AppProps): App { +export function app(props: AppProps | string): App { + if (typeof props === "string") { + props = { id: props }; + } return new App(props); } +export async function getApps(props: { token?: string } = {}): Promise { + const client = makeClient(props); + const response = await client.get(`/apps`); + if (response.status !== 200) return undefined; + const { data: apps }: { data: IDName[] } = await response.json(); + return apps.map(idName => app({ ...props, ...idName })); +} + +export async function getAppNamed( + name: string, + props: { token?: string } = {} +): Promise { + const apps = await getApps(props); + return apps?.find(a => a.name === name); +} + export function table(props: TableProps) { return new Table(props); } diff --git a/src/rest.ts b/src/rest.ts index 8ed048b..fc6b4b8 100644 --- a/src/rest.ts +++ b/src/rest.ts @@ -1,6 +1,6 @@ import fetch from "cross-fetch"; -export function makeClient({ token }: { token: string }) { +export function makeClient({ token = process.env.GLIDE_TOKEN! }: { token?: string } = {}) { return { get(route: string, r: RequestInit = {}) { return fetch(`https://functions.prod.internal.glideapps.com/api${route}`, { diff --git a/src/table.test.ts b/src/table.test.ts index 03b89a1..e92246d 100644 --- a/src/table.test.ts +++ b/src/table.test.ts @@ -3,9 +3,11 @@ require("dotenv").config(); import * as glide from "."; import type { RowOf } from "."; +const token = process.env.GLIDE_TOKEN!; + const app = glide.app({ id: "bAFxpGXU1bHiBgUMcDgn", - token: process.env.GLIDE_TOKEN, + token, }); const inventory = app.table({ @@ -28,6 +30,17 @@ const inventoryStaging = glide.table({ }); describe("app", () => { + it("can get apps", async () => { + const apps = await glide.getApps(); + expect(apps).toBeDefined(); + expect(apps?.length).toBeGreaterThan(0); + }); + + it("can get an app by name", async () => { + const app = await glide.getAppNamed("API Testing"); + expect(app).toBeDefined(); + }); + it("can get tables", async () => { const tables = await app.getTables(); expect(tables).toBeDefined(); diff --git a/src/types.ts b/src/types.ts index f43272b..5869086 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,4 +61,5 @@ export interface AppProps { id: string; token?: string; endpoint?: string; + name?: string; } From 7a88c6491f02874a47ce0e09917b7c7fa5e97fde Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 22:03:57 -0800 Subject: [PATCH 7/8] docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9bc209b..877ea6c 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ ## Authorization -Set `GLIDE_TOKEN` environment variable to your Glide token, or pass the token inline as props. +Set `GLIDE_TOKEN` environment variable or pass the token as props. ## Apps ```ts import * as glide from "@glideapps/tables"; -// Create an app with its ID +// Create a reference to an app using its ID const myApp = glide.app("bAFxpGXU1bHiBgUMcDgn"); // Or get by name From 1a7e0ab79f9b929dc4db95b79dcec01ad4439945 Mon Sep 17 00:00:00 2001 From: David Siegel Date: Wed, 15 Nov 2023 22:35:38 -0800 Subject: [PATCH 8/8] names and care --- src/index.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 189ac67..8eb75f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,10 @@ class Table { return this.props.app; } + public get id(): string { + return this.props.table; + } + public get table(): string { return this.props.table; } @@ -37,8 +41,8 @@ class Table { constructor(props: TableProps) { this.props = { - token: process.env.GLIDE_TOKEN, ...props, + token: props.token ?? process.env.GLIDE_TOKEN, }; this.client = makeClient({ token: process.env.GLIDE_TOKEN!, @@ -201,7 +205,12 @@ class Table { }); if (!response.ok) { - throw new Error(`Failed to get rows: ${response.status} ${response.statusText}`); + throw new Error( + `Failed to get rows: ${response.status} ${response.statusText} ${JSON.stringify({ + app, + table, + })}` + ); } const [result] = await response.json(); @@ -215,13 +224,19 @@ class Table { } class App { + private props: AppProps; private client: Client; + public get id() { + return this.props.id; + } + public get name() { return this.props.name; } - constructor(private props: AppProps) { + constructor(props: AppProps) { + this.props = { ...props, token: props.token ?? process.env.GLIDE_TOKEN! }; this.client = makeClient({ token: process.env.GLIDE_TOKEN!, });