Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(sdk) add helper to create studio project aliases #48

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
35 changes: 35 additions & 0 deletions packages/sdk/src/helpers/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { api as studioApi } from "@tableland/studio-client";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @tableland/studio-client package is not published yet. Anyone wanting to try out this feature will have to npm link. Linking between to two monorepos is tricky. I can help with that via voice if needed.

import { type WaitableTransactionReceipt } from "../registry/utils.js";
import { type FetchConfig } from "../validator/client/index.js";
import { type ChainName, getBaseUrl } from "./chains.js";
Expand Down Expand Up @@ -116,6 +117,40 @@ export function jsonFileAliases(filepath: string): AliasesNameMap {
};
}

// NOTE: In the future we may need to use `environmentId` instead of `projectId`, but
// there is currently no concept of an environment for a user, and the api doesn't
// support querying for deployments based on environment.
export function studioAliases(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the meat of the feature. The studio project name map is loaded and used for read and insert only.

projectId: string,
apiUrl?: string
): AliasesNameMap {
const api = studioApi({
url: apiUrl,
});
const loadMap = async function (): Promise<void> {
const res = await api.deployments.projectDeployments.query({ projectId });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works now because there is a single environment called default. Once there are multiple environments, you'll end up with duplicate aliases in the map. Actually they will just overwrite each other and the map will end up with a mix of tables from different environments.

The root of this problem is using projectId as the parameter. We should have the user pass in the environment id. We can call it something opaque sounding like key or whatever since "environment id" isn't a user facing concept yet.

If we do that, we can create a new api function to return deployments for an environment id, then the map will be accurate.


_map = {};
// map the response to a `NameMapping` Object
// { tokenId: string; tableId: string; tableName: string; environmentId: string; chainId: number; blockNumber: number | null; txnHash: string | null; createdAt: string; }
res.forEach(function (row) {
_map[row.tableName.split("_").slice(0, -2).join("_")] = row.tableName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not quite right. You're keying the map based on the Tableland table name. We want to key it on the Studio Table name in the Blueprint. It is true that these should be the same, but if we introduce the ability to rename a Studio Table, they will be different. They can also be different for imported tables because the user chooses the Studio table name for the imported Tableland table.

This can be easily addressed when we implement the deploymentsForEnvId function mentioned in the above comment... we can join against the tables table to get the Studio table name.

});
};

let _map: NameMapping;
return {
read: async function (): Promise<NameMapping> {
if (typeof _map === "undefined") await loadMap();

return _map;
},
write: async function () {
throw new Error("cannot create project tables via studio sdk aliases");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The write function may never be used since creating tables must be done via Studio

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect.

},
};
}

export function prepReadConfig(config: Partial<ReadConfig>): FetchConfig {
const conf: FetchConfig = {};
if (config.apiKey) {
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {
extractSigner,
jsonFileAliases,
prepReadConfig,
studioAliases,
} from "./config.js";
export {
type Signer,
Expand Down
78 changes: 76 additions & 2 deletions packages/sdk/test/aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import url from "node:url";
import path from "node:path";
import fs from "node:fs";
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { strictEqual, rejects } from "assert";
import { deepStrictEqual, equal, strictEqual, rejects } from "assert";
import { describe, test } from "mocha";
import { Wallet } from "ethers";
import { getAccounts } from "@tableland/local";
import {
type NameMapping,
getDefaultProvider,
jsonFileAliases,
studioAliases,
} from "../src/helpers/index.js";
import { Database } from "../src/index.js";
import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup";
Expand All @@ -17,7 +19,7 @@ import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

describe("aliases", function () {
this.timeout(TEST_TIMEOUT_FACTOR * 10000);
this.timeout(TEST_TIMEOUT_FACTOR * 30000);
// Note that we're using the second account here
const [, wallet] = getAccounts();
const provider = getDefaultProvider(TEST_PROVIDER_URL);
Expand Down Expand Up @@ -232,4 +234,76 @@ describe("aliases", function () {
strictEqual(nameMap[tablePrefix], uuTableName);
});
});

describe.skip("studio based aliases", function () {
// TODO: these values are set per the actual production studio. It's tempting
// to sandbox this, but doing so would add significant complexity.
// TODO: move the argument values to env vars.
const aliases = studioAliases(
"6f254b66-d9cf-482b-a4b9-76cfe5eb2f19",
"https://studio-neon.vercel.app/"
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usage with the SDK is fairly straight forward. Just provide a projectId and the URL of the studio. We can remove the requirement for studio url once we have a permanent url.


const wallet = new Wallet(process.env.PRIVATE_KEY as string);
const provider = getDefaultProvider(process.env.PROVIDER_URL as string);
const signer = wallet.connect(provider);
const db = new Database({
signer,
aliases,
autoWait: true,
});

const getNextId = async function (): Promise<number> {
const res = await db
.prepare("select * from users ORDER BY id DESC LIMIT 1;")
.all();

// @ts-expect-error TODO
const nextId = res.results[0]?.id;

if (typeof nextId !== "number" || isNaN(nextId)) {
return 0;
}

return nextId;
};

test("can use the production studio to do reads", async function () {
const res = await db
.prepare("select * from users ORDER BY id DESC LIMIT 1;")
.all();

// @ts-expect-error TODO
deepStrictEqual(res.results[0].full_name, "Bobby Tables");
// @ts-expect-error TODO
deepStrictEqual(res.results[0].favorite_color, "blue");
equal(res.success, true);
equal(typeof res.meta.duration, "number");
});

test("can use the production studio to do inserts", async function () {
const nextId = await getNextId();
await db
.prepare(
`INSERT INTO users (id, full_name, favorite_color) VALUES (${
nextId + 1
}, 'Bobby Tables', 'blue');`
)
.all();

const res = await db
.prepare("select * from users ORDER BY id DESC LIMIT 1;")
.all();

deepStrictEqual(res.results, [
{
full_name: "Bobby Tables",
favorite_color: "blue",
id: nextId + 1,
},
]);
equal(res.success, true);
equal(typeof res.meta.duration, "number");
});
});
});
Loading