diff --git a/.gitignore b/.gitignore index 8683d114..f00a0560 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ packages/local/validator/*.db* packages/local/validator/backups # registry artifacts -packages/local/registry/.openzeppelin/ \ No newline at end of file +packages/local/registry/.openzeppelin/ + +### cli ### +packages/cli/tableland.aliases.json diff --git a/packages/cli/.env.example b/packages/cli/.env.example index 1637c784..cdd37c90 100644 --- a/packages/cli/.env.example +++ b/packages/cli/.env.example @@ -1,3 +1,4 @@ TBL_PRIVATE_KEY=fillme TBL_CHAIN=fillme TBL_PROVIDER_URL=fillme +TBL_ALIASES=fillme diff --git a/packages/cli/README.md b/packages/cli/README.md index 997695f3..9242a165 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -52,7 +52,7 @@ Options: --version Show version number [boolean] -k, --privateKey Private key string [string] -c, --chain The EVM chain to target [string] [default: "maticmum"] - -p, --providerUrl JSON RPC API provider URL. (e.g., https://eth-rinkeby.alche + -p, --providerUrl JSON RPC API provider URL. (e.g., https://eth-sepolia.alche myapi.io/v2/123abc123a...) [string] ``` diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b186d38c..01ce109e 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -46,6 +46,7 @@ export interface GlobalOptions { verbose: boolean; ensProviderUrl?: string; enableEnsExperiment?: boolean; + aliases?: string; } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -59,7 +60,7 @@ const _argv = yargs(hideBin(process.argv)) .env("TBL") .config(config?.config) // the help and version options are internal to yargs, hence they are - // at the top of the help message no matter what order we specifiy + // at the top of the help message no matter what order we specify .option("help", { alias: "h", }) @@ -91,7 +92,13 @@ const _argv = yargs(hideBin(process.argv)) alias: "p", type: "string", description: - "JSON RPC API provider URL. (e.g., https://eth-rinkeby.alchemyapi.io/v2/123abc123a...)", + "JSON RPC API provider URL (e.g., https://eth-sepolia.g.alchemy.com/v2/123abc123a...)", + }) + .option("aliases", { + alias: "a", + type: "string", + description: + "Path to table aliases JSON file (e.g., ./tableland.aliases.json)", }) .demandCommand(1, "") .strict().argv; diff --git a/packages/cli/src/commands/controller.ts b/packages/cli/src/commands/controller.ts index 261df842..6239dbf1 100644 --- a/packages/cli/src/commands/controller.ts +++ b/packages/cli/src/commands/controller.ts @@ -1,7 +1,9 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; import { Registry } from "@tableland/sdk"; +import { init } from "@tableland/sqlparser"; import { + getTableNameWithAlias, getWalletWithProvider, getLink, logger, @@ -28,11 +30,13 @@ export const builder: CommandBuilder, Options> = ( (yargs) => yargs.positional("name", { type: "string", - description: "The target table name", + description: "The target table name (or alias, if enabled)", }) as yargs.Argv, async (argv) => { - const { name, privateKey, providerUrl } = argv; + await init(); + const { privateKey, providerUrl } = argv; const chain = getChainName(argv.chain); + const name = await getTableNameWithAlias(argv.aliases, argv.name); try { const signer = await getWalletWithProvider({ @@ -40,8 +44,8 @@ export const builder: CommandBuilder, Options> = ( chain, providerUrl, }); - const reg = new Registry({ signer }); + const reg = new Registry({ signer }); const res = await reg.getController(name); logger.log(res); @@ -62,11 +66,13 @@ export const builder: CommandBuilder, Options> = ( }) .positional("name", { type: "string", - description: "The target table name", + description: "The target table name (or alias, if enabled)", }) as yargs.Argv, async (argv) => { - const { name, controller, privateKey, providerUrl } = argv; + await init(); + const { controller, privateKey, providerUrl } = argv; const chain = getChainName(argv.chain); + const name = await getTableNameWithAlias(argv.aliases, argv.name); try { const signer = await getWalletWithProvider({ @@ -93,11 +99,13 @@ export const builder: CommandBuilder, Options> = ( (yargs) => yargs.positional("name", { type: "string", - description: "The target table name", + description: "The target table name (or alias, if enabled)", }) as yargs.Argv, async (argv) => { - const { name, privateKey, providerUrl } = argv; + await init(); + const { privateKey, providerUrl } = argv; const chain = getChainName(argv.chain); + const name = await getTableNameWithAlias(argv.aliases, argv.name); try { const signer = await getWalletWithProvider({ @@ -107,7 +115,6 @@ export const builder: CommandBuilder, Options> = ( }); const reg = new Registry({ signer }); - const res = await reg.lockController(name); const link = getLink(chain, res.hash); diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts index 9a603260..a1f8c0c6 100644 --- a/packages/cli/src/commands/info.ts +++ b/packages/cli/src/commands/info.ts @@ -1,8 +1,9 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; +import { init } from "@tableland/sqlparser"; import { type GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; -import { logger } from "../utils.js"; +import { getTableNameWithAlias, logger } from "../utils.js"; export interface Options extends GlobalOptions { name: string; @@ -20,10 +21,21 @@ export const builder: CommandBuilder, Options> = ( }) as yargs.Argv; export const handler = async (argv: Arguments): Promise => { + await init(); try { - let { name } = argv; - const [tableId, chainId] = name.split("_").reverse(); + let name = await getTableNameWithAlias(argv.aliases, argv.name); + + // Check if the passed `name` uses ENS + // Note: duplicative `setupCommand` calls will occur with ENS, but this is + // required to properly parse the chainId from the table name + if (argv.enableEnsExperiment != null && argv.ensProviderUrl != null) { + const { ens } = await setupCommand({ + ...argv, + }); + if (ens != null) name = await ens.resolveTable(name); + } + const [tableId, chainId] = name.split("_").reverse(); const parts = name.split("_"); if (parts.length < 3 && argv.enableEnsExperiment == null) { @@ -33,18 +45,15 @@ export const handler = async (argv: Arguments): Promise => { return; } - const { ens, validator } = await setupCommand({ + const { validator } = await setupCommand({ ...argv, chain: parseInt(chainId) as any, }); - /* c8 ignore next 3 */ - if (argv.enableEnsExperiment != null && ens != null) { - name = await ens.resolveTable(name); - } - + // Get the table ID, now that the name comes from either an alias, ENS, or + // the standard naming convention const res = await validator.getTableById({ - tableId, + tableId: tableId.toString(), chainId: parseInt(chainId), }); logger.log(JSON.stringify(res)); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 036ce347..044d38fc 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,10 +1,15 @@ import { resolve, dirname } from "path"; -import { mkdirSync, createWriteStream, type WriteStream } from "fs"; +import { + mkdirSync, + createWriteStream, + type WriteStream, + writeFileSync, +} from "fs"; import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; import yaml from "js-yaml"; import inquirer from "inquirer"; -import { getChains, logger } from "../utils.js"; +import { getChains, logger, checkAliasesPath } from "../utils.js"; import { type GlobalOptions } from "../cli.js"; export interface Options extends GlobalOptions { @@ -82,6 +87,12 @@ export const handler = async (argv: Arguments): Promise => { return resolve(`.${moduleName}rc.${answers.format as string}`); }, }, + { + type: "input", + name: "aliases", + message: + "Enter file path to existing table aliases file, or directory path to create a new one (optional)", + }, ]; // Extract path and format as we don't include them in the config file @@ -92,14 +103,30 @@ export const handler = async (argv: Arguments): Promise => { } else { output = { ...defaults, ...answers }; } - const { path, format, ...rest } = output; - const filePath = resolve( - typeof path === "string" ? path : `.${moduleName}rc` - ); + // Create the config file + const { path, format, aliases, ...rest } = output; + const configFilePath = resolve(path ?? `.${moduleName}rc`); + // Make sure the table aliases file or provided directory exists + if (aliases != null) { + try { + const type = checkAliasesPath(aliases); + if (type === "file") { + rest.aliases = resolve(aliases); + } + if (type === "dir") { + const aliasesFilePath = resolve(aliases, "tableland.aliases.json"); + writeFileSync(aliasesFilePath, JSON.stringify({})); + rest.aliases = aliasesFilePath; + } + } catch (err: any) { + logger.error(err.message); // exit early since the input was invalid + return; + } + } let stream = process.stdout as unknown as WriteStream; if (path !== ".") { - mkdirSync(dirname(filePath), { recursive: true }); - stream = createWriteStream(filePath, "utf-8"); + mkdirSync(dirname(configFilePath), { recursive: true }); + stream = createWriteStream(configFilePath, "utf-8"); } try { switch (format) { @@ -113,7 +140,7 @@ export const handler = async (argv: Arguments): Promise => { break; } if (path !== ".") { - logger.log(`Config created at ${filePath}`); + logger.log(`Config created at ${configFilePath}`); } } catch (err: any) { logger.error(err.message); diff --git a/packages/cli/src/commands/namespace.ts b/packages/cli/src/commands/namespace.ts index 621c7227..0bf5d50f 100644 --- a/packages/cli/src/commands/namespace.ts +++ b/packages/cli/src/commands/namespace.ts @@ -48,10 +48,10 @@ async function setHandler( const valueRegex = /^[a-zA-Z_][a-zA-Z0-9_]*_[0-9]+_[0-9]+$/; if (keyRegex.exec(key) === null) { - throw new Error("Only letters or underscores in key name"); + throw new Error("only letters or underscores in key name"); } if (valueRegex.exec(value) === null) { - throw new Error("Tablename is invalid"); + throw new Error("table name is invalid"); } return { key, @@ -84,7 +84,7 @@ export const builder: CommandBuilder, Options> = ( yargs .command( "get ", - "Pass in a record to find it's table name", + "Pass in a record to find its table name", (yargs) => yargs.positional("record", { type: "string", diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts index b4441c2e..a14518f5 100644 --- a/packages/cli/src/commands/schema.ts +++ b/packages/cli/src/commands/schema.ts @@ -1,8 +1,9 @@ import type yargs from "yargs"; import type { Arguments, CommandBuilder } from "yargs"; +import { init } from "@tableland/sqlparser"; import { type GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; -import { logger } from "../utils.js"; +import { logger, getTableNameWithAlias } from "../utils.js"; export interface Options extends GlobalOptions { name: string; @@ -21,9 +22,20 @@ export const builder: CommandBuilder, Options> = ( export const handler = async (argv: Arguments): Promise => { try { - const { name } = argv; - const [tableId, chainId] = name.split("_").reverse(); + await init(); + let name = await getTableNameWithAlias(argv.aliases, argv.name); + + // Check if the passed `name` uses ENS + // Note: duplicative `setupCommand` calls will occur with ENS, but this is + // required to properly parse the chainId from the table name + if (argv.enableEnsExperiment != null && argv.ensProviderUrl != null) { + const { ens } = await setupCommand({ + ...argv, + }); + if (ens != null) name = await ens.resolveTable(name); + } + const [tableId, chainId] = name.split("_").reverse(); const parts = name.split("_"); if (parts.length < 3 && argv.enableEnsExperiment == null) { @@ -39,7 +51,7 @@ export const handler = async (argv: Arguments): Promise => { }); const res = await validator.getTableById({ - tableId, + tableId: tableId.toString(), chainId: parseInt(chainId), }); logger.log(JSON.stringify(res.schema)); diff --git a/packages/cli/src/commands/shell.ts b/packages/cli/src/commands/shell.ts index b22bb3d4..550c1282 100644 --- a/packages/cli/src/commands/shell.ts +++ b/packages/cli/src/commands/shell.ts @@ -10,7 +10,7 @@ const help = `Commands: .exit - exit the shell .help - show this help -SQL Queries can be multi-line, and must end with a semicolon (;).`; +SQL Queries can be multi-line, and must end with a semicolon (;)`; export interface Options extends GlobalOptions { statement?: string; @@ -59,6 +59,7 @@ async function fireFullQuery( ): Promise { try { const { database, ens } = tablelandConnection; + if (argv.enableEnsExperiment != null && ens != null) { statement = await ens.resolve(statement); } @@ -70,15 +71,38 @@ async function fireFullQuery( const response = await stmt.all(); logger.log(JSON.stringify(response.results)); + const tableName = response.meta.txn?.name; + // Check if table aliases are enabled and, if so, include them in the logging + let tableAlias; + const aliasesEnabled = database.config.aliases != null; + if (aliasesEnabled) { + const tableAliases = await database.config.aliases?.read(); + if (tableAliases != null) { + tableAlias = Object.keys(tableAliases).find( + (alias) => tableAliases[alias] === tableName + ); + } + } + const logDataCreate: Partial<{ createdTable: string; alias: string }> = { + createdTable: tableName, + }; + const logDataWrite: Partial<{ updatedTable: string; alias: string }> = { + updatedTable: tableName, + }; + if (tableAlias != null) { + logDataCreate.alias = tableAlias; + logDataWrite.alias = tableAlias; + } switch (type) { case "create": - logger.log(JSON.stringify({ createdTable: response.meta.txn?.name })); + logger.log(JSON.stringify(logDataCreate)); break; case "write": - logger.log(JSON.stringify({ updatedTable: response.meta.txn?.name })); + logger.log(JSON.stringify(logDataWrite)); break; default: } + /* c8 ignore next 3 */ } catch (err: any) { logger.error( @@ -105,7 +129,7 @@ async function shellYeah( history, input: process.stdin, output: process.stdout, - prompt: "tableland>", + prompt: "tableland> ", terminal: true, }); rl.prompt(); @@ -131,7 +155,6 @@ async function shellYeah( case "help": default: logger.log(help); - break; } } @@ -177,7 +200,7 @@ export const builder: CommandBuilder, Options> = ( .option("format", { type: "string", choices: ["pretty", "table", "objects"] as const, - description: "Output format. One of 'pretty', 'table', or 'objects'.", + description: "Output format. One of 'pretty', 'table', or 'objects'", default: "pretty", }) as yargs.Argv; diff --git a/packages/cli/src/commands/transfer.ts b/packages/cli/src/commands/transfer.ts index 35669d22..4371485a 100644 --- a/packages/cli/src/commands/transfer.ts +++ b/packages/cli/src/commands/transfer.ts @@ -3,7 +3,7 @@ import type { Arguments, CommandBuilder } from "yargs"; import { init } from "@tableland/sqlparser"; import { type GlobalOptions } from "../cli.js"; import { setupCommand } from "../lib/commandSetup.js"; -import { logger } from "../utils.js"; +import { logger, getTableNameWithAlias } from "../utils.js"; export interface Options extends GlobalOptions { name: string; @@ -29,7 +29,9 @@ export const builder: CommandBuilder, Options> = ( export const handler = async (argv: Arguments): Promise => { try { await init(); - const { name, receiver, chain } = argv; + const { receiver, chain } = argv; + const name = await getTableNameWithAlias(argv.aliases, argv.name); + const tableDetails = await globalThis.sqlparser.validateTableName(name); const chainId = tableDetails.chainId; diff --git a/packages/cli/src/commands/write.ts b/packages/cli/src/commands/write.ts index 06e89c9d..ceda2a0e 100644 --- a/packages/cli/src/commands/write.ts +++ b/packages/cli/src/commands/write.ts @@ -6,6 +6,7 @@ import { getLink, logger, getChainName, + getTableNameWithAlias, type NormalizedStatement, } from "../utils.js"; import { type GlobalOptions } from "../cli.js"; @@ -105,9 +106,14 @@ export const handler = async (argv: Arguments): Promise => { normalized.statements.map(async function (stmt) { // re-normalize so we can be sure we've isolated each statement and it's tableId const norm = (await normalize(stmt)) as NormalizedStatement; - const { tableId } = await globalThis.sqlparser.validateTableName( + const name = await getTableNameWithAlias( + argv.aliases, norm.tables[0] ); + + const { tableId } = await globalThis.sqlparser.validateTableName( + name + ); return { statement: stmt, tableId: tableId.toString(), diff --git a/packages/cli/src/lib/commandSetup.ts b/packages/cli/src/lib/commandSetup.ts index e0af149f..e2100cb6 100644 --- a/packages/cli/src/lib/commandSetup.ts +++ b/packages/cli/src/lib/commandSetup.ts @@ -2,7 +2,7 @@ import { helpers, Database, Registry, Validator } from "@tableland/sdk"; import { init } from "@tableland/sqlparser"; import { type Signer } from "ethers"; import { type GlobalOptions } from "../cli.js"; -import { getWalletWithProvider, logger } from "../utils.js"; +import { getWalletWithProvider, logger, jsonFileAliases } from "../utils.js"; import EnsResolver from "./EnsResolver.js"; export class Connections { @@ -101,6 +101,7 @@ export class Connections { baseUrl, enableEnsExperiment, ensProviderUrl, + aliases, } = argv; if (privateKey != null && chain != null) { @@ -130,11 +131,17 @@ export class Connections { if (this._signer != null) this._registry = new Registry({ signer: this._signer }); + let aliasesNameMap; + if (aliases != null) { + aliasesNameMap = jsonFileAliases(aliases); + } + this._database = new Database({ - // both of these props might be undefined + // signer, baseURL, and aliases might be undefined signer: this._signer, baseUrl, autoWait: true, + aliases: aliasesNameMap, }); if (typeof baseUrl === "string" && baseUrl.trim() !== "") { diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 9e773e55..ff44c0ab 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -1,3 +1,5 @@ +import { readFileSync, writeFileSync, statSync } from "node:fs"; +import { extname } from "path"; import { Wallet, providers, getDefaultProvider } from "ethers"; import { helpers } from "@tableland/sdk"; @@ -36,8 +38,8 @@ export const wait = async (timeout: number): Promise => export function getLink(chain: helpers.ChainName, hash: string): string { /* c8 ignore start */ if (chain.includes("ethereum")) { - if (chain.includes("goerli")) { - return `https://goerli.etherscan.io/tx/${hash}`; + if (chain.includes("sepolia")) { + return `https://sepolia.etherscan.io/tx/${hash}`; } return `https://etherscan.io/tx/${hash}`; } else if (chain.includes("polygon")) { @@ -47,12 +49,15 @@ export function getLink(chain: helpers.ChainName, hash: string): string { return `https://polygonscan.com/tx/${hash}`; } else if (chain.includes("optimism")) { if (chain.includes("goerli")) { - return `https://blockscout.com/optimism/goerli/tx/${hash}`; + return `https://goerli-optimism.etherscan.io/tx/${hash}`; } return `https://optimistic.etherscan.io/tx/${hash}`; } else if (chain.includes("arbitrum")) { if (chain.includes("goerli")) { - return `https://goerli-rollup-explorer.arbitrum.io/tx/${hash}`; + return `https://goerli.arbiscan.io/tx/${hash}`; + } + if (chain.includes("nova")) { + return `https://nova.arbiscan.io/tx/${hash}`; } return `https://arbiscan.io/tx/${hash}`; } @@ -77,7 +82,7 @@ export async function getWalletWithProvider({ const wallet = new Wallet(privateKey); - // We want to aquire a provider using the params given by the caller. + // We want to acquire a provider using the params given by the caller. let provider: providers.BaseProvider | undefined; // first we check if a providerUrl was given. if (typeof providerUrl === "string") { @@ -118,14 +123,14 @@ export async function getWalletWithProvider({ } if (providerChainId !== network.chainId) { - throw new Error("provider / chain mismatch."); + throw new Error("provider / chain mismatch"); } /* c8 ignore stop */ return wallet.connect(provider); } -// Wrap any direct calls to console.log, so that test spies can distinguise between +// Wrap any direct calls to console.log, so that test spies can distinguish between // the CLI's output, and messaging that originates outside the CLI export const logger = { log: function (message: string) { @@ -138,3 +143,80 @@ export const logger = { console.error(message); }, }; + +/** + * Check if a table aliases file exists, or if a directory exists where we can + * create a new one (note: only used with `init`, where creation can happen). + * @param path Path to existing aliases file or directory to create one at. + * @returns The type of the path, either "file" or "dir". + */ +/* c8 ignore start */ +export function checkAliasesPath(path: string): string { + let type; + let isStatErr; + try { + const stats = statSync(path); + if (stats.isFile() && extname(path) === ".json") type = "file"; // only set "type" if it's JSON + if (stats.isDirectory()) type = "dir"; + } catch { + isStatErr = true; + } + if (type === undefined || isStatErr != null) + throw new Error("invalid table aliases path"); + return type; +} +/* c8 ignore stop */ + +/** + * Check if a table aliases file exists and is JSON. + * @param path Path to existing aliases file. + * @returns true if the file exists and is JSON, false otherwise. + */ +export function isValidAliasesFile(path: string): boolean { + try { + const stats = statSync(path); + if (stats.isFile() && extname(path) === ".json") return true; + } catch { + /* c8 ignore next 4 */ + return false; + } + return false; +} + +// Recreate SDK helper's `jsonFileAliases` but with updating the file, not overwriting +type NameMapping = Record; + +interface AliasesNameMap { + read: () => Promise; + write: (map: NameMapping) => Promise; +} + +export function jsonFileAliases(filepath: string): AliasesNameMap { + const isValid = isValidAliasesFile(filepath); + if (!isValid) { + throw new Error(`invalid table aliases file`); + } + return { + read: async function (): Promise { + const file = readFileSync(filepath); + return JSON.parse(file.toString()); + }, + write: async function (nameMap: NameMapping) { + const file = readFileSync(filepath); + const original = JSON.parse(file.toString()); + const merged = { ...original, ...nameMap }; + writeFileSync(filepath, JSON.stringify(merged)); + }, + }; +} + +export async function getTableNameWithAlias( + filepath: unknown, + name: string +): Promise { + if (typeof filepath !== "string" || filepath.trim() === "") return name; + + const nameMap = await jsonFileAliases(filepath).read(); + const uuName = nameMap[name]; + return uuName ?? name; +} diff --git a/packages/cli/test/controller.test.ts b/packages/cli/test/controller.test.ts index 9f6d6c71..7595eaaa 100644 --- a/packages/cli/test/controller.test.ts +++ b/packages/cli/test/controller.test.ts @@ -5,13 +5,20 @@ import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; import { Database } from "@tableland/sdk"; import { getAccounts } from "@tableland/local"; +import { temporaryWrite } from "tempy"; import * as mod from "../src/commands/controller.js"; -import { wait, logger } from "../src/utils.js"; -import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup"; +import { jsonFileAliases, logger, wait } from "../src/utils.js"; +import { + TEST_TIMEOUT_FACTOR, + TEST_PROVIDER_URL, + TEST_VALIDATOR_URL, +} from "./setup"; const defaultArgs = [ "--providerUrl", TEST_PROVIDER_URL, + "--baseUrl", + TEST_VALIDATOR_URL, "--chain", "local-tableland", ]; @@ -54,7 +61,7 @@ describe("commands/controller", function () { await yargs([ "controller", "set", - "someting", + "something", "another", "--privateKey", privateKey, @@ -107,6 +114,24 @@ describe("commands/controller", function () { match(value, /error validating name: table name has wrong format:/); }); + test("throws with invalid lock arguments", async function () { + const privateKey = accounts[1].privateKey.slice(2); + const consoleError = spy(logger, "error"); + await yargs([ + "controller", + "lock", + "invalid", + "--privateKey", + privateKey, + ...defaultArgs, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + match(value, /error validating name: table name has wrong format:/); + }); + test("passes when setting a controller", async function () { const privateKey = accounts[1].privateKey.slice(2); const consoleLog = spy(logger, "log"); @@ -168,4 +193,229 @@ describe("commands/controller", function () { equal(value.hash.startsWith("0x"), true); equal(value.from, accounts[1].address); }); + + describe("with table aliases", function () { + test("throws with invalid get arguments", async function () { + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + + const consoleError = spy(logger, "error"); + await yargs([ + "controller", + "get", + "invalid", + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + match(value, /error validating name: table name has wrong format:/); + }); + + test("throws with invalid set arguments", async function () { + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + + const consoleError = spy(logger, "error"); + await yargs([ + "controller", + "set", + "invalid", + "invalid", + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + match(value, /error validating name: table name has wrong format:/); + }); + + test("throws with invalid lock arguments", async function () { + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + + const consoleError = spy(logger, "error"); + await yargs([ + "controller", + "lock", + "invalid", + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + match(value, /error validating name: table name has wrong format:/); + }); + + test("passes when setting a controller", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const nameFromCreate = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find( + (alias) => nameMap[alias] === nameFromCreate + ) ?? ""; + equal(tableAlias, prefix); + + const account1 = accounts[1]; + const account2 = accounts[2]; + + // Now, set the controller + const consoleLog = spy(logger, "log"); + await yargs([ + "controller", + "set", + account2.address, + tableAlias, + "--privateKey", + account1.privateKey.slice(2), + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const { hash, link } = JSON.parse(res); + equal(typeof hash, "string"); + equal(hash.startsWith("0x"), true); + equal(link != null, true); + }); + + test("passes when getting a controller", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const nameFromCreate = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find( + (alias) => nameMap[alias] === nameFromCreate + ) ?? ""; + equal(tableAlias, prefix); + + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + // Now, get the controller + const consoleLog = spy(logger, "log"); + await yargs([ + "controller", + "get", + tableAlias, + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + equal(value, "0x0000000000000000000000000000000000000000"); + }); + + test("passes when locking a controller", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const nameFromCreate = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find( + (alias) => nameMap[alias] === nameFromCreate + ) ?? ""; + equal(tableAlias, prefix); + + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + // Now, lock the controller + const consoleLog = spy(logger, "log"); + await yargs([ + "controller", + "lock", + tableAlias, + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + + equal(value.hash.startsWith("0x"), true); + equal(value.from, account.address); + }); + }); }); diff --git a/packages/cli/test/create.test.ts b/packages/cli/test/create.test.ts index 1edfd580..3817ad83 100644 --- a/packages/cli/test/create.test.ts +++ b/packages/cli/test/create.test.ts @@ -7,7 +7,7 @@ import mockStd from "mock-stdin"; import { getAccounts } from "@tableland/local"; import { ethers } from "ethers"; import * as mod from "../src/commands/create.js"; -import { wait, logger } from "../src/utils.js"; +import { wait, logger, jsonFileAliases } from "../src/utils.js"; import { getResolverMock } from "./mock.js"; import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup"; @@ -67,12 +67,12 @@ describe("commands/create", function () { "(id int primary key, desc text)", "--privateKey", privateKey, + "--providerUrl", + TEST_PROVIDER_URL, "--prefix", "invalid_chain_table", "--chain", "foozbaaz", - "--providerUrl", - TEST_PROVIDER_URL, ]) .command(mod) .parse(); @@ -88,11 +88,11 @@ describe("commands/create", function () { await yargs([ "create", "invalid", + ...defaultArgs, "--prefix", "cooltable", "--privateKey", privateKey, - ...defaultArgs, ]) .command(mod) .parse(); @@ -111,11 +111,11 @@ describe("commands/create", function () { await yargs([ "create", "create table fooz (a int);insert into fooz (a) values (1);", + ...defaultArgs, "--prefix", "cooltable", "--privateKey", privateKey, - ...defaultArgs, ]) .command(mod) .parse(); @@ -132,9 +132,9 @@ describe("commands/create", function () { "create", "--file", "missing.sql", + ...defaultArgs, "--privateKey", privateKey, - ...defaultArgs, ]) .command(mod) .parse(); @@ -151,7 +151,7 @@ describe("commands/create", function () { setTimeout(() => { stdin.send("\n").end(); }, 300); - await yargs(["create", "--privateKey", privateKey, ...defaultArgs]) + await yargs(["create", ...defaultArgs, "--privateKey", privateKey]) .command(mod) .parse(); @@ -162,6 +162,31 @@ describe("commands/create", function () { ); }); + test("throws with invalid table alias file", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + // Set up faux aliases file + const aliasesFilePath = "./invalid.json"; + + await yargs([ + "create", + "id int", + "--prefix", + "table_aliases", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleError.getCall(0).firstArg; + equal(res, "invalid table aliases file"); + }); + test("creates table if prefix not provided", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); @@ -183,18 +208,18 @@ describe("commands/create", function () { match(name, /^_31337_[0-9]+$/); }); - test("Create passes with local-tableland", async function () { + test("create passes with local-tableland", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); const consoleLog = spy(logger, "log"); await yargs([ "create", "id int primary key, name text", + ...defaultArgs, "--privateKey", privateKey, "--prefix", "first_table", - ...defaultArgs, ]) .command(mod) .parse(); @@ -220,12 +245,12 @@ describe("commands/create", function () { "id int primary key, name text", "--chain", "31337", + "--providerUrl", + TEST_PROVIDER_URL, "--privateKey", privateKey, "--prefix", "chainid_table", - "--providerUrl", - TEST_PROVIDER_URL, ]) .command(mod) .parse(); @@ -249,11 +274,11 @@ describe("commands/create", function () { await yargs([ "create", "create table second_table (id int primary key, name text);", + ...defaultArgs, "--privateKey", privateKey, "--prefix", "ignore_me", - ...defaultArgs, ]) .command(mod) .parse(); @@ -278,11 +303,11 @@ describe("commands/create", function () { "create", `create table first_table (id int primary key, name text); create table second_table (id int primary key, name text);`, + ...defaultArgs, "--privateKey", privateKey, "--prefix", "ignore_me", - ...defaultArgs, ]) .command(mod) .parse(); @@ -312,13 +337,13 @@ describe("commands/create", function () { const path = await temporaryWrite(`\nid int primary key,\nname text\n`); await yargs([ "create", + ...defaultArgs, "--file", path, "--privateKey", privateKey, "--prefix", "file_test", - ...defaultArgs, ]) .command(mod) .parse(); @@ -345,7 +370,7 @@ describe("commands/create", function () { .send("create table stdin_test (id int primary key, name text);\n") .end(); }, 100); - await yargs(["create", "--privateKey", privateKey, ...defaultArgs]) + await yargs(["create", ...defaultArgs, "--privateKey", privateKey]) .command(mod) .parse(); @@ -374,14 +399,14 @@ describe("commands/create", function () { "create", "id integer, message text", "hello", + ...defaultArgs, "--privateKey", privateKey, "--ns", "foo.bar.eth", "--enableEnsExperiment", "--ensProviderUrl", - "https://localhost:8082", - ...defaultArgs, + "https://localhost:8080", ]) .command(mod) .parse(); @@ -403,8 +428,7 @@ describe("commands/create", function () { "id int primary key, name text", "--prefix", "custom_url_table", - "--chain", - "local-tableland", + ...defaultArgs, "--privateKey", privateKey, "--providerUrl", @@ -416,4 +440,64 @@ describe("commands/create", function () { const value = consoleError.getCall(0).firstArg; equal(value, "cannot determine provider chain ID"); }); + + test("passes with table aliases", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleLog = spy(logger, "log"); + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + + await yargs([ + "create", + "id int", + "--prefix", + "table_aliases", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + let res = consoleLog.getCall(0).firstArg; + let value = JSON.parse(res); + const { prefix1, name1 } = value.meta.txn; + + // Check the aliases file was updated and matches with the prefix + let nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias1 = Object.keys(nameMap).find( + (alias) => nameMap[alias] === name1 + ); + equal(tableAlias1, prefix1); + + // Make sure that creating another table mutates the file, not overwrites it + await yargs([ + "create", + "id int", + "--prefix", + "second_table", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + res = consoleLog.getCall(0).firstArg; + value = JSON.parse(res); + const { prefix2, name2 } = value.meta.txn; + + // Check the aliases file was updated and matches with both prefixes + nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias2 = Object.keys(nameMap).find( + (alias) => nameMap[alias] === name2 + ); + equal(tableAlias1, prefix1); + equal(tableAlias2, prefix2); + }); }); diff --git a/packages/cli/test/info.test.ts b/packages/cli/test/info.test.ts index 02d23396..ac9cf46d 100644 --- a/packages/cli/test/info.test.ts +++ b/packages/cli/test/info.test.ts @@ -1,12 +1,33 @@ import { equal } from "node:assert"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore } from "sinon"; +import { spy, restore, stub } from "sinon"; import yargs from "yargs/yargs"; +import { ethers, getDefaultProvider } from "ethers"; +import { getAccounts } from "@tableland/local"; +import { Database } from "@tableland/sdk"; +import { temporaryWrite } from "tempy"; +import ensLib from "../src/lib/EnsCommand"; import * as mod from "../src/commands/info.js"; -import { wait, logger } from "../src/utils.js"; -import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup"; +import * as ns from "../src/commands/namespace.js"; +import { jsonFileAliases, logger, wait } from "../src/utils.js"; +import { getResolverMock } from "./mock.js"; +import { + TEST_TIMEOUT_FACTOR, + TEST_PROVIDER_URL, + TEST_VALIDATOR_URL, +} from "./setup"; -const defaultArgs = ["--providerUrl", TEST_PROVIDER_URL]; +const defaultArgs = [ + "--providerUrl", + TEST_PROVIDER_URL, + "--baseUrl", + TEST_VALIDATOR_URL, +]; + +const accounts = getAccounts(); +const wallet = accounts[1]; +const provider = getDefaultProvider(TEST_PROVIDER_URL); +const signer = wallet.connect(provider); describe("commands/info", function () { this.timeout(30000 * TEST_TIMEOUT_FACTOR); @@ -19,7 +40,7 @@ describe("commands/info", function () { restore(); }); - test("info throws with invalid table name", async function () { + test("throws with invalid table name", async function () { const consoleError = spy(logger, "error"); await yargs(["info", "invalid_name", ...defaultArgs]) .command(mod) @@ -32,7 +53,7 @@ describe("commands/info", function () { ); }); - test("info throws with invalid chain", async function () { + test("throws with invalid chain", async function () { const consoleError = spy(logger, "error"); await yargs(["info", "valid_9999_0", ...defaultArgs]) .command(mod) @@ -42,7 +63,56 @@ describe("commands/info", function () { equal(value, "unsupported chain (see `chains` command for details)"); }); - test("Info passes with local-tableland", async function () { + test("throws with missing table", async function () { + const consoleError = spy(logger, "error"); + await yargs(["info", "ignored_31337_99", ...defaultArgs]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "Not Found"); + }); + + test("throws with invalid table aliases file", async function () { + const consoleError = spy(logger, "error"); + await yargs([ + "info", + "table_alias", + ...defaultArgs, + "--aliases", + "./invalid.json", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "invalid table aliases file"); + }); + + test("throws with invalid table alias definition", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + const consoleError = spy(logger, "error"); + await yargs([ + "info", + "table_alias", + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal( + value, + "invalid table name (name format is `{prefix}_{chainId}_{tableId}`)" + ); + }); + + test("passes with local-tableland", async function () { const consoleLog = spy(logger, "log"); await yargs(["info", "healthbot_31337_1", ...defaultArgs]) .command(mod) @@ -53,17 +123,176 @@ describe("commands/info", function () { const { name, attributes, externalUrl } = value; equal(name, "healthbot_31337_1"); - equal(externalUrl, "http://localhost:8082/api/v1/tables/31337/1"); + equal(externalUrl, `${TEST_VALIDATOR_URL}/tables/31337/1`); equal(Array.isArray(attributes), true); }); - test("info throws with missing table", async function () { - const consoleError = spy(logger, "error"); - await yargs(["info", "ignored_31337_99", ...defaultArgs]) + test("passes with valid ENS name", async function () { + // Must do initial ENS setup and set the record name to the table + await new Promise((resolve) => setTimeout(resolve, 1000)); + stub(ensLib, "ENS").callsFake(function () { + return { + withProvider: () => { + return { + setRecords: async () => { + return false; + }, + }; + }, + }; + }); + stub(ethers.providers.JsonRpcProvider.prototype, "getResolver").callsFake( + getResolverMock + ); + + const consoleLog = spy(logger, "log"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "healthbot=healthbot_31337_1", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ...defaultArgs, + ]) + .command(ns) + .parse(); + + let res = consoleLog.getCall(0).firstArg; + let value = JSON.parse(res); + equal(value.domain, "foo.bar.eth"); + equal(value.records[0].key, "healthbot"); + equal(value.records[0].value, "healthbot_31337_1"); + + // Now, check the table info using ENS as the name + await yargs([ + "info", + "foo.bar.eth", + ...defaultArgs, + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) .command(mod) .parse(); - const value = consoleError.getCall(0).firstArg; - equal(value, "Not Found"); + res = consoleLog.getCall(1).firstArg; + value = JSON.parse(res); + const { name, attributes, externalUrl } = value; + + equal(name, "healthbot_31337_1"); + equal(externalUrl, `${TEST_VALIDATOR_URL}/tables/31337/1`); + equal(Array.isArray(attributes), true); + }); + + test("passes with valid ENS name when invalid alias provided", async function () { + // Must do initial ENS setup and set the record name to the table + await new Promise((resolve) => setTimeout(resolve, 1000)); + stub(ensLib, "ENS").callsFake(function () { + return { + withProvider: () => { + return { + setRecords: async () => { + return false; + }, + }; + }, + }; + }); + stub(ethers.providers.JsonRpcProvider.prototype, "getResolver").callsFake( + getResolverMock + ); + + const consoleLog = spy(logger, "log"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "healthbot=healthbot_31337_1", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ...defaultArgs, + ]) + .command(ns) + .parse(); + + let res = consoleLog.getCall(0).firstArg; + let value = JSON.parse(res); + equal(value.domain, "foo.bar.eth"); + equal(value.records[0].key, "healthbot"); + equal(value.records[0].value, "healthbot_31337_1"); + + // Create an empty aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + + // Now, check the table info using ENS as the name + await yargs([ + "info", + "foo.bar.eth", + ...defaultArgs, + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + res = consoleLog.getCall(1).firstArg; + value = JSON.parse(res); + const { name, attributes, externalUrl } = value; + + equal(name, "healthbot_31337_1"); + equal(externalUrl, `${TEST_VALIDATOR_URL}/tables/31337/1`); + equal(Array.isArray(attributes), true); + }); + + test("passes with table aliases", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const nameFromCreate = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find((alias) => nameMap[alias] === nameFromCreate) ?? + ""; + equal(tableAlias, prefix); + + // Get table info via alias + const consoleLog = spy(logger, "log"); + await yargs([ + "info", + tableAlias, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { name: nameFromInfo, attributes } = value; + + equal(nameFromInfo, nameFromCreate); + equal(Array.isArray(attributes), true); }); }); diff --git a/packages/cli/test/list.test.ts b/packages/cli/test/list.test.ts index bf78e0e4..c5eaac38 100644 --- a/packages/cli/test/list.test.ts +++ b/packages/cli/test/list.test.ts @@ -4,14 +4,16 @@ import { spy, restore } from "sinon"; import { getAccounts } from "@tableland/local"; import yargs from "yargs/yargs"; import * as mod from "../src/commands/list.js"; -import { logger } from "../src/utils.js"; +import { logger, wait } from "../src/utils.js"; import { TEST_PROVIDER_URL } from "./setup"; const defaultArgs = ["--providerUrl", TEST_PROVIDER_URL]; +const accounts = getAccounts(); + describe("commands/list", function () { before(async function () { - await new Promise((resolve) => setTimeout(resolve, 1000)); + await wait(1000); }); afterEach(function () { @@ -29,7 +31,7 @@ describe("commands/list", function () { }); test("List throws without chain", async function () { - const [account] = getAccounts(); + const account = accounts[1]; const privateKey = account.privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs(["list", "--privateKey", privateKey, ...defaultArgs]) @@ -41,7 +43,7 @@ describe("commands/list", function () { }); test("List throws with invalid chain", async function () { - const [account] = getAccounts(); + const account = accounts[1]; const privateKey = account.privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs([ @@ -60,7 +62,7 @@ describe("commands/list", function () { }); test("throws with custom network", async function () { - const [account] = getAccounts(); + const account = accounts[1]; const privateKey = account.privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs([ @@ -79,7 +81,9 @@ describe("commands/list", function () { }); test("List passes with local-tableland", async function () { - const [account] = getAccounts(); + // need to use the Validator wallet here, since we are asserting + // that the healthbot table is returned from `list` + const account = accounts[0]; const privateKey = account.privateKey.slice(2); const consoleLog = spy(logger, "log"); await yargs([ diff --git a/packages/cli/test/namespace.test.ts b/packages/cli/test/namespace.test.ts index 69bd25fc..bbdf95c0 100644 --- a/packages/cli/test/namespace.test.ts +++ b/packages/cli/test/namespace.test.ts @@ -67,7 +67,7 @@ describe("commands/namespace", function () { ); }); - test("fails if ens name is invalid", async function () { + test("fails if ENS name is invalid", async function () { const consoleError = spy(logger, "error"); await yargs([ "namespace", @@ -82,7 +82,7 @@ describe("commands/namespace", function () { .parse(); const value = consoleError.getCall(0).firstArg; - equal(value, "Only letters or underscores in key name"); + equal(value, "only letters or underscores in key name"); }); test("fails if table name is invalid", async function () { @@ -100,10 +100,10 @@ describe("commands/namespace", function () { .parse(); const value = consoleError.getCall(0).firstArg; - equal(value, "Tablename is invalid"); + equal(value, "table name is invalid"); }); - test("Get ENS name", async function () { + test("get ENS name", async function () { stub(ethers.providers.JsonRpcProvider.prototype, "getResolver").callsFake( getResolverMock ); @@ -125,7 +125,7 @@ describe("commands/namespace", function () { equal(value.value, "healthbot_31337_1"); }); - test("Set ENS name", async function () { + test("set ENS name", async function () { const consoleLog = spy(logger, "log"); await yargs([ "namespace", diff --git a/packages/cli/test/read.test.ts b/packages/cli/test/read.test.ts index 09f2b04e..24790458 100644 --- a/packages/cli/test/read.test.ts +++ b/packages/cli/test/read.test.ts @@ -8,7 +8,7 @@ import { ethers, getDefaultProvider } from "ethers"; import { Database } from "@tableland/sdk"; import { getAccounts } from "@tableland/local"; import * as mod from "../src/commands/read.js"; -import { wait, logger } from "../src/utils.js"; +import { wait, logger, jsonFileAliases } from "../src/utils.js"; import { getResolverMock } from "./mock.js"; import { TEST_TIMEOUT_FACTOR, @@ -16,6 +16,15 @@ import { TEST_VALIDATOR_URL, } from "./setup"; +const defaultArgs = [ + "--baseUrl", + TEST_VALIDATOR_URL, + "--providerUrl", + TEST_PROVIDER_URL, + "--chain", + "local-tableland", +]; + const accounts = getAccounts(); const wallet = accounts[1]; const provider = getDefaultProvider(TEST_PROVIDER_URL); @@ -31,7 +40,7 @@ describe("commands/read", function () { afterEach(async function () { restore(); - // ensure these tests don't hit rate limitting errors + // ensure these tests don't hit rate limiting errors await wait(500); }); @@ -94,8 +103,7 @@ describe("commands/read", function () { "--format", "objects", "--unwrap", - "--chain", - "local-tableland", + ...defaultArgs, ]) .command(mod) .parse(); @@ -135,14 +143,36 @@ describe("commands/read", function () { ); }); + test("fails with invalid table alias file", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + // Set up faux aliases file + const aliasesFilePath = "./invalid.json"; + + await yargs([ + "read", + "SELECT * FROM table_aliases;", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleError.getCall(0).firstArg; + equal(res, "invalid table aliases file"); + }); + test("passes with extract option", async function () { const consoleLog = spy(logger, "log"); await yargs([ "read", "select counter from healthbot_31337_1;", "--extract", - "--chain", - "local-tableland", + ...defaultArgs, ]) .command(mod) .parse(); @@ -160,8 +190,7 @@ describe("commands/read", function () { "read", "select counter from healthbot_31337_1 where counter = 1;", "--unwrap", - "--chain", - "local-tableland", + ...defaultArgs, ]) .command(mod) .parse(); @@ -314,7 +343,7 @@ describe("commands/read", function () { deepStrictEqual(value, '{"columns":[{"name":"counter"}],"rows":[[1]]}'); }); - test("passes withoutput format (table) when results are empty", async function () { + test("passes with output format (table) when results are empty", async function () { const { meta } = await db .prepare("CREATE TABLE empty_table (a int);") .all(); @@ -328,4 +357,44 @@ describe("commands/read", function () { const value = consoleLog.getCall(0).firstArg; deepStrictEqual(value, '{"columns":[],"rows":[]}'); }); + + test("passes with table aliases", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + let { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const name = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find((alias) => nameMap[alias] === name) ?? ""; + equal(tableAlias, prefix); + + // Write to the table + ({ meta } = await db.prepare(`INSERT INTO ${name} values (1);`).run()); + await meta.txn?.wait(); + + const consoleLog = spy(logger, "log"); + await yargs([ + "read", + `select * from ${tableAlias};`, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + deepStrictEqual(value, '[{"id":1}]'); + }); }); diff --git a/packages/cli/test/schema.test.ts b/packages/cli/test/schema.test.ts index b54e3110..d5763adb 100644 --- a/packages/cli/test/schema.test.ts +++ b/packages/cli/test/schema.test.ts @@ -1,11 +1,37 @@ import { equal } from "node:assert"; +import { ethers, getDefaultProvider } from "ethers"; import { describe, test, afterEach, before } from "mocha"; -import { spy, restore } from "sinon"; +import { spy, restore, stub } from "sinon"; import yargs from "yargs/yargs"; -import * as mod from "../src/commands/schema.js"; -import { wait, logger } from "../src/utils.js"; +import { getAccounts } from "@tableland/local"; +import { Database } from "@tableland/sdk"; +import { temporaryWrite } from "tempy"; +import ensLib from "../src/lib/EnsCommand"; +import * as mod from "../src/commands/schema"; +import * as ns from "../src/commands/namespace.js"; +import { jsonFileAliases, logger, wait } from "../src/utils.js"; +import { getResolverMock } from "./mock.js"; +import { + TEST_TIMEOUT_FACTOR, + TEST_PROVIDER_URL, + TEST_VALIDATOR_URL, +} from "./setup"; + +const defaultArgs = [ + "--chain", + "local-tableland", + "--baseUrl", + TEST_VALIDATOR_URL, +]; + +const accounts = getAccounts(); +const wallet = accounts[1]; +const provider = getDefaultProvider(TEST_PROVIDER_URL, { chainId: 31337 }); +const signer = wallet.connect(provider); describe("commands/schema", function () { + this.timeout(10000 * TEST_TIMEOUT_FACTOR); + before(async function () { await wait(1000); }); @@ -14,7 +40,7 @@ describe("commands/schema", function () { restore(); }); - test("throws without invalid table name", async function () { + test("throws with invalid table name", async function () { const consoleError = spy(logger, "error"); await yargs(["schema", "invalid_name"]).command(mod).parse(); @@ -41,11 +67,197 @@ describe("commands/schema", function () { equal(value, "Not Found"); }); - test("Schema passes with local-tableland", async function () { + test("throws with invalid table aliases file", async function () { + const consoleError = spy(logger, "error"); + await yargs([ + "schema", + "table_alias", + ...defaultArgs, + "--aliases", + "./invalid.json", + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal(value, "invalid table aliases file"); + }); + + test("throws with invalid table alias definition", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + const consoleError = spy(logger, "error"); + await yargs([ + "schema", + "table_alias", + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).firstArg; + equal( + value, + "invalid table name (name format is `{prefix}_{chainId}_{tableId}`)" + ); + }); + + test("passes with local-tableland", async function () { const consoleLog = spy(logger, "log"); await yargs(["schema", "healthbot_31337_1"]).command(mod).parse(); const value = consoleLog.getCall(0).firstArg; equal(value, `{"columns":[{"name":"counter","type":"integer"}]}`); }); + + test("passes with valid ENS name", async function () { + // Must do initial ENS setup and set the record name to the table + await new Promise((resolve) => setTimeout(resolve, 1000)); + stub(ensLib, "ENS").callsFake(function () { + return { + withProvider: () => { + return { + setRecords: async () => { + return false; + }, + }; + }, + }; + }); + stub(ethers.providers.JsonRpcProvider.prototype, "getResolver").callsFake( + getResolverMock + ); + + const consoleLog = spy(logger, "log"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "healthbot=healthbot_31337_1", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(ns) + .parse(); + + let res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value.domain, "foo.bar.eth"); + equal(value.records[0].key, "healthbot"); + equal(value.records[0].value, "healthbot_31337_1"); + + // Now, check the table schema using ENS as the name + await yargs([ + "schema", + "foo.bar.eth", + ...defaultArgs, + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(mod) + .parse(); + + res = consoleLog.getCall(1).firstArg; + equal(res, `{"columns":[{"name":"counter","type":"integer"}]}`); + }); + + test("passes with valid ENS name when invalid alias provided", async function () { + // Must do initial ENS setup and set the record name to the table + await new Promise((resolve) => setTimeout(resolve, 1000)); + stub(ensLib, "ENS").callsFake(function () { + return { + withProvider: () => { + return { + setRecords: async () => { + return false; + }, + }; + }, + }; + }); + stub(ethers.providers.JsonRpcProvider.prototype, "getResolver").callsFake( + getResolverMock + ); + + const consoleLog = spy(logger, "log"); + await yargs([ + "namespace", + "set", + "foo.bar.eth", + "healthbot=healthbot_31337_1", + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + ]) + .command(ns) + .parse(); + + let res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + equal(value.domain, "foo.bar.eth"); + equal(value.records[0].key, "healthbot"); + equal(value.records[0].value, "healthbot_31337_1"); + + // Create an empty aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + + // Now, check the table info using ENS as the name + await yargs([ + "schema", + "foo.bar.eth", + ...defaultArgs, + "--enableEnsExperiment", + "--ensProviderUrl", + "https://localhost:7070", + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + res = consoleLog.getCall(1).firstArg; + equal(res, `{"columns":[{"name":"counter","type":"integer"}]}`); + }); + + test("passes with table aliases", async function () { + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { + extension: "json", + }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const nameFromCreate = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find((alias) => nameMap[alias] === nameFromCreate) ?? + ""; + equal(tableAlias, prefix); + + // Get table schema via alias + const consoleLog = spy(logger, "log"); + await yargs(["schema", tableAlias, "--aliases", aliasesFilePath]) + .command(mod) + .parse(); + + const value = consoleLog.getCall(0).firstArg; + equal(value, `{"columns":[{"name":"id","type":"int"}]}`); + }); }); diff --git a/packages/cli/test/shell.test.ts b/packages/cli/test/shell.test.ts index 371cdd8a..865c6064 100644 --- a/packages/cli/test/shell.test.ts +++ b/packages/cli/test/shell.test.ts @@ -1,11 +1,12 @@ -import { equal, match } from "node:assert"; +import { deepStrictEqual, equal, match } from "node:assert"; import { describe, test } from "mocha"; import { spy, restore, stub, assert } from "sinon"; import yargs from "yargs/yargs"; import mockStd from "mock-stdin"; -import { Database } from "@tableland/sdk"; import { getAccounts } from "@tableland/local"; +import { Database } from "@tableland/sdk"; import { ethers, getDefaultProvider } from "ethers"; +import { temporaryWrite } from "tempy"; import * as mod from "../src/commands/shell.js"; import { wait, logger } from "../src/utils.js"; import { getResolverMock } from "./mock.js"; @@ -175,7 +176,7 @@ describe("commands/shell", function () { equal(value, '[{"counter":1}]'); }); - test("Shell Works with initial input", async function () { + test("works with initial input", async function () { const consoleLog = spy(logger, "log"); const privateKey = accounts[0].privateKey.slice(2); @@ -195,7 +196,7 @@ describe("commands/shell", function () { equal(value, '[{"counter":1}]'); }); - test("Shell handles invalid query", async function () { + test("handles invalid query", async function () { const consoleError = spy(logger, "error"); const stdin = mockStd.stdin(); @@ -219,7 +220,7 @@ describe("commands/shell", function () { match(value, /error parsing statement/); }); - test("Write queries continue with 'y' input", async function () { + test("write queries continue with 'y' input", async function () { const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); @@ -247,7 +248,7 @@ describe("commands/shell", function () { match(value, /sometable_31337_\d+/); }); - test("Write queries aborts with 'n' input", async function () { + test("write queries aborts with 'n' input", async function () { const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); @@ -273,7 +274,7 @@ describe("commands/shell", function () { match(consoleLog.getCall(3).args[0], /Aborting\./i); }); - test("Shell throws without chain", async function () { + test("throws without chain", async function () { const privateKey = accounts[0].privateKey.slice(2); const consoleError = spy(logger, "error"); await yargs(["shell", "--privateKey", privateKey]).command(mod).parse(); @@ -282,25 +283,39 @@ describe("commands/shell", function () { equal(value, "missing required flag (`-c` or `--chain`)"); }); - test("Shell throws with invalid chain", async function () { + test("throws with invalid chain", async function () { const privateKey = accounts[0].privateKey.slice(2); const consoleError = spy(logger, "error"); + await yargs(["shell", "--privateKey", privateKey, "--chain", "foozbazz"]) + .command(mod) + .parse(); + + const value = consoleError.getCall(0).args[0]; + equal(value, "unsupported chain (see `chains` command for details)"); + }); + + test("throws with invalid table aliases file", async function () { + const consoleError = spy(logger, "error"); + // Set up faux aliases file + const aliasesFilePath = "./invalid.json"; + + const privateKey = accounts[0].privateKey.slice(2); await yargs([ "shell", ...defaultArgs, "--privateKey", privateKey, - "--chain", - "foozbazz", + "--aliases", + aliasesFilePath, ]) .command(mod) .parse(); - const value = consoleError.getCall(0).args[0]; - equal(value, "unsupported chain (see `chains` command for details)"); + const res = consoleError.getCall(0).args[0]; + equal(res, "invalid table aliases file"); }); - test("Custom baseUrl is called", async function () { + test("works when custom baseUrl is called", async function () { const stdin = mockStd.stdin(); const fetchSpy = spy(global, "fetch"); @@ -342,7 +357,7 @@ describe("commands/shell", function () { exit.restore(); }); - test("Shell Works with write statement", async function () { + test("works with write statement", async function () { const { meta } = await db .prepare("CREATE TABLE shell_write (a int);") .all(); @@ -374,7 +389,7 @@ describe("commands/shell", function () { equal(value, "[]"); }); - test("Shell Works with multi-line", async function () { + test("works with multi-line", async function () { const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); @@ -398,7 +413,7 @@ describe("commands/shell", function () { equal(value, '[{"counter":1}]'); }); - test("Shell can print help statement", async function () { + test("can print help statement", async function () { const consoleLog = spy(logger, "log"); const stdin = mockStd.stdin(); @@ -426,7 +441,66 @@ describe("commands/shell", function () { .exit - exit the shell .help - show this help -SQL Queries can be multi-line, and must end with a semicolon (;).` +SQL Queries can be multi-line, and must end with a semicolon (;)` ); }); + + test("works with table aliases (creates, writes, reads)", async function () { + const consoleLog = spy(logger, "log"); + const stdin = mockStd.stdin(); + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + + // First, create a table + setTimeout(() => { + stdin.send("CREATE TABLE table_aliases (id int);\n"); + setTimeout(() => { + stdin.send("y\n"); + }, 500); + }, 1000); + + const privateKey = accounts[0].privateKey.slice(2); + await yargs([ + "shell", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + // Check the create was successful + let res = consoleLog.getCall(4).args[0]; + let filter = res.replace("tableland> ", ""); + let value = JSON.parse(filter); + const { createdTable, alias } = value; + match(createdTable, /table_aliases_31337_\d+/); + equal(alias, "table_aliases"); + + // Write to the table using the alias + stdin.send(`INSERT INTO table_aliases VALUES (1);\n`); + setTimeout(() => { + stdin.send("y\n"); + }, 500); + await wait(4000); + + // Check the write was successful + res = consoleLog.getCall(6).args[0]; + filter = res.replace("tableland> ", ""); + value = JSON.parse(filter); + const { updatedTable, alias: aliasFromWrite } = value; + match(updatedTable, /table_aliases_31337_\d+/); + equal(aliasFromWrite, alias); + + // Read from the table using the alias + stdin.send(`SELECT * FROM table_aliases;\n`); + await wait(2000); + + // Check the read was successful + res = consoleLog.getCall(7).args[0]; + filter = res.replace("tableland> ", ""); + deepStrictEqual(filter, '[{"id":1}]'); + }); }); diff --git a/packages/cli/test/transfer.test.ts b/packages/cli/test/transfer.test.ts index 67a10ccf..e9dd476f 100644 --- a/packages/cli/test/transfer.test.ts +++ b/packages/cli/test/transfer.test.ts @@ -1,12 +1,13 @@ import { equal } from "node:assert"; +import { getDefaultProvider } from "ethers"; import { describe, test, afterEach, before } from "mocha"; import { spy, restore } from "sinon"; import yargs from "yargs/yargs"; -import { getDefaultProvider } from "ethers"; +import { temporaryWrite } from "tempy"; import { getAccounts } from "@tableland/local"; import { helpers, Database } from "@tableland/sdk"; import * as mod from "../src/commands/transfer.js"; -import { wait, logger } from "../src/utils.js"; +import { jsonFileAliases, logger, wait } from "../src/utils.js"; import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup"; const defaultArgs = [ @@ -43,9 +44,9 @@ describe("commands/transfer", function () { const consoleError = spy(logger, "error"); await yargs([ "transfer", - ...defaultArgs, tableName, "0x0000000000000000000000", + ...defaultArgs, ]) .command(mod) .parse(); @@ -84,7 +85,7 @@ describe("commands/transfer", function () { const account = accounts[1]; const privateKey = account.privateKey.slice(2); const consoleError = spy(logger, "error"); - await yargs(["transfer", "fooz", "blah", ...defaultArgs, "-k", privateKey]) + await yargs(["transfer", "fooz", "blah", "-k", privateKey, ...defaultArgs]) .command(mod) .parse(); @@ -100,9 +101,9 @@ describe("commands/transfer", function () { "transfer", tableName, "0x00", - ...defaultArgs, "--privateKey", privateKey, + ...defaultArgs, ]) .command(mod) .parse(); @@ -114,8 +115,58 @@ describe("commands/transfer", function () { ); }); + test("throws with invalid table aliases file", async function () { + const [, account1, account2] = accounts; + const account2Address = account2.address; + const consoleError = spy(logger, "error"); + const privateKey = account1.privateKey.slice(2); + + // Transfer the table + await yargs([ + "transfer", + "invalid", + account2Address, + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + "./invalid.json", + ]) + .command(mod) + .parse(); + + const res = consoleError.getCall(0).firstArg; + equal(res, "invalid table aliases file"); + }); + + test("throws with invalid table alias definition", async function () { + const [, account1, account2] = accounts; + const account2Address = account2.address; + const consoleError = spy(logger, "error"); + const privateKey = account1.privateKey.slice(2); + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + + // Transfer the table + await yargs([ + "transfer", + "invalid", + account2Address, + "--privateKey", + privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleError.getCall(0).firstArg; + equal(res, "error validating name: table name has wrong format: invalid"); + }); + // Does transfering table have knock-on effects on other tables? - test("Write passes with local-tableland", async function () { + test("passes with local-tableland", async function () { const [, account1, account2] = accounts; const account2Address = account2.address; const consoleLog = spy(logger, "log"); @@ -124,9 +175,60 @@ describe("commands/transfer", function () { "transfer", tableName, account2Address, + "--privateKey", + privateKey, ...defaultArgs, + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { to, from } = value; + + equal(from.toLowerCase(), account1.address.toLowerCase()); + equal( + to.toLowerCase(), + helpers.getContractAddress("local-tableland").toLowerCase() + ); + }); + + test("passes with table alias", async function () { + const [, account1, account2] = accounts; + const account2Address = account2.address; + const consoleLog = spy(logger, "log"); + const privateKey = account1.privateKey.slice(2); + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const name = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find((alias) => nameMap[alias] === name) ?? ""; + equal(tableAlias, prefix); + + // Transfer the table + await yargs([ + "transfer", + tableAlias, + account2Address, "--privateKey", privateKey, + ...defaultArgs, + "--aliases", + aliasesFilePath, ]) .command(mod) .parse(); diff --git a/packages/cli/test/write.test.ts b/packages/cli/test/write.test.ts index 697f8bed..ac2232e0 100644 --- a/packages/cli/test/write.test.ts +++ b/packages/cli/test/write.test.ts @@ -5,10 +5,10 @@ import { ethers, getDefaultProvider } from "ethers"; import yargs from "yargs/yargs"; import { temporaryWrite } from "tempy"; import mockStd from "mock-stdin"; -import { Database } from "@tableland/sdk"; import { getAccounts } from "@tableland/local"; +import { Database } from "@tableland/sdk"; import * as mod from "../src/commands/write.js"; -import { wait, logger } from "../src/utils.js"; +import { wait, logger, jsonFileAliases } from "../src/utils.js"; import { getResolverUndefinedMock } from "./mock.js"; import { TEST_TIMEOUT_FACTOR, TEST_PROVIDER_URL } from "./setup"; @@ -178,7 +178,7 @@ describe("commands/write", function () { setTimeout(() => { stdin.send("\n").end(); }, 100); - await yargs(["write", "--privateKey", privateKey, ...defaultArgs]) + await yargs(["write", ...defaultArgs, "--privateKey", privateKey]) .command(mod) .parse(); @@ -189,6 +189,52 @@ describe("commands/write", function () { ); }); + test("throws with invalid table aliases file", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + // Set up faux aliases file + const aliasesFilePath = "./invalid.json"; + + await yargs([ + "write", + "INSERT INTO table_aliases (id) VALUES (1);", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleError.getCall(0).firstArg; + equal(res, "invalid table aliases file"); + }); + + test("throws with invalid table alias definition", async function () { + const [account] = accounts; + const privateKey = account.privateKey.slice(2); + const consoleError = spy(logger, "error"); + // Set up faux aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + + await yargs([ + "write", + "INSERT INTO table_aliases (id) VALUES (1);", + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleError.getCall(0).firstArg; + match(res, /error validating name: table name has wrong format:/); + }); + test("passes when writing with local-tableland", async function () { const [account] = accounts; const privateKey = account.privateKey.slice(2); @@ -354,7 +400,7 @@ describe("commands/write", function () { // db is configged with account 1 const { meta } = await db.prepare("CREATE TABLE test_grant (a int);").all(); const tableName = meta.txn?.name ?? ""; - + console.log(tableName); const consoleError = spy(logger, "error"); // first ensure account 2 cannot insert @@ -523,4 +569,127 @@ describe("commands/write", function () { equal(results[0].id, 1); equal(results[0].data, data); }); + + test("passes with table aliases", async function () { + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta } = await db + .prepare("CREATE TABLE table_aliases (id int);") + .all(); + const name = meta.txn?.name ?? ""; + const prefix = meta.txn?.prefix ?? ""; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias = + Object.keys(nameMap).find((alias) => nameMap[alias] === name) ?? ""; + + equal(tableAlias, prefix); + + // Write to the table using the alias + const consoleLog = spy(logger, "log"); + await yargs([ + "write", + `insert into ${tableAlias} values (1);`, + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { transactionHash, link } = value.meta?.txn; + + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); + equal(link, undefined); + + // Make sure data was materialized (note aliases aren't enabled on `db`) + const { results } = await db + .prepare(`SELECT * FROM ${name}`) + .all<{ id: number }>(); + equal(results.length, 1); + equal(results[0].id, 1); + }); + + test("passes with table aliases when writing to two different tables", async function () { + const account = accounts[1]; + const privateKey = account.privateKey.slice(2); + // Set up test aliases file + const aliasesFilePath = await temporaryWrite(`{}`, { extension: "json" }); + // Create new db instance to enable aliases + const db = new Database({ + signer, + autoWait: true, + aliases: jsonFileAliases(aliasesFilePath), + }); + const { meta: meta1 } = await db + .prepare("create table multi_tbl_1 (a int, b text);") + .all(); + const { meta: meta2 } = await db + .prepare("create table multi_tbl_2 (a int, b text);") + .all(); + + const tableName1 = meta1.txn!.name; + const tablePrefix1 = meta1.txn!.prefix; + const tableName2 = meta2.txn!.name; + const tablePrefix2 = meta2.txn!.prefix; + + // Check the aliases file was updated and matches with the prefix + const nameMap = await jsonFileAliases(aliasesFilePath).read(); + const tableAlias1 = + Object.keys(nameMap).find((alias) => nameMap[alias] === tableName1) ?? ""; + const tableAlias2 = + Object.keys(nameMap).find((alias) => nameMap[alias] === tableName2) ?? ""; + equal(tableAlias1, tablePrefix1); + equal(tableAlias2, tablePrefix2); + + // Write to the tables using the aliases + const consoleLog = spy(logger, "log"); + await yargs([ + "write", + `insert into ${tableAlias1} (a, b) values (1, 'one'); + insert into ${tableAlias2} (a, b) values (2, 'two');`, + ...defaultArgs, + "--privateKey", + privateKey, + "--aliases", + aliasesFilePath, + ]) + .command(mod) + .parse(); + + const res = consoleLog.getCall(0).firstArg; + const value = JSON.parse(res); + const { transactionHash, link } = value.meta?.txn; + + equal(typeof transactionHash, "string"); + equal(transactionHash.startsWith("0x"), true); + equal(link, undefined); + + const results = await db.batch([ + db.prepare(`select * from ${tableName1};`), + db.prepare(`select * from ${tableName2};`), + ]); + + const result1 = (results[0] as any)?.results; + const result2 = (results[1] as any)?.results; + + equal(result1[0].a, 1); + equal(result1[0].b, "one"); + equal(result2[0].a, 2); + equal(result2[0].b, "two"); + }); });