From 55985cba62e490a6afe85f967474fa77aecfb95b Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 23 Jul 2024 17:13:05 +0100 Subject: [PATCH 1/8] make table shorthand native --- .../ts/config/v2/expandTableShorthand.ts | 91 ++++++ packages/store/ts/config/v2/input.ts | 20 +- packages/store/ts/config/v2/store.test.ts | 7 - packages/store/ts/config/v2/store.ts | 6 +- .../ts/config/v2/storeWithShorthands.test.ts | 296 ------------------ .../store/ts/config/v2/storeWithShorthands.ts | 55 ---- .../store/ts/config/v2/tableShorthand.test.ts | 151 --------- packages/store/ts/config/v2/tableShorthand.ts | 136 -------- packages/store/ts/config/v2/tables.test.ts | 167 ++++++++++ packages/store/ts/config/v2/tables.ts | 32 +- packages/store/ts/exports/index.ts | 2 +- 11 files changed, 289 insertions(+), 674 deletions(-) create mode 100644 packages/store/ts/config/v2/expandTableShorthand.ts delete mode 100644 packages/store/ts/config/v2/storeWithShorthands.test.ts delete mode 100644 packages/store/ts/config/v2/storeWithShorthands.ts delete mode 100644 packages/store/ts/config/v2/tableShorthand.test.ts delete mode 100644 packages/store/ts/config/v2/tableShorthand.ts create mode 100644 packages/store/ts/config/v2/tables.test.ts diff --git a/packages/store/ts/config/v2/expandTableShorthand.ts b/packages/store/ts/config/v2/expandTableShorthand.ts new file mode 100644 index 0000000000..f5bf4919f5 --- /dev/null +++ b/packages/store/ts/config/v2/expandTableShorthand.ts @@ -0,0 +1,91 @@ +import { FixedArrayAbiType, isFixedArrayAbiType, isStaticAbiType } from "@latticexyz/schema-type/internal"; +import { hasOwnKey, isObject } from "./generics"; +import { SchemaInput, ScopedSchemaInput, TableShorthandInput } from "./input"; +import { isSchemaInput } from "./schema"; +import { AbiTypeScope, Scope, getStaticAbiTypeKeys } from "./scope"; +import { ErrorMessage, conform } from "@arktype/util"; + +export const NoStaticKeyFieldError = + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option."; + +export type NoStaticKeyFieldError = ErrorMessage; + +export function isTableShorthandInput(shorthand: unknown): shorthand is TableShorthandInput { + return ( + typeof shorthand === "string" || + (isObject(shorthand) && Object.values(shorthand).every((value) => typeof value === "string")) + ); +} + +// We don't use `conform` here because the restrictions we're imposing here are not native to typescript +export type validateTableShorthand = input extends SchemaInput + ? // If a shorthand schema is provided, require it to have a static `id` field + "id" extends getStaticAbiTypeKeys + ? // Require all values to be valid types in this scope + conform> + : NoStaticKeyFieldError + : // If a fixed array type or a valid type from the scope is provided, accept it + input extends FixedArrayAbiType | keyof scope["types"] + ? input + : // If the input is not a valid shorthand, return the expected type + input extends string + ? keyof scope["types"] + : ScopedSchemaInput; + +export function validateTableShorthand( + shorthand: unknown, + scope: scope = AbiTypeScope as never, +): asserts shorthand is TableShorthandInput { + if (typeof shorthand === "string") { + if (isFixedArrayAbiType(shorthand) || hasOwnKey(scope.types, shorthand)) { + return; + } + throw new Error(`Invalid ABI type. \`${shorthand}\` not found in scope.`); + } + if (typeof shorthand === "object" && shorthand !== null) { + if (isSchemaInput(shorthand, scope)) { + if (hasOwnKey(shorthand, "id") && isStaticAbiType(scope.types[shorthand.id as keyof typeof scope.types])) { + return; + } + throw new Error(`Invalid schema. Expected an \`id\` field with a static ABI type or an explicit \`key\` option.`); + } + throw new Error(`Invalid schema. Are you using invalid types or missing types in your scope?`); + } + throw new Error(`Invalid table shorthand.`); +} + +export type expandTableShorthand = shorthand extends string + ? { + readonly schema: { + readonly id: "bytes32"; + readonly value: shorthand; + }; + readonly key: ["id"]; + } + : shorthand extends SchemaInput + ? { + readonly schema: shorthand; + readonly key: ["id"]; + } + : shorthand; + +export function expandTableShorthand(shorthand: shorthand): expandTableShorthand { + if (typeof shorthand === "string") { + return { + schema: { + id: "bytes32", + value: shorthand, + }, + key: ["id"], + } as never; + } + + if (isSchemaInput(shorthand)) { + return { + schema: shorthand, + key: ["id"], + } as never; + } + + return shorthand as never; +} diff --git a/packages/store/ts/config/v2/input.ts b/packages/store/ts/config/v2/input.ts index 058b07bba1..b074e5a8c1 100644 --- a/packages/store/ts/config/v2/input.ts +++ b/packages/store/ts/config/v2/input.ts @@ -1,7 +1,6 @@ import { Hex } from "viem"; import { Codegen, TableCodegen, TableDeploy, UserTypes } from "./output"; import { Scope } from "./scope"; -import { show } from "@arktype/util"; export type EnumsInput = { readonly [enumName: string]: readonly [string, ...string[]]; @@ -45,9 +44,12 @@ export type TableInput = { readonly deploy?: TableDeployInput; }; +export type TableShorthandInput = SchemaInput | string; + export type TablesInput = { - // remove label and namespace as these are set contextually - readonly [label: string]: Omit; + // remove label and namespace from table input as these are set contextually + // and allow defining a table using shorthand + readonly [label: string]: Omit | TableShorthandInput; }; export type CodegenInput = Partial; @@ -78,15 +80,3 @@ export type StoreInput = Omit & { readonly enums?: EnumsInput; readonly codegen?: CodegenInput; }; - -/******** Variations with shorthands ********/ - -export type TableShorthandInput = SchemaInput | string; - -export type TablesWithShorthandsInput = { - readonly [label: string]: TablesInput[string] | TableShorthandInput; -}; - -export type StoreWithShorthandsInput = show< - Omit & { readonly tables?: TablesWithShorthandsInput } ->; diff --git a/packages/store/ts/config/v2/store.test.ts b/packages/store/ts/config/v2/store.test.ts index 55eff2a974..18a50077b7 100644 --- a/packages/store/ts/config/v2/store.test.ts +++ b/packages/store/ts/config/v2/store.test.ts @@ -541,13 +541,6 @@ describe("defineStore", () => { ); }); - it("should throw if a string is passed in as schema", () => { - // @ts-expect-error Invalid table config - attest(() => defineStore({ tables: { Invalid: "uint256" } })) - .throws('Expected full table config, got `"uint256"`') - .type.errors("Expected full table config"); - }); - it("should show a type error if an invalid schema is passed in", () => { // @ts-expect-error Key `invalidKey` does not exist in TableInput attest(() => defineStore({ tables: { Invalid: { invalidKey: 1 } } })).type.errors( diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index 3c632c9ac4..8a7eb7079b 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -2,7 +2,7 @@ import { ErrorMessage, show, flatMorph, narrow } from "@arktype/util"; import { get, hasOwnKey, mergeIfUndefined } from "./generics"; import { UserTypes } from "./output"; import { CONFIG_DEFAULTS } from "./defaults"; -import { StoreInput } from "./input"; +import { StoreInput, TableInput } from "./input"; import { resolveTables, validateTables } from "./tables"; import { scopeWithUserTypes, validateUserTypes } from "./userTypes"; import { mapEnums, resolveEnums, scopeWithEnums } from "./enums"; @@ -10,6 +10,7 @@ import { resolveCodegen } from "./codegen"; import { resolveNamespacedTables } from "./namespacedTables"; import { resolveTable } from "./table"; import { resolveNamespace } from "./namespace"; +import { expandTableShorthand } from "./expandTableShorthand"; export type extendedScope = scopeWithEnums, scopeWithUserTypes>>; @@ -88,7 +89,8 @@ export function resolveStore(input: input): reso const namespace = input.namespace ?? CONFIG_DEFAULTS["namespace"]; const codegen = resolveCodegen(input.codegen); - const tablesInput = flatMorph(input.tables ?? {}, (label, table) => { + const tablesInput = flatMorph(input.tables ?? {}, (label, shorthand) => { + const table = expandTableShorthand(shorthand) as TableInput; return [ label, { diff --git a/packages/store/ts/config/v2/storeWithShorthands.test.ts b/packages/store/ts/config/v2/storeWithShorthands.test.ts deleted file mode 100644 index 963e4e4c52..0000000000 --- a/packages/store/ts/config/v2/storeWithShorthands.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { describe, it } from "vitest"; -import { defineStoreWithShorthands } from "./storeWithShorthands"; -import { attest } from "@arktype/attest"; -import { resourceToHex } from "@latticexyz/common"; -import { CODEGEN_DEFAULTS, TABLE_CODEGEN_DEFAULTS, TABLE_DEPLOY_DEFAULTS } from "./defaults"; -import { defineStore } from "./store"; -import { satisfy } from "@arktype/util"; -import { Store } from "./output"; - -describe("defineStoreWithShorthands", () => { - it("should accept a shorthand store config as input and expand it", () => { - const config = defineStoreWithShorthands({ tables: { Name: "address" } }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Name: { - label: "Name", - type: "table", - namespace: "" as string, - name: "Name" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), - schema: { - id: { - type: "bytes32", - internalType: "bytes32", - }, - value: { - type: "address", - internalType: "address", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - sourceDirectory: "src", - userTypes: {}, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - }); - - it("should satisfy the output type", () => { - const config = defineStoreWithShorthands({ - tables: { Name: { schema: { id: "address" }, key: ["id"] } }, - userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, - }); - - attest>(); - }); - - it("should accept an empty input", () => { - const config = defineStoreWithShorthands({}); - attest>(); - }); - - it("should accept a user type as input and expand it", () => { - const config = defineStoreWithShorthands({ - tables: { Name: "CustomType" }, - userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, - }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Name: { - label: "Name", - type: "table", - namespace: "" as string, - name: "Name" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), - schema: { - id: { - type: "bytes32", - internalType: "bytes32", - }, - value: { - type: "address", - internalType: "CustomType", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - sourceDirectory: "src", - userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - attest(expectedConfig); - }); - - it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { - const config = defineStoreWithShorthands({ - tables: { Example: { id: "address", name: "string", age: "uint256" } }, - }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Example: { - label: "Example", - type: "table", - namespace: "" as string, - name: "Example" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), - schema: { - id: { - type: "address", - internalType: "address", - }, - name: { - type: "string", - internalType: "string", - }, - age: { - type: "uint256", - internalType: "uint256", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - sourceDirectory: "src", - userTypes: {}, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - }); - - it("given a schema with a key field with static custom type, it should use `id` as single key", () => { - const config = defineStoreWithShorthands({ - tables: { Example: { id: "address", name: "string", age: "uint256" } }, - }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Example: { - label: "Example", - type: "table", - namespace: "" as string, - name: "Example" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), - schema: { - id: { - type: "address", - internalType: "address", - }, - name: { - type: "string", - internalType: "string", - }, - age: { - type: "uint256", - internalType: "uint256", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - sourceDirectory: "src", - userTypes: {}, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - }); - - it("should pass through full table config inputs", () => { - const config = defineStoreWithShorthands({ - tables: { - Example: { - schema: { id: "address", name: "string", age: "uint256" }, - key: ["age", "id"], - }, - }, - }); - const expected = defineStore({ - tables: { - Example: { - schema: { id: "address", name: "string", age: "uint256" }, - key: ["age", "id"], - }, - }, - }); - - attest(config).equals(expected); - }); - - it("should throw if the shorthand doesn't include a key field", () => { - attest(() => - defineStoreWithShorthands({ - tables: { - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - Example: { - name: "string", - age: "uint256", - }, - }, - }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("should throw if the shorthand config includes a non-static key field", () => { - attest(() => - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - defineStoreWithShorthands({ tables: { Example: { id: "string", name: "string", age: "uint256" } } }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("should throw if the shorthand config includes a non-static user type as key field", () => { - attest(() => - defineStoreWithShorthands({ - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - tables: { Example: { id: "dynamic", name: "string", age: "uint256" } }, - userTypes: { - dynamic: { type: "string", filePath: "path/to/file" }, - static: { type: "address", filePath: "path/to/file" }, - }, - }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("should throw if the shorthand key is neither a custom nor ABI type", () => { - // @ts-expect-error Type '"NotAnAbiType"' is not assignable to type 'AbiType' - attest(() => defineStoreWithShorthands({ tables: { Invalid: "NotAnAbiType" } })) - .throws("Invalid ABI type. `NotAnAbiType` not found in scope.") - .type.errors(`Type '"NotAnAbiType"' is not assignable to type 'AbiType'`); - }); -}); diff --git a/packages/store/ts/config/v2/storeWithShorthands.ts b/packages/store/ts/config/v2/storeWithShorthands.ts deleted file mode 100644 index 74413c6127..0000000000 --- a/packages/store/ts/config/v2/storeWithShorthands.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { mapObject } from "@latticexyz/common/utils"; -import { resolveStore, validateStore, extendedScope } from "./store"; -import { - isTableShorthandInput, - resolveTableShorthand, - resolveTablesWithShorthands, - validateTablesWithShorthands, -} from "./tableShorthand"; -import { hasOwnKey, isObject } from "./generics"; -import { StoreWithShorthandsInput } from "./input"; - -export type validateStoreWithShorthands = { - [key in keyof store]: key extends "tables" - ? validateTablesWithShorthands> - : validateStore[key]; -}; - -export function validateStoreWithShorthands(store: unknown): asserts store is StoreWithShorthandsInput { - const scope = extendedScope(store); - if (hasOwnKey(store, "tables") && isObject(store.tables)) { - validateTablesWithShorthands(store.tables, scope); - } -} - -export type resolveStoreWithShorthands = resolveStore<{ - [key in keyof store]: key extends "tables" - ? resolveTablesWithShorthands> - : store[key]; -}>; - -export function resolveStoreWithShorthands( - store: store, -): resolveStoreWithShorthands { - const scope = extendedScope(store); - const tables = store.tables - ? mapObject(store.tables, (table) => { - return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; - }) - : null; - - const fullConfig = { - ...store, - ...(tables ? { tables } : null), - }; - - validateStore(fullConfig); - return resolveStore(fullConfig) as never; -} - -export function defineStoreWithShorthands( - store: validateStoreWithShorthands, -): resolveStoreWithShorthands { - validateStoreWithShorthands(store); - return resolveStoreWithShorthands(store) as never; -} diff --git a/packages/store/ts/config/v2/tableShorthand.test.ts b/packages/store/ts/config/v2/tableShorthand.test.ts deleted file mode 100644 index b3813b28d3..0000000000 --- a/packages/store/ts/config/v2/tableShorthand.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { describe, it } from "vitest"; -import { attest } from "@arktype/attest"; -import { AbiTypeScope, extendScope } from "./scope"; -import { defineTableShorthand, NoStaticKeyFieldError } from "./tableShorthand"; - -describe("defineTableShorthand", () => { - it("should expand a single ABI type into a id/value schema", () => { - const table = defineTableShorthand("address"); - - attest<{ - schema: { - id: "bytes32"; - value: "address"; - }; - key: ["id"]; - }>(table).equals({ - schema: { - id: "bytes32", - value: "address", - }, - key: ["id"], - }); - }); - - it("should expand a fixed array ABI type into a id/value schema", () => { - const table = defineTableShorthand("address[4]"); - - attest<{ - schema: { - id: "bytes32"; - value: "address[4]"; - }; - key: ["id"]; - }>(table).equals({ - schema: { - id: "bytes32", - value: "address[4]", - }, - key: ["id"], - }); - }); - - it("should expand a single custom type into a id/value schema", () => { - const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); - const table = defineTableShorthand("CustomType", scope); - - attest<{ - schema: { - id: "bytes32"; - value: "CustomType"; - }; - key: ["id"]; - }>(table).equals({ - schema: { - id: "bytes32", - value: "CustomType", - }, - key: ["id"], - }); - }); - - it("should throw if the provided shorthand is not an ABI type and no user types are provided", () => { - attest(() => - // @ts-expect-error Argument of type '"NotAnAbiType"' is not assignable to parameter of type AbiType' - defineTableShorthand("NotAnAbiType"), - ) - .throws("Invalid ABI type. `NotAnAbiType` not found in scope.") - .type.errors(`Argument of type '"NotAnAbiType"' is not assignable to parameter of type 'AbiType'.`); - }); - - it("should throw if the provided shorthand is not a user type", () => { - const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); - - attest(() => - // @ts-expect-error Argument of type '"NotACustomType"' is not assignable to parameter of type AbiType | "CustomType" - defineTableShorthand("NotACustomType", scope), - ) - .throws("Invalid ABI type. `NotACustomType` not found in scope.") - .type.errors( - `Argument of type '"NotACustomType"' is not assignable to parameter of type 'AbiType | "CustomType"'.`, - ); - }); - - it("should use `id` as single key if it has a static ABI type", () => { - const table = defineTableShorthand({ id: "address", name: "string", age: "uint256" }); - - attest<{ - schema: { - id: "address"; - name: "string"; - age: "uint256"; - }; - key: ["id"]; - }>(table).equals({ - schema: { - id: "address", - name: "string", - age: "uint256", - }, - key: ["id"], - }); - }); - - it("should throw an error if the shorthand doesn't include an `id` field", () => { - attest(() => - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - defineTableShorthand({ name: "string", age: "uint256" }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("should throw an error if the shorthand config includes a non-static `id` field", () => { - attest(() => - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - defineTableShorthand({ id: "string", name: "string", age: "uint256" }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("should throw an error if an invalid type is passed in", () => { - attest(() => - // @ts-expect-error Type '"NotACustomType"' is not assignable to type 'AbiType'. - defineTableShorthand({ id: "uint256", name: "NotACustomType" }), - ) - .throws("Invalid schema. Are you using invalid types or missing types in your scope?") - .type.errors(`Type '"NotACustomType"' is not assignable to type 'AbiType'.`); - }); - - it("should use `id` as single key if it has a static custom type", () => { - const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); - const table = defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope); - - attest<{ - schema: { id: "CustomType"; name: "string"; age: "uint256" }; - key: ["id"]; - }>(table).equals({ - schema: { id: "CustomType", name: "string", age: "uint256" }, - key: ["id"], - }); - }); - - it("should throw an error if `id` is not a custom static type", () => { - const scope = extendScope(AbiTypeScope, { CustomType: "bytes" }); - attest(() => - // @ts-expect-error "Error: Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option." - defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope), - ).throwsAndHasTypeError(NoStaticKeyFieldError); - }); -}); diff --git a/packages/store/ts/config/v2/tableShorthand.ts b/packages/store/ts/config/v2/tableShorthand.ts deleted file mode 100644 index 331cb91b91..0000000000 --- a/packages/store/ts/config/v2/tableShorthand.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ErrorMessage, conform } from "@arktype/util"; -import { FixedArrayAbiType, isFixedArrayAbiType, isStaticAbiType } from "@latticexyz/schema-type/internal"; -import { get, hasOwnKey, isObject } from "./generics"; -import { isSchemaInput } from "./schema"; -import { AbiTypeScope, Scope, getStaticAbiTypeKeys } from "./scope"; -import { SchemaInput, ScopedSchemaInput, TablesWithShorthandsInput } from "./input"; -import { TableShorthandInput } from "./input"; -import { ValidateTableOptions, validateTable } from "./table"; - -export const NoStaticKeyFieldError = - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option."; - -export type NoStaticKeyFieldError = ErrorMessage; - -export function isTableShorthandInput(shorthand: unknown): shorthand is TableShorthandInput { - return ( - typeof shorthand === "string" || - (isObject(shorthand) && Object.values(shorthand).every((value) => typeof value === "string")) - ); -} - -export type validateTableWithShorthand< - table, - scope extends Scope = AbiTypeScope, - options extends ValidateTableOptions = { inStoreContext: boolean }, -> = table extends TableShorthandInput ? validateTableShorthand : validateTable; - -// We don't use `conform` here because the restrictions we're imposing here are not native to typescript -export type validateTableShorthand = input extends SchemaInput - ? // If a shorthand schema is provided, require it to have a static `id` field - "id" extends getStaticAbiTypeKeys - ? // Require all values to be valid types in this scope - conform> - : NoStaticKeyFieldError - : // If a fixed array type is provided, accept it - input extends FixedArrayAbiType - ? input - : // If a valid type from the scope is provided, accept it - input extends keyof scope["types"] - ? input - : // If the input is not a valid shorthand, return the expected type - input extends string - ? keyof scope["types"] - : ScopedSchemaInput; - -export function validateTableShorthand( - shorthand: unknown, - scope: scope = AbiTypeScope as never, -): asserts shorthand is TableShorthandInput { - if (typeof shorthand === "string") { - if (isFixedArrayAbiType(shorthand) || hasOwnKey(scope.types, shorthand)) { - return; - } - throw new Error(`Invalid ABI type. \`${shorthand}\` not found in scope.`); - } - if (typeof shorthand === "object" && shorthand !== null) { - if (isSchemaInput(shorthand, scope)) { - if (hasOwnKey(shorthand, "id") && isStaticAbiType(scope.types[shorthand.id as keyof typeof scope.types])) { - return; - } - throw new Error(`Invalid schema. Expected an \`id\` field with a static ABI type or an explicit \`key\` option.`); - } - throw new Error(`Invalid schema. Are you using invalid types or missing types in your scope?`); - } - throw new Error(`Invalid table shorthand.`); -} - -export type resolveTableShorthand = shorthand extends FixedArrayAbiType - ? { schema: { id: "bytes32"; value: shorthand }; key: ["id"] } - : shorthand extends keyof scope["types"] - ? { schema: { id: "bytes32"; value: shorthand }; key: ["id"] } - : shorthand extends SchemaInput - ? "id" extends getStaticAbiTypeKeys - ? // If the shorthand includes a static field called `id`, use it as `key` - { schema: shorthand; key: ["id"] } - : never - : never; - -export function resolveTableShorthand( - shorthand: shorthand, - scope: scope = AbiTypeScope as never, -): resolveTableShorthand { - if (isSchemaInput(shorthand, scope)) { - return { - schema: shorthand, - key: ["id"], - } as never; - } - - return { - schema: { - id: "bytes32", - value: shorthand, - }, - key: ["id"], - } as never; -} - -export function defineTableShorthand( - shorthand: validateTableShorthand, - scope: scope = AbiTypeScope as never, -): resolveTableShorthand { - validateTableShorthand(shorthand, scope); - return resolveTableShorthand(shorthand, scope) as never; -} - -/** - * If a shorthand is provided, it is resolved to a full config. - * If a full config is provided, it is passed through. - */ -export type resolveTableWithShorthand = table extends TableShorthandInput - ? resolveTableShorthand - : table; - -export type resolveTablesWithShorthands = { - [label in keyof input]: resolveTableWithShorthand; -}; - -export type validateTablesWithShorthands = { - [label in keyof tables]: validateTableWithShorthand; -}; - -export function validateTablesWithShorthands( - tables: unknown, - scope: scope, -): asserts tables is TablesWithShorthandsInput { - if (isObject(tables)) { - for (const label of Object.keys(tables)) { - if (isTableShorthandInput(get(tables, label))) { - validateTableShorthand(get(tables, label), scope); - } else { - validateTable(get(tables, label), scope); - } - } - } -} diff --git a/packages/store/ts/config/v2/tables.test.ts b/packages/store/ts/config/v2/tables.test.ts new file mode 100644 index 0000000000..63d1ab9cce --- /dev/null +++ b/packages/store/ts/config/v2/tables.test.ts @@ -0,0 +1,167 @@ +import { describe, it } from "vitest"; +import { attest } from "@arktype/attest"; +import { AbiTypeScope } from "./scope"; +import { resolveTables, validateTables } from "./tables"; + +describe("validateTables", () => { + it("should validate shorthands", () => { + attest(() => validateTables({ Short: "uint256" }, AbiTypeScope)); + attest(() => validateTables({ ShortSchema: { first: "uint256", second: "bool" } }, AbiTypeScope)); + }); + + it.skip("should reject invalid shorthand", () => { + attest(() => validateTables({ Short: "nonexistent" }, AbiTypeScope)) + .throws("Invalid ABI type. `nonexistent` not found in scope.") + .type.errors("Invalid ABI type. `nonexistent` not found in scope."); + attest(() => validateTables({ ShortSchema: { first: "nonexistent", second: "bool" } }, AbiTypeScope)) + .throws("Invalid ABI type. `nonexistent` not found in scope.") + .type.errors("Invalid ABI type. `nonexistent` not found in scope."); + }); +}); + +describe("resolveTables", () => { + it("expands shorthand table schemas", () => { + const tables = resolveTables( + { + Short: "uint256", + ShortSchema: { first: "uint256", second: "bool" }, + }, + AbiTypeScope, + ); + + const expectedTables = { + Short: { + schema: { + id: { type: "bytes32", internalType: "bytes32" }, + value: { type: "uint256", internalType: "uint256" }, + }, + key: ["id"], + }, + ShortSchema: { + schema: { + id: { type: "bytes32", internalType: "bytes32" }, + first: { type: "uint256", internalType: "uint256" }, + second: { type: "bool", internalType: "bool" }, + }, + key: ["id"], + }, + } as const; + + attest(tables.Short.schema).equals(expectedTables.Short.schema); + attest(tables.Short.key).equals(expectedTables.Short.key); + + attest(tables.ShortSchema.schema).equals( + expectedTables.ShortSchema.schema, + ); + attest(tables.ShortSchema.key).equals(expectedTables.ShortSchema.key); + }); + + // it("should expand a single custom type into a id/value schema", () => { + // const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + // const table = defineTableShorthand("CustomType", scope); + + // attest<{ + // schema: { + // id: "bytes32"; + // value: "CustomType"; + // }; + // key: ["id"]; + // }>(table).equals({ + // schema: { + // id: "bytes32", + // value: "CustomType", + // }, + // key: ["id"], + // }); + // }); + + // it("should throw if the provided shorthand is not an ABI type and no user types are provided", () => { + // attest(() => + // // @ts-expect-error Argument of type '"NotAnAbiType"' is not assignable to parameter of type AbiType' + // defineTableShorthand("NotAnAbiType"), + // ) + // .throws("Invalid ABI type. `NotAnAbiType` not found in scope.") + // .type.errors(`Argument of type '"NotAnAbiType"' is not assignable to parameter of type 'AbiType'.`); + // }); + + // it("should throw if the provided shorthand is not a user type", () => { + // const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + + // attest(() => + // // @ts-expect-error Argument of type '"NotACustomType"' is not assignable to parameter of type AbiType | "CustomType" + // defineTableShorthand("NotACustomType", scope), + // ) + // .throws("Invalid ABI type. `NotACustomType` not found in scope.") + // .type.errors( + // `Argument of type '"NotACustomType"' is not assignable to parameter of type 'AbiType | "CustomType"'.`, + // ); + // }); + + // it("should use `id` as single key if it has a static ABI type", () => { + // const table = defineTableShorthand({ id: "address", name: "string", age: "uint256" }); + + // attest<{ + // schema: { + // id: "address"; + // name: "string"; + // age: "uint256"; + // }; + // key: ["id"]; + // }>(table).equals({ + // schema: { + // id: "address", + // name: "string", + // age: "uint256", + // }, + // key: ["id"], + // }); + // }); + + // it("should throw an error if the shorthand doesn't include an `id` field", () => { + // attest(() => + // // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + // defineTableShorthand({ name: "string", age: "uint256" }), + // ).throwsAndHasTypeError( + // "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + // ); + // }); + + // it("should throw an error if the shorthand config includes a non-static `id` field", () => { + // attest(() => + // // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + // defineTableShorthand({ id: "string", name: "string", age: "uint256" }), + // ).throwsAndHasTypeError( + // "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + // ); + // }); + + // it("should throw an error if an invalid type is passed in", () => { + // attest(() => + // // @ts-expect-error Type '"NotACustomType"' is not assignable to type 'AbiType'. + // defineTableShorthand({ id: "uint256", name: "NotACustomType" }), + // ) + // .throws("Invalid schema. Are you using invalid types or missing types in your scope?") + // .type.errors(`Type '"NotACustomType"' is not assignable to type 'AbiType'.`); + // }); + + // it("should use `id` as single key if it has a static custom type", () => { + // const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + // const table = defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope); + + // attest<{ + // schema: { id: "CustomType"; name: "string"; age: "uint256" }; + // key: ["id"]; + // }>(table).equals({ + // schema: { id: "CustomType", name: "string", age: "uint256" }, + // key: ["id"], + // }); + // }); + + // it("should throw an error if `id` is not a custom static type", () => { + // const scope = extendScope(AbiTypeScope, { CustomType: "bytes" }); + // attest(() => + // // @ts-expect-error "Error: Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option." + // defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope), + // ).throwsAndHasTypeError(NoStaticKeyFieldError); + // }); +}); diff --git a/packages/store/ts/config/v2/tables.ts b/packages/store/ts/config/v2/tables.ts index f6ab5f60ad..9a5ab96c3d 100644 --- a/packages/store/ts/config/v2/tables.ts +++ b/packages/store/ts/config/v2/tables.ts @@ -1,13 +1,16 @@ import { ErrorMessage, show } from "@arktype/util"; import { isObject, mergeIfUndefined } from "./generics"; -import { TablesInput } from "./input"; +import { TableShorthandInput, TablesInput } from "./input"; import { Scope, AbiTypeScope } from "./scope"; import { validateTable, resolveTable } from "./table"; +import { expandTableShorthand, isTableShorthandInput, validateTableShorthand } from "./expandTableShorthand"; export type validateTables = { - [label in keyof tables]: tables[label] extends object - ? validateTable - : ErrorMessage<`Expected full table config.`>; + [label in keyof tables]: tables[label] extends TableShorthandInput + ? validateTableShorthand + : tables[label] extends object + ? validateTable + : ErrorMessage<`Expected tables config.`>; }; export function validateTables( @@ -16,24 +19,31 @@ export function validateTables( ): asserts input is TablesInput { if (isObject(input)) { for (const table of Object.values(input)) { - validateTable(table, scope, { inStoreContext: true }); + if (isTableShorthandInput(table)) { + validateTableShorthand(table, scope); + } else { + validateTable(table, scope, { inStoreContext: true }); + } } return; } - throw new Error(`Expected store config, received ${JSON.stringify(input)}`); + throw new Error(`Expected tables config, received ${JSON.stringify(input)}`); } -export type resolveTables = show<{ - readonly [label in keyof tables]: resolveTable, scope>; -}>; +export type resolveTables = { + readonly [label in keyof tables]: resolveTable< + mergeIfUndefined, { label: label }>, + scope + >; +}; export function resolveTables( tables: tables, scope: scope, -): resolveTables { +): show> { return Object.fromEntries( Object.entries(tables).map(([label, table]) => { - return [label, resolveTable(mergeIfUndefined(table, { label }), scope)]; + return [label, resolveTable(mergeIfUndefined(expandTableShorthand(table), { label }), scope)]; }), ) as never; } diff --git a/packages/store/ts/exports/index.ts b/packages/store/ts/exports/index.ts index a44a7a2658..7048492f24 100644 --- a/packages/store/ts/exports/index.ts +++ b/packages/store/ts/exports/index.ts @@ -16,5 +16,5 @@ export { export { storeEventsAbi } from "../storeEventsAbi"; export type { StoreEventsAbi, StoreEventsAbiItem } from "../storeEventsAbi"; -export { defineStoreWithShorthands as defineStore } from "../config/v2/storeWithShorthands"; +export { defineStore } from "../config/v2/store"; export type { Store } from "../config/v2/output"; From d98f2bbad030441aaa449c6e608242364069ce9e Mon Sep 17 00:00:00 2001 From: alvrs Date: Tue, 23 Jul 2024 11:08:46 -0700 Subject: [PATCH 2/8] use validator --- packages/store/ts/config/v2/tables.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/store/ts/config/v2/tables.ts b/packages/store/ts/config/v2/tables.ts index 9a5ab96c3d..6ee45f7baa 100644 --- a/packages/store/ts/config/v2/tables.ts +++ b/packages/store/ts/config/v2/tables.ts @@ -37,10 +37,11 @@ export type resolveTables = { >; }; -export function resolveTables( - tables: tables, +export function resolveTables( + tables: validateTables, scope: scope, ): show> { + validateTables(tables, scope); return Object.fromEntries( Object.entries(tables).map(([label, table]) => { return [label, resolveTable(mergeIfUndefined(expandTableShorthand(table), { label }), scope)]; From 0e08a9562b2befaf1ab3bdd6a9e92ffee10e3baf Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 23 Jul 2024 19:47:26 +0100 Subject: [PATCH 3/8] fix ups --- .../ts/config/v2/expandTableShorthand.test.ts | 151 ++++++++++++++++++ .../ts/config/v2/expandTableShorthand.ts | 15 +- packages/store/ts/config/v2/tables.test.ts | 151 ++++-------------- packages/store/ts/config/v2/tables.ts | 19 ++- 4 files changed, 207 insertions(+), 129 deletions(-) create mode 100644 packages/store/ts/config/v2/expandTableShorthand.test.ts diff --git a/packages/store/ts/config/v2/expandTableShorthand.test.ts b/packages/store/ts/config/v2/expandTableShorthand.test.ts new file mode 100644 index 0000000000..119b390705 --- /dev/null +++ b/packages/store/ts/config/v2/expandTableShorthand.test.ts @@ -0,0 +1,151 @@ +import { describe, it } from "vitest"; +import { attest } from "@arktype/attest"; +import { AbiTypeScope, extendScope } from "./scope"; +import { NoStaticKeyFieldError, defineTableShorthand } from "./expandTableShorthand"; + +describe("defineTableShorthand", () => { + it("should expand a single ABI type into a id/value schema", () => { + const table = defineTableShorthand("address"); + + attest<{ + schema: { + id: "bytes32"; + value: "address"; + }; + key: ["id"]; + }>(table).equals({ + schema: { + id: "bytes32", + value: "address", + }, + key: ["id"], + }); + }); + + it("should expand a fixed array ABI type into a id/value schema", () => { + const table = defineTableShorthand("address[4]"); + + attest<{ + schema: { + id: "bytes32"; + value: "address[4]"; + }; + key: ["id"]; + }>(table).equals({ + schema: { + id: "bytes32", + value: "address[4]", + }, + key: ["id"], + }); + }); + + it("should expand a single custom type into a id/value schema", () => { + const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + const table = defineTableShorthand("CustomType", scope); + + attest<{ + schema: { + id: "bytes32"; + value: "CustomType"; + }; + key: ["id"]; + }>(table).equals({ + schema: { + id: "bytes32", + value: "CustomType", + }, + key: ["id"], + }); + }); + + it("should throw if the provided shorthand is not an ABI type and no user types are provided", () => { + attest(() => + // @ts-expect-error Argument of type '"NotAnAbiType"' is not assignable to parameter of type AbiType' + defineTableShorthand("NotAnAbiType"), + ) + .throws("Invalid ABI type. `NotAnAbiType` not found in scope.") + .type.errors(`Argument of type '"NotAnAbiType"' is not assignable to parameter of type 'AbiType'.`); + }); + + it("should throw if the provided shorthand is not a user type", () => { + const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + + attest(() => + // @ts-expect-error Argument of type '"NotACustomType"' is not assignable to parameter of type AbiType | "CustomType" + defineTableShorthand("NotACustomType", scope), + ) + .throws("Invalid ABI type. `NotACustomType` not found in scope.") + .type.errors( + `Argument of type '"NotACustomType"' is not assignable to parameter of type 'AbiType | "CustomType"'.`, + ); + }); + + it("should use `id` as single key if it has a static ABI type", () => { + const table = defineTableShorthand({ id: "address", name: "string", age: "uint256" }); + + attest<{ + schema: { + id: "address"; + name: "string"; + age: "uint256"; + }; + key: ["id"]; + }>(table).equals({ + schema: { + id: "address", + name: "string", + age: "uint256", + }, + key: ["id"], + }); + }); + + it("should throw an error if the shorthand doesn't include an `id` field", () => { + attest(() => + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + defineTableShorthand({ name: "string", age: "uint256" }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("should throw an error if the shorthand config includes a non-static `id` field", () => { + attest(() => + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + defineTableShorthand({ id: "string", name: "string", age: "uint256" }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("should throw an error if an invalid type is passed in", () => { + attest(() => + // @ts-expect-error Type '"NotACustomType"' is not assignable to type 'AbiType'. + defineTableShorthand({ id: "uint256", name: "NotACustomType" }), + ) + .throws("Invalid schema. Are you using invalid types or missing types in your scope?") + .type.errors(`Type '"NotACustomType"' is not assignable to type 'AbiType'.`); + }); + + it("should use `id` as single key if it has a static custom type", () => { + const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + const table = defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope); + + attest<{ + schema: { id: "CustomType"; name: "string"; age: "uint256" }; + key: ["id"]; + }>(table).equals({ + schema: { id: "CustomType", name: "string", age: "uint256" }, + key: ["id"], + }); + }); + + it("should throw an error if `id` is not a custom static type", () => { + const scope = extendScope(AbiTypeScope, { CustomType: "bytes" }); + attest(() => + // @ts-expect-error "Error: Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option." + defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope), + ).throwsAndHasTypeError(NoStaticKeyFieldError); + }); +}); diff --git a/packages/store/ts/config/v2/expandTableShorthand.ts b/packages/store/ts/config/v2/expandTableShorthand.ts index f5bf4919f5..2da4c7bd38 100644 --- a/packages/store/ts/config/v2/expandTableShorthand.ts +++ b/packages/store/ts/config/v2/expandTableShorthand.ts @@ -69,7 +69,10 @@ export type expandTableShorthand = shorthand extends string } : shorthand; -export function expandTableShorthand(shorthand: shorthand): expandTableShorthand { +export function expandTableShorthand( + shorthand: shorthand, + scope: scope, +): expandTableShorthand { if (typeof shorthand === "string") { return { schema: { @@ -80,7 +83,7 @@ export function expandTableShorthand(shorthand: shorthand): expandTab } as never; } - if (isSchemaInput(shorthand)) { + if (isSchemaInput(shorthand, scope)) { return { schema: shorthand, key: ["id"], @@ -89,3 +92,11 @@ export function expandTableShorthand(shorthand: shorthand): expandTab return shorthand as never; } + +export function defineTableShorthand( + input: validateTableShorthand, + scope: scope = AbiTypeScope as unknown as scope, +): expandTableShorthand { + validateTableShorthand(input, scope); + return expandTableShorthand(input, scope) as never; +} diff --git a/packages/store/ts/config/v2/tables.test.ts b/packages/store/ts/config/v2/tables.test.ts index 63d1ab9cce..28a6a12c0f 100644 --- a/packages/store/ts/config/v2/tables.test.ts +++ b/packages/store/ts/config/v2/tables.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "vitest"; import { attest } from "@arktype/attest"; -import { AbiTypeScope } from "./scope"; -import { resolveTables, validateTables } from "./tables"; +import { AbiTypeScope, extendScope } from "./scope"; +import { defineTables, validateTables } from "./tables"; describe("validateTables", () => { it("should validate shorthands", () => { @@ -9,22 +9,23 @@ describe("validateTables", () => { attest(() => validateTables({ ShortSchema: { first: "uint256", second: "bool" } }, AbiTypeScope)); }); - it.skip("should reject invalid shorthand", () => { - attest(() => validateTables({ Short: "nonexistent" }, AbiTypeScope)) - .throws("Invalid ABI type. `nonexistent` not found in scope.") - .type.errors("Invalid ABI type. `nonexistent` not found in scope."); - attest(() => validateTables({ ShortSchema: { first: "nonexistent", second: "bool" } }, AbiTypeScope)) - .throws("Invalid ABI type. `nonexistent` not found in scope.") - .type.errors("Invalid ABI type. `nonexistent` not found in scope."); + it("should reject invalid shorthand", () => { + // TODO: type errors too? + attest(() => validateTables({ Short: "nonexistent" }, AbiTypeScope)).throws( + "Invalid ABI type. `nonexistent` not found in scope.", + ); + attest(() => validateTables({ ShortSchema: { first: "nonexistent", second: "bool" } }, AbiTypeScope)).throws( + "Invalid schema. Are you using invalid types or missing types in your scope?", + ); }); }); -describe("resolveTables", () => { +describe("defineTables", () => { it("expands shorthand table schemas", () => { - const tables = resolveTables( + const tables = defineTables( { Short: "uint256", - ShortSchema: { first: "uint256", second: "bool" }, + ShortSchema: { id: "uint256", exists: "bool" }, }, AbiTypeScope, ); @@ -39,9 +40,8 @@ describe("resolveTables", () => { }, ShortSchema: { schema: { - id: { type: "bytes32", internalType: "bytes32" }, - first: { type: "uint256", internalType: "uint256" }, - second: { type: "bool", internalType: "bool" }, + id: { type: "uint256", internalType: "uint256" }, + exists: { type: "bool", internalType: "bool" }, }, key: ["id"], }, @@ -56,112 +56,21 @@ describe("resolveTables", () => { attest(tables.ShortSchema.key).equals(expectedTables.ShortSchema.key); }); - // it("should expand a single custom type into a id/value schema", () => { - // const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); - // const table = defineTableShorthand("CustomType", scope); - - // attest<{ - // schema: { - // id: "bytes32"; - // value: "CustomType"; - // }; - // key: ["id"]; - // }>(table).equals({ - // schema: { - // id: "bytes32", - // value: "CustomType", - // }, - // key: ["id"], - // }); - // }); - - // it("should throw if the provided shorthand is not an ABI type and no user types are provided", () => { - // attest(() => - // // @ts-expect-error Argument of type '"NotAnAbiType"' is not assignable to parameter of type AbiType' - // defineTableShorthand("NotAnAbiType"), - // ) - // .throws("Invalid ABI type. `NotAnAbiType` not found in scope.") - // .type.errors(`Argument of type '"NotAnAbiType"' is not assignable to parameter of type 'AbiType'.`); - // }); - - // it("should throw if the provided shorthand is not a user type", () => { - // const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); - - // attest(() => - // // @ts-expect-error Argument of type '"NotACustomType"' is not assignable to parameter of type AbiType | "CustomType" - // defineTableShorthand("NotACustomType", scope), - // ) - // .throws("Invalid ABI type. `NotACustomType` not found in scope.") - // .type.errors( - // `Argument of type '"NotACustomType"' is not assignable to parameter of type 'AbiType | "CustomType"'.`, - // ); - // }); - - // it("should use `id` as single key if it has a static ABI type", () => { - // const table = defineTableShorthand({ id: "address", name: "string", age: "uint256" }); - - // attest<{ - // schema: { - // id: "address"; - // name: "string"; - // age: "uint256"; - // }; - // key: ["id"]; - // }>(table).equals({ - // schema: { - // id: "address", - // name: "string", - // age: "uint256", - // }, - // key: ["id"], - // }); - // }); - - // it("should throw an error if the shorthand doesn't include an `id` field", () => { - // attest(() => - // // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - // defineTableShorthand({ name: "string", age: "uint256" }), - // ).throwsAndHasTypeError( - // "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - // ); - // }); - - // it("should throw an error if the shorthand config includes a non-static `id` field", () => { - // attest(() => - // // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - // defineTableShorthand({ id: "string", name: "string", age: "uint256" }), - // ).throwsAndHasTypeError( - // "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - // ); - // }); + it("should expand a single custom type into a id/value schema", () => { + const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); + const tables = defineTables({ Custom: "CustomType" }, scope); - // it("should throw an error if an invalid type is passed in", () => { - // attest(() => - // // @ts-expect-error Type '"NotACustomType"' is not assignable to type 'AbiType'. - // defineTableShorthand({ id: "uint256", name: "NotACustomType" }), - // ) - // .throws("Invalid schema. Are you using invalid types or missing types in your scope?") - // .type.errors(`Type '"NotACustomType"' is not assignable to type 'AbiType'.`); - // }); - - // it("should use `id` as single key if it has a static custom type", () => { - // const scope = extendScope(AbiTypeScope, { CustomType: "uint256" }); - // const table = defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope); - - // attest<{ - // schema: { id: "CustomType"; name: "string"; age: "uint256" }; - // key: ["id"]; - // }>(table).equals({ - // schema: { id: "CustomType", name: "string", age: "uint256" }, - // key: ["id"], - // }); - // }); + const expectedTables = { + Custom: { + schema: { + id: { type: "bytes32", internalType: "bytes32" }, + value: { type: "uint256", internalType: "CustomType" }, + }, + key: ["id"], + }, + } as const; - // it("should throw an error if `id` is not a custom static type", () => { - // const scope = extendScope(AbiTypeScope, { CustomType: "bytes" }); - // attest(() => - // // @ts-expect-error "Error: Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option." - // defineTableShorthand({ id: "CustomType", name: "string", age: "uint256" }, scope), - // ).throwsAndHasTypeError(NoStaticKeyFieldError); - // }); + attest(tables.Custom.schema).equals(expectedTables.Custom.schema); + attest(tables.Custom.key).equals(expectedTables.Custom.key); + }); }); diff --git a/packages/store/ts/config/v2/tables.ts b/packages/store/ts/config/v2/tables.ts index 6ee45f7baa..efa72527ff 100644 --- a/packages/store/ts/config/v2/tables.ts +++ b/packages/store/ts/config/v2/tables.ts @@ -32,19 +32,26 @@ export function validateTables( export type resolveTables = { readonly [label in keyof tables]: resolveTable< - mergeIfUndefined, { label: label }>, + mergeIfUndefined, { readonly label: label }>, scope >; }; -export function resolveTables( - tables: validateTables, +export function resolveTables( + tables: tables, scope: scope, -): show> { - validateTables(tables, scope); +): resolveTables { return Object.fromEntries( Object.entries(tables).map(([label, table]) => { - return [label, resolveTable(mergeIfUndefined(expandTableShorthand(table), { label }), scope)]; + return [label, resolveTable(mergeIfUndefined(expandTableShorthand(table, scope), { label }), scope)]; }), ) as never; } + +export function defineTables( + input: validateTables, + scope: scope = AbiTypeScope as never, +): show> { + validateTables(input, scope); + return resolveTables(input, scope) as never; +} From becc5f84ed6c19251dbaf1bf19163537f3fa7a73 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 23 Jul 2024 19:55:23 +0100 Subject: [PATCH 4/8] convert world too --- packages/store/ts/config/v2/index.ts | 1 - packages/store/ts/config/v2/namespace.ts | 5 +- packages/store/ts/config/v2/store.ts | 4 +- ...orthand.test.ts => tableShorthand.test.ts} | 2 +- ...andTableShorthand.ts => tableShorthand.ts} | 0 packages/store/ts/config/v2/tables.ts | 2 +- packages/world/ts/config/v2/input.ts | 6 +- .../ts/config/v2/worldWithShorthands.test.ts | 435 ------------------ .../world/ts/config/v2/worldWithShorthands.ts | 64 --- packages/world/ts/exports/index.ts | 2 +- 10 files changed, 9 insertions(+), 512 deletions(-) rename packages/store/ts/config/v2/{expandTableShorthand.test.ts => tableShorthand.test.ts} (98%) rename packages/store/ts/config/v2/{expandTableShorthand.ts => tableShorthand.ts} (100%) delete mode 100644 packages/world/ts/config/v2/worldWithShorthands.test.ts delete mode 100644 packages/world/ts/config/v2/worldWithShorthands.ts diff --git a/packages/store/ts/config/v2/index.ts b/packages/store/ts/config/v2/index.ts index e034a57be4..bc37e67a37 100644 --- a/packages/store/ts/config/v2/index.ts +++ b/packages/store/ts/config/v2/index.ts @@ -2,7 +2,6 @@ export * from "./generics"; export * from "./scope"; export * from "./schema"; export * from "./tableShorthand"; -export * from "./storeWithShorthands"; export * from "./table"; export * from "./tables"; export * from "./store"; diff --git a/packages/store/ts/config/v2/namespace.ts b/packages/store/ts/config/v2/namespace.ts index 44d4583ec8..0a4b2e3237 100644 --- a/packages/store/ts/config/v2/namespace.ts +++ b/packages/store/ts/config/v2/namespace.ts @@ -3,6 +3,7 @@ import { hasOwnKey, mergeIfUndefined } from "./generics"; import { NamespaceInput } from "./input"; import { resolveTables, validateTables } from "./tables"; import { AbiTypeScope, Scope } from "./scope"; +import { expandTableShorthand } from "./tableShorthand"; export type validateNamespace = { [key in keyof input]: key extends "tables" @@ -33,7 +34,7 @@ export type resolveNamespace = input : resolveTables< { readonly [label in keyof input["tables"]]: mergeIfUndefined< - input["tables"][label], + expandTableShorthand, { readonly namespace: string } >; }, @@ -53,7 +54,7 @@ export function resolveNamespace { - return [label, mergeIfUndefined(table, { namespace })]; + return [label, mergeIfUndefined(expandTableShorthand(table, scope), { namespace })]; }), scope, ), diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index 8a7eb7079b..5f92e15df1 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -10,7 +10,7 @@ import { resolveCodegen } from "./codegen"; import { resolveNamespacedTables } from "./namespacedTables"; import { resolveTable } from "./table"; import { resolveNamespace } from "./namespace"; -import { expandTableShorthand } from "./expandTableShorthand"; +import { expandTableShorthand } from "./tableShorthand"; export type extendedScope = scopeWithEnums, scopeWithUserTypes>>; @@ -90,7 +90,7 @@ export function resolveStore(input: input): reso const codegen = resolveCodegen(input.codegen); const tablesInput = flatMorph(input.tables ?? {}, (label, shorthand) => { - const table = expandTableShorthand(shorthand) as TableInput; + const table = expandTableShorthand(shorthand, scope) as TableInput; return [ label, { diff --git a/packages/store/ts/config/v2/expandTableShorthand.test.ts b/packages/store/ts/config/v2/tableShorthand.test.ts similarity index 98% rename from packages/store/ts/config/v2/expandTableShorthand.test.ts rename to packages/store/ts/config/v2/tableShorthand.test.ts index 119b390705..096c4c8c21 100644 --- a/packages/store/ts/config/v2/expandTableShorthand.test.ts +++ b/packages/store/ts/config/v2/tableShorthand.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "vitest"; import { attest } from "@arktype/attest"; import { AbiTypeScope, extendScope } from "./scope"; -import { NoStaticKeyFieldError, defineTableShorthand } from "./expandTableShorthand"; +import { NoStaticKeyFieldError, defineTableShorthand } from "./tableShorthand"; describe("defineTableShorthand", () => { it("should expand a single ABI type into a id/value schema", () => { diff --git a/packages/store/ts/config/v2/expandTableShorthand.ts b/packages/store/ts/config/v2/tableShorthand.ts similarity index 100% rename from packages/store/ts/config/v2/expandTableShorthand.ts rename to packages/store/ts/config/v2/tableShorthand.ts diff --git a/packages/store/ts/config/v2/tables.ts b/packages/store/ts/config/v2/tables.ts index efa72527ff..88787165fb 100644 --- a/packages/store/ts/config/v2/tables.ts +++ b/packages/store/ts/config/v2/tables.ts @@ -3,7 +3,7 @@ import { isObject, mergeIfUndefined } from "./generics"; import { TableShorthandInput, TablesInput } from "./input"; import { Scope, AbiTypeScope } from "./scope"; import { validateTable, resolveTable } from "./table"; -import { expandTableShorthand, isTableShorthandInput, validateTableShorthand } from "./expandTableShorthand"; +import { expandTableShorthand, isTableShorthandInput, validateTableShorthand } from "./tableShorthand"; export type validateTables = { [label in keyof tables]: tables[label] extends TableShorthandInput diff --git a/packages/world/ts/config/v2/input.ts b/packages/world/ts/config/v2/input.ts index ae43df5963..15a8964896 100644 --- a/packages/world/ts/config/v2/input.ts +++ b/packages/world/ts/config/v2/input.ts @@ -1,5 +1,5 @@ import { show } from "@arktype/util"; -import { StoreInput, StoreWithShorthandsInput } from "@latticexyz/store/config/v2"; +import { StoreInput } from "@latticexyz/store/config/v2"; import { DynamicResolution, ValueWithType } from "./dynamicResolution"; export type SystemInput = { @@ -121,7 +121,3 @@ export type NamespacesInput = { }; export type NamespaceInput = Pick; - -/******** Variations with shorthands ********/ - -export type WorldWithShorthandsInput = Omit & Pick; diff --git a/packages/world/ts/config/v2/worldWithShorthands.test.ts b/packages/world/ts/config/v2/worldWithShorthands.test.ts deleted file mode 100644 index e373c069d8..0000000000 --- a/packages/world/ts/config/v2/worldWithShorthands.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { describe, it } from "vitest"; -import { defineWorldWithShorthands } from "./worldWithShorthands"; -import { attest } from "@arktype/attest"; -import { resourceToHex } from "@latticexyz/common"; -import { - CONFIG_DEFAULTS as STORE_CONFIG_DEFAULTS, - TABLE_CODEGEN_DEFAULTS, - CODEGEN_DEFAULTS as STORE_CODEGEN_DEFAULTS, - TABLE_DEPLOY_DEFAULTS, -} from "@latticexyz/store/config/v2"; -import { CODEGEN_DEFAULTS as WORLD_CODEGEN_DEFAULTS, CONFIG_DEFAULTS as WORLD_CONFIG_DEFAULTS } from "./defaults"; - -const CONFIG_DEFAULTS = { ...STORE_CONFIG_DEFAULTS, ...WORLD_CONFIG_DEFAULTS }; -const CODEGEN_DEFAULTS = { ...STORE_CODEGEN_DEFAULTS, ...WORLD_CODEGEN_DEFAULTS }; - -describe("defineWorldWithShorthands", () => { - it.skip("should resolve namespaced shorthand table config with user types and enums", () => { - const config = defineWorldWithShorthands({ - // @ts-expect-error TODO - namespaces: { - ExampleNS: { - tables: { - ExampleTable: "Static", - }, - }, - }, - userTypes: { - Static: { type: "address", filePath: "path/to/file" }, - Dynamic: { type: "string", filePath: "path/to/file" }, - }, - enums: { - MyEnum: ["First", "Second"], - }, - }); - - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, - tables: { - ExampleNS__ExampleTable: { - label: "ExampleTable", - type: "table", - namespace: "ExampleNS", - name: "ExampleTable" as string, - tableId: resourceToHex({ type: "table", namespace: "ExampleNS", name: "ExampleTable" }), - schema: { - id: { - type: "bytes32", - internalType: "bytes32", - }, - value: { - type: "address", - internalType: "Static", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - userTypes: { - Static: { type: "address", filePath: "path/to/file" as string }, - Dynamic: { type: "string", filePath: "path/to/file" as string }, - }, - enums: { - MyEnum: ["First", "Second"], - }, - enumValues: { - MyEnum: { - First: 0, - Second: 1, - }, - }, - namespace: "" as string, - } as const; - - attest(config).equals(expected); - }); - - it.skip("should resolve namespaced shorthand schema table config with user types and enums", () => { - const config = defineWorldWithShorthands({ - // @ts-expect-error TODO - namespaces: { - ExampleNS: { - tables: { - ExampleTable: { - id: "Static", - value: "MyEnum", - dynamic: "Dynamic", - }, - }, - }, - }, - userTypes: { - Static: { type: "address", filePath: "path/to/file" }, - Dynamic: { type: "string", filePath: "path/to/file" }, - }, - enums: { - MyEnum: ["First", "Second"], - }, - }); - - const expected = { - ...CONFIG_DEFAULTS, - codegen: CODEGEN_DEFAULTS, - tables: { - ExampleNS__ExampleTable: { - label: "ExampleTable", - type: "table", - namespace: "ExampleNS", - name: "ExampleTable" as string, - tableId: resourceToHex({ type: "table", namespace: "ExampleNS", name: "ExampleTable" }), - schema: { - id: { - type: "address", - internalType: "Static", - }, - value: { - type: "uint8", - internalType: "MyEnum", - }, - dynamic: { - type: "string", - internalType: "Dynamic", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - userTypes: { - Static: { type: "address", filePath: "path/to/file" as string }, - Dynamic: { type: "string", filePath: "path/to/file" as string }, - }, - enums: { - MyEnum: ["First", "Second"], - }, - enumValues: { - MyEnum: { - First: 0, - Second: 1, - }, - }, - namespace: "" as string, - } as const; - - attest(config).equals(expected); - }); - - it("should accept a shorthand store config as input and expand it", () => { - const config = defineWorldWithShorthands({ tables: { Name: "address" } }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Name: { - label: "Name", - type: "table", - namespace: "" as string, - name: "Name" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), - schema: { - id: { - type: "bytes32", - internalType: "bytes32", - }, - value: { - type: "address", - internalType: "address", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...CONFIG_DEFAULTS, - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - userTypes: {}, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - attest(expectedConfig); - }); - - it("should accept a user type as input and expand it", () => { - const config = defineWorldWithShorthands({ - tables: { Name: "CustomType" }, - userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, - }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Name: { - label: "Name", - type: "table", - namespace: "" as string, - name: "Name" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), - schema: { - id: { - type: "bytes32", - internalType: "bytes32", - }, - value: { - type: "address", - internalType: "CustomType", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...CONFIG_DEFAULTS, - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - userTypes: { CustomType: { type: "address", filePath: "path/to/file" as string } }, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - }); - - it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { - const config = defineWorldWithShorthands({ - tables: { Example: { id: "address", name: "string", age: "uint256" } }, - }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Example: { - label: "Example", - type: "table", - namespace: "" as string, - name: "Example" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), - schema: { - id: { - type: "address", - internalType: "address", - }, - name: { - type: "string", - internalType: "string", - }, - age: { - type: "uint256", - internalType: "uint256", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...CONFIG_DEFAULTS, - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - userTypes: {}, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - }); - - it("given a schema with a key field with static custom type, it should use `id` as single key", () => { - const config = defineWorldWithShorthands({ - tables: { Example: { id: "address", name: "string", age: "uint256" } }, - }); - - const expectedBaseNamespace = { - namespace: "" as string, - tables: { - Example: { - label: "Example", - type: "table", - namespace: "" as string, - name: "Example" as string, - tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), - schema: { - id: { - type: "address", - internalType: "address", - }, - name: { - type: "string", - internalType: "string", - }, - age: { - type: "uint256", - internalType: "uint256", - }, - }, - key: ["id"], - codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - } as const; - - const expectedConfig = { - ...CONFIG_DEFAULTS, - ...expectedBaseNamespace, - namespaces: { - "": { - label: "", - ...expectedBaseNamespace, - }, - }, - userTypes: {}, - enums: {}, - enumValues: {}, - codegen: CODEGEN_DEFAULTS, - } as const; - - attest(config).equals(expectedConfig); - }); - - it("throw an error if the shorthand doesn't include a key field", () => { - attest(() => - defineWorldWithShorthands({ - tables: { - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - Example: { - name: "string", - age: "uint256", - }, - }, - }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("throw an error if the shorthand config includes a non-static key field", () => { - attest(() => - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - defineWorldWithShorthands({ tables: { Example: { id: "string", name: "string", age: "uint256" } } }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("throw an error if the shorthand config includes a non-static user type as key field", () => { - attest(() => - defineWorldWithShorthands({ - // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. - tables: { Example: { id: "dynamic", name: "string", age: "uint256" } }, - userTypes: { - dynamic: { type: "string", filePath: "path/to/file" }, - static: { type: "address", filePath: "path/to/file" }, - }, - }), - ).throwsAndHasTypeError( - "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", - ); - }); - - it("should allow a const config as input", () => { - const config = { - tables: { - Example: { - schema: { id: "address", name: "string", age: "uint256" }, - key: ["age"], - }, - }, - } as const; - - defineWorldWithShorthands(config); - }); - - it.skip("should throw with an invalid namespace config option", () => { - attest(() => - defineWorldWithShorthands({ - // @ts-expect-error TODO - namespaces: { - ExampleNS: { - tables: { - ExampleTable: "number", - }, - }, - }, - }), - ).type.errors(`Type '"number"' is not assignable to type 'AbiType'.`); - }); - - it.skip("should throw with a non-existent namespace config option", () => { - attest(() => - defineWorldWithShorthands({ - // @ts-expect-error TODO - namespaces: { - ExampleNS: { - invalidProperty: true, - }, - }, - }), - ).type.errors("`invalidProperty` is not a valid namespace config option."); - }); -}); diff --git a/packages/world/ts/config/v2/worldWithShorthands.ts b/packages/world/ts/config/v2/worldWithShorthands.ts deleted file mode 100644 index 18c85a15bb..0000000000 --- a/packages/world/ts/config/v2/worldWithShorthands.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { mapObject } from "@latticexyz/common/utils"; -import { - extendedScope, - getPath, - hasOwnKey, - isObject, - isTableShorthandInput, - resolveTableShorthand, - resolveTablesWithShorthands, - validateTablesWithShorthands, -} from "@latticexyz/store/config/v2"; -import { WorldWithShorthandsInput } from "./input"; -import { resolveWorld, validateWorld } from "./world"; - -export type validateWorldWithShorthands = { - [key in keyof world]: key extends "tables" - ? validateTablesWithShorthands> - : validateWorld[key]; -}; - -function validateWorldWithShorthands(world: unknown): asserts world is WorldWithShorthandsInput { - const scope = extendedScope(world); - if (hasOwnKey(world, "tables")) { - validateTablesWithShorthands(world.tables, scope); - } - - if (hasOwnKey(world, "namespaces") && isObject(world.namespaces)) { - for (const namespaceKey of Object.keys(world.namespaces)) { - validateTablesWithShorthands(getPath(world.namespaces, [namespaceKey, "tables"]) ?? {}, scope); - } - } -} - -export type resolveWorldWithShorthands = resolveWorld<{ - readonly [key in keyof world]: key extends "tables" - ? resolveTablesWithShorthands> - : world[key]; -}>; - -export function resolveWorldWithShorthands( - world: world, -): resolveWorldWithShorthands { - const scope = extendedScope(world); - const tables = world.tables - ? mapObject(world.tables, (table) => { - return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; - }) - : null; - - const fullConfig = { - ...world, - ...(tables ? { tables } : null), - }; - - validateWorld(fullConfig); - return resolveWorld(fullConfig) as never; -} - -export function defineWorldWithShorthands( - world: validateWorldWithShorthands, -): resolveWorldWithShorthands { - validateWorldWithShorthands(world); - return resolveWorldWithShorthands(world) as never; -} diff --git a/packages/world/ts/exports/index.ts b/packages/world/ts/exports/index.ts index 254108b7dc..283c71b3e1 100644 --- a/packages/world/ts/exports/index.ts +++ b/packages/world/ts/exports/index.ts @@ -6,5 +6,5 @@ export { helloWorldEvent, worldDeployedEvent } from "../worldEvents"; -export { defineWorldWithShorthands as defineWorld } from "../config/v2/worldWithShorthands"; +export { defineWorld } from "../config/v2/world"; export type { World } from "../config/v2/output"; From 5bc63f4234ec3634ed73d7717ce12fc0fbc600c8 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 23 Jul 2024 12:02:28 -0700 Subject: [PATCH 5/8] Create wicked-numbers-check.md --- .changeset/wicked-numbers-check.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wicked-numbers-check.md diff --git a/.changeset/wicked-numbers-check.md b/.changeset/wicked-numbers-check.md new file mode 100644 index 0000000000..43811d3d0e --- /dev/null +++ b/.changeset/wicked-numbers-check.md @@ -0,0 +1,6 @@ +--- +"@latticexyz/store": patch +"@latticexyz/world": patch +--- + +Refactored how the config handles shorthand table definitions, greatly simplifying the codebase. This will make it easier to add support for multiple namespaces. From ce2bca918dddb61e2dcd16df73520e1650a7eeec Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 23 Jul 2024 20:12:22 +0100 Subject: [PATCH 6/8] bring back shorthand tests --- packages/store/ts/config/v2/store.test.ts | 288 +++++++++++++++ packages/world/ts/config/v2/world.test.ts | 421 ++++++++++++++++++++++ 2 files changed, 709 insertions(+) diff --git a/packages/store/ts/config/v2/store.test.ts b/packages/store/ts/config/v2/store.test.ts index 18a50077b7..3c8ec66ad5 100644 --- a/packages/store/ts/config/v2/store.test.ts +++ b/packages/store/ts/config/v2/store.test.ts @@ -720,4 +720,292 @@ describe("defineStore", () => { attest(config).equals(expectedConfig); }); + + describe("shorthands", () => { + it("should accept a shorthand store config as input and expand it", () => { + const config = defineStore({ tables: { Name: "address" } }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Name: { + label: "Name", + type: "table", + namespace: "" as string, + name: "Name" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), + schema: { + id: { + type: "bytes32", + internalType: "bytes32", + }, + value: { + type: "address", + internalType: "address", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", + userTypes: {}, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + }); + + it("should satisfy the output type", () => { + const config = defineStore({ + tables: { Name: { schema: { id: "address" }, key: ["id"] } }, + userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, + }); + + attest>(); + }); + + it("should accept an empty input", () => { + const config = defineStore({}); + attest>(); + }); + + it("should accept a user type as input and expand it", () => { + const config = defineStore({ + tables: { Name: "CustomType" }, + userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, + }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Name: { + label: "Name", + type: "table", + namespace: "" as string, + name: "Name" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), + schema: { + id: { + type: "bytes32", + internalType: "bytes32", + }, + value: { + type: "address", + internalType: "CustomType", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", + userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + attest(expectedConfig); + }); + + it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { + const config = defineStore({ + tables: { Example: { id: "address", name: "string", age: "uint256" } }, + }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Example: { + label: "Example", + type: "table", + namespace: "" as string, + name: "Example" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), + schema: { + id: { + type: "address", + internalType: "address", + }, + name: { + type: "string", + internalType: "string", + }, + age: { + type: "uint256", + internalType: "uint256", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", + userTypes: {}, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + }); + + it("given a schema with a key field with static custom type, it should use `id` as single key", () => { + const config = defineStore({ + tables: { Example: { id: "address", name: "string", age: "uint256" } }, + }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Example: { + label: "Example", + type: "table", + namespace: "" as string, + name: "Example" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), + schema: { + id: { + type: "address", + internalType: "address", + }, + name: { + type: "string", + internalType: "string", + }, + age: { + type: "uint256", + internalType: "uint256", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + sourceDirectory: "src", + userTypes: {}, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + }); + + it("should pass through full table config inputs", () => { + const config = defineStore({ + tables: { + Example: { + schema: { id: "address", name: "string", age: "uint256" }, + key: ["age", "id"], + }, + }, + }); + const expected = defineStore({ + tables: { + Example: { + schema: { id: "address", name: "string", age: "uint256" }, + key: ["age", "id"], + }, + }, + }); + + attest(config).equals(expected); + }); + + it("should throw if the shorthand doesn't include a key field", () => { + attest(() => + defineStore({ + tables: { + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + Example: { + name: "string", + age: "uint256", + }, + }, + }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("should throw if the shorthand config includes a non-static key field", () => { + attest(() => + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + defineStore({ tables: { Example: { id: "string", name: "string", age: "uint256" } } }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("should throw if the shorthand config includes a non-static user type as key field", () => { + attest(() => + defineStore({ + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + tables: { Example: { id: "dynamic", name: "string", age: "uint256" } }, + userTypes: { + dynamic: { type: "string", filePath: "path/to/file" }, + static: { type: "address", filePath: "path/to/file" }, + }, + }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("should throw if the shorthand key is neither a custom nor ABI type", () => { + // @ts-expect-error Type '"NotAnAbiType"' is not assignable to type 'AbiType' + attest(() => defineStore({ tables: { Invalid: "NotAnAbiType" } })) + .throws("Invalid ABI type. `NotAnAbiType` not found in scope.") + .type.errors(`Type '"NotAnAbiType"' is not assignable to type 'AbiType'`); + }); + }); }); diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 695a98790d..8448f76535 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -883,4 +883,425 @@ describe("defineWorld", () => { }), ).type.errors("`invalidOption` is not a valid World config option."); }); + + describe("shorthands", () => { + it.skip("should resolve namespaced shorthand table config with user types and enums", () => { + const config = defineWorld({ + // @ts-expect-error TODO + namespaces: { + ExampleNS: { + tables: { + ExampleTable: "Static", + }, + }, + }, + userTypes: { + Static: { type: "address", filePath: "path/to/file" }, + Dynamic: { type: "string", filePath: "path/to/file" }, + }, + enums: { + MyEnum: ["First", "Second"], + }, + }); + + const expected = { + ...CONFIG_DEFAULTS, + codegen: CODEGEN_DEFAULTS, + tables: { + ExampleNS__ExampleTable: { + label: "ExampleTable", + type: "table", + namespace: "ExampleNS", + name: "ExampleTable" as string, + tableId: resourceToHex({ type: "table", namespace: "ExampleNS", name: "ExampleTable" }), + schema: { + id: { + type: "bytes32", + internalType: "bytes32", + }, + value: { + type: "address", + internalType: "Static", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + userTypes: { + Static: { type: "address", filePath: "path/to/file" as string }, + Dynamic: { type: "string", filePath: "path/to/file" as string }, + }, + enums: { + MyEnum: ["First", "Second"], + }, + enumValues: { + MyEnum: { + First: 0, + Second: 1, + }, + }, + namespace: "" as string, + } as const; + + attest(config).equals(expected); + }); + + it.skip("should resolve namespaced shorthand schema table config with user types and enums", () => { + const config = defineWorld({ + // @ts-expect-error TODO + namespaces: { + ExampleNS: { + tables: { + ExampleTable: { + id: "Static", + value: "MyEnum", + dynamic: "Dynamic", + }, + }, + }, + }, + userTypes: { + Static: { type: "address", filePath: "path/to/file" }, + Dynamic: { type: "string", filePath: "path/to/file" }, + }, + enums: { + MyEnum: ["First", "Second"], + }, + }); + + const expected = { + ...CONFIG_DEFAULTS, + codegen: CODEGEN_DEFAULTS, + tables: { + ExampleNS__ExampleTable: { + label: "ExampleTable", + type: "table", + namespace: "ExampleNS", + name: "ExampleTable" as string, + tableId: resourceToHex({ type: "table", namespace: "ExampleNS", name: "ExampleTable" }), + schema: { + id: { + type: "address", + internalType: "Static", + }, + value: { + type: "uint8", + internalType: "MyEnum", + }, + dynamic: { + type: "string", + internalType: "Dynamic", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + userTypes: { + Static: { type: "address", filePath: "path/to/file" as string }, + Dynamic: { type: "string", filePath: "path/to/file" as string }, + }, + enums: { + MyEnum: ["First", "Second"], + }, + enumValues: { + MyEnum: { + First: 0, + Second: 1, + }, + }, + namespace: "" as string, + } as const; + + attest(config).equals(expected); + }); + + it("should accept a shorthand store config as input and expand it", () => { + const config = defineWorld({ tables: { Name: "address" } }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Name: { + label: "Name", + type: "table", + namespace: "" as string, + name: "Name" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), + schema: { + id: { + type: "bytes32", + internalType: "bytes32", + }, + value: { + type: "address", + internalType: "address", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + userTypes: {}, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + attest(expectedConfig); + }); + + it("should accept a user type as input and expand it", () => { + const config = defineWorld({ + tables: { Name: "CustomType" }, + userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, + }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Name: { + label: "Name", + type: "table", + namespace: "" as string, + name: "Name" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Name" }), + schema: { + id: { + type: "bytes32", + internalType: "bytes32", + }, + value: { + type: "address", + internalType: "CustomType", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + userTypes: { CustomType: { type: "address", filePath: "path/to/file" as string } }, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + }); + + it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { + const config = defineWorld({ + tables: { Example: { id: "address", name: "string", age: "uint256" } }, + }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Example: { + label: "Example", + type: "table", + namespace: "" as string, + name: "Example" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), + schema: { + id: { + type: "address", + internalType: "address", + }, + name: { + type: "string", + internalType: "string", + }, + age: { + type: "uint256", + internalType: "uint256", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + userTypes: {}, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + }); + + it("given a schema with a key field with static custom type, it should use `id` as single key", () => { + const config = defineWorld({ + tables: { Example: { id: "address", name: "string", age: "uint256" } }, + }); + + const expectedBaseNamespace = { + namespace: "" as string, + tables: { + Example: { + label: "Example", + type: "table", + namespace: "" as string, + name: "Example" as string, + tableId: resourceToHex({ type: "table", namespace: "", name: "Example" }), + schema: { + id: { + type: "address", + internalType: "address", + }, + name: { + type: "string", + internalType: "string", + }, + age: { + type: "uint256", + internalType: "uint256", + }, + }, + key: ["id"], + codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, + deploy: TABLE_DEPLOY_DEFAULTS, + }, + }, + } as const; + + const expectedConfig = { + ...CONFIG_DEFAULTS, + ...expectedBaseNamespace, + namespaces: { + "": { + label: "", + ...expectedBaseNamespace, + }, + }, + userTypes: {}, + enums: {}, + enumValues: {}, + codegen: CODEGEN_DEFAULTS, + } as const; + + attest(config).equals(expectedConfig); + }); + + it("throw an error if the shorthand doesn't include a key field", () => { + attest(() => + defineWorld({ + tables: { + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + Example: { + name: "string", + age: "uint256", + }, + }, + }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("throw an error if the shorthand config includes a non-static key field", () => { + attest(() => + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + defineWorld({ tables: { Example: { id: "string", name: "string", age: "uint256" } } }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("throw an error if the shorthand config includes a non-static user type as key field", () => { + attest(() => + defineWorld({ + // @ts-expect-error Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option. + tables: { Example: { id: "dynamic", name: "string", age: "uint256" } }, + userTypes: { + dynamic: { type: "string", filePath: "path/to/file" }, + static: { type: "address", filePath: "path/to/file" }, + }, + }), + ).throwsAndHasTypeError( + "Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.", + ); + }); + + it("should allow a const config as input", () => { + const config = { + tables: { + Example: { + schema: { id: "address", name: "string", age: "uint256" }, + key: ["age"], + }, + }, + } as const; + + defineWorld(config); + }); + + it.skip("should throw with an invalid namespace config option", () => { + attest(() => + defineWorld({ + // @ts-expect-error TODO + namespaces: { + ExampleNS: { + tables: { + ExampleTable: "number", + }, + }, + }, + }), + ).type.errors(`Type '"number"' is not assignable to type 'AbiType'.`); + }); + + it.skip("should throw with a non-existent namespace config option", () => { + attest(() => + defineWorld({ + // @ts-expect-error TODO + namespaces: { + ExampleNS: { + invalidProperty: true, + }, + }, + }), + ).type.errors("`invalidProperty` is not a valid namespace config option."); + }); + }); }); From 19e8f6a4b19a1cca9743472c6b590a7efdeba5ad Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 24 Jul 2024 10:55:47 +0100 Subject: [PATCH 7/8] fix store --- packages/store/ts/config/v2/namespacedTables.ts | 4 ++-- packages/store/ts/config/v2/store.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/store/ts/config/v2/namespacedTables.ts b/packages/store/ts/config/v2/namespacedTables.ts index 9873a52c85..8cddb3a779 100644 --- a/packages/store/ts/config/v2/namespacedTables.ts +++ b/packages/store/ts/config/v2/namespacedTables.ts @@ -1,4 +1,4 @@ -import { flatMorph } from "@arktype/util"; +import { flatMorph, show } from "@arktype/util"; import { Tables } from "./output"; /** @@ -16,7 +16,7 @@ export type resolveNamespacedTables = { export function resolveNamespacedTables( tables: tables, namespace: namespace, -): resolveNamespacedTables { +): show> { return flatMorph(tables as Tables, (label, table) => [ namespace === "" ? label : `${namespace}__${label}`, table, diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index 5f92e15df1..43708c52e8 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -62,7 +62,7 @@ export type resolveStore< ? resolveNamespacedTables< { readonly [label in keyof input["tables"]]: resolveTable< - mergeIfUndefined, + mergeIfUndefined, { label: label; namespace: namespace }>, extendedScope >; }, From 043480b4212a7aad0e24cbe5ccdb71f702035303 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 24 Jul 2024 11:00:23 +0100 Subject: [PATCH 8/8] fix world --- packages/world/ts/config/v2/namespaces.ts | 3 ++- packages/world/ts/config/v2/world.test.ts | 3 ++- packages/world/ts/config/v2/world.ts | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/world/ts/config/v2/namespaces.ts b/packages/world/ts/config/v2/namespaces.ts index f8b1e08853..ced34101c3 100644 --- a/packages/world/ts/config/v2/namespaces.ts +++ b/packages/world/ts/config/v2/namespaces.ts @@ -8,6 +8,7 @@ import { mergeIfUndefined, extendedScope, getPath, + expandTableShorthand, } from "@latticexyz/store/config/v2"; import { NamespaceInput, NamespacesInput } from "./input"; import { ErrorMessage, conform } from "@arktype/util"; @@ -58,7 +59,7 @@ export type resolveNamespacedTables = "namespaces" extends keyof world readonly [key in namespacedTableKeys]: key extends `${infer namespace}__${infer table}` ? resolveTable< mergeIfUndefined< - getPath, + expandTableShorthand>, { name: table; namespace: namespace } >, extendedScope diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 8448f76535..273c92a887 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -1108,13 +1108,14 @@ describe("defineWorld", () => { ...expectedBaseNamespace, }, }, - userTypes: { CustomType: { type: "address", filePath: "path/to/file" as string } }, + userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, enums: {}, enumValues: {}, codegen: CODEGEN_DEFAULTS, } as const; attest(config).equals(expectedConfig); + attest(expectedConfig); }); it("given a schema with a key field with static ABI type, it should use `id` as single key", () => { diff --git a/packages/world/ts/config/v2/world.ts b/packages/world/ts/config/v2/world.ts index 725940aba3..bdb9c4420b 100644 --- a/packages/world/ts/config/v2/world.ts +++ b/packages/world/ts/config/v2/world.ts @@ -11,6 +11,7 @@ import { Store, hasOwnKey, validateStore, + expandTableShorthand, } from "@latticexyz/store/config/v2"; import { SystemsInput, WorldInput } from "./input"; import { CONFIG_DEFAULTS, MODULE_DEFAULTS } from "./defaults"; @@ -79,7 +80,10 @@ export function resolveWorld(world: world): reso validateTable(table, scope); return [ `${namespaceKey}__${tableKey}`, - resolveTable(mergeIfUndefined(table, { namespace: namespaceKey, name: tableKey }), scope), + resolveTable( + mergeIfUndefined(expandTableShorthand(table, scope), { namespace: namespaceKey, label: tableKey }), + scope, + ), ]; }), )