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. 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/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/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/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.test.ts b/packages/store/ts/config/v2/store.test.ts index 55eff2a974..3c8ec66ad5 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( @@ -727,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/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index 3c632c9ac4..43708c52e8 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 "./tableShorthand"; export type extendedScope = scopeWithEnums, scopeWithUserTypes>>; @@ -61,7 +62,7 @@ export type resolveStore< ? resolveNamespacedTables< { readonly [label in keyof input["tables"]]: resolveTable< - mergeIfUndefined, + mergeIfUndefined, { label: label; namespace: namespace }>, extendedScope >; }, @@ -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, scope) 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 index b3813b28d3..096c4c8c21 100644 --- a/packages/store/ts/config/v2/tableShorthand.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 { defineTableShorthand, NoStaticKeyFieldError } from "./tableShorthand"; +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/tableShorthand.ts b/packages/store/ts/config/v2/tableShorthand.ts index 331cb91b91..2da4c7bd38 100644 --- a/packages/store/ts/config/v2/tableShorthand.ts +++ b/packages/store/ts/config/v2/tableShorthand.ts @@ -1,11 +1,9 @@ -import { ErrorMessage, conform } from "@arktype/util"; import { FixedArrayAbiType, isFixedArrayAbiType, isStaticAbiType } from "@latticexyz/schema-type/internal"; -import { get, hasOwnKey, isObject } from "./generics"; +import { hasOwnKey, isObject } from "./generics"; +import { SchemaInput, ScopedSchemaInput, TableShorthandInput } from "./input"; 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"; +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."; @@ -19,12 +17,6 @@ export function isTableShorthandInput(shorthand: unknown): shorthand is TableSho ); } -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 @@ -32,16 +24,13 @@ export type validateTableShorthand = ? // Require all values to be valid types in this scope conform> : NoStaticKeyFieldError - : // If a fixed array type is provided, accept it - input extends FixedArrayAbiType + : // If a fixed array type or a valid type from the scope is provided, accept it + input extends FixedArrayAbiType | keyof scope["types"] ? 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; + : // If the input is not a valid shorthand, return the expected type + input extends string + ? keyof scope["types"] + : ScopedSchemaInput; export function validateTableShorthand( shorthand: unknown, @@ -65,21 +54,35 @@ export function validateTableShorthand( 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 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 resolveTableShorthand( +export function expandTableShorthand( shorthand: shorthand, - scope: scope = AbiTypeScope as never, -): resolveTableShorthand { + scope: scope, +): expandTableShorthand { + if (typeof shorthand === "string") { + return { + schema: { + id: "bytes32", + value: shorthand, + }, + key: ["id"], + } as never; + } + if (isSchemaInput(shorthand, scope)) { return { schema: shorthand, @@ -87,50 +90,13 @@ export function resolveTableShorthand( - shorthand: validateTableShorthand, - scope: scope = AbiTypeScope as never, -): resolveTableShorthand { - validateTableShorthand(shorthand, scope); - return resolveTableShorthand(shorthand, scope) as never; + return shorthand 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); - } - } - } +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 new file mode 100644 index 0000000000..28a6a12c0f --- /dev/null +++ b/packages/store/ts/config/v2/tables.test.ts @@ -0,0 +1,76 @@ +import { describe, it } from "vitest"; +import { attest } from "@arktype/attest"; +import { AbiTypeScope, extendScope } from "./scope"; +import { defineTables, validateTables } from "./tables"; + +describe("validateTables", () => { + it("should validate shorthands", () => { + attest(() => validateTables({ Short: "uint256" }, AbiTypeScope)); + attest(() => validateTables({ ShortSchema: { first: "uint256", second: "bool" } }, AbiTypeScope)); + }); + + 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("defineTables", () => { + it("expands shorthand table schemas", () => { + const tables = defineTables( + { + Short: "uint256", + ShortSchema: { id: "uint256", exists: "bool" }, + }, + AbiTypeScope, + ); + + const expectedTables = { + Short: { + schema: { + id: { type: "bytes32", internalType: "bytes32" }, + value: { type: "uint256", internalType: "uint256" }, + }, + key: ["id"], + }, + ShortSchema: { + schema: { + id: { type: "uint256", internalType: "uint256" }, + exists: { 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 tables = defineTables({ Custom: "CustomType" }, scope); + + const expectedTables = { + Custom: { + schema: { + id: { type: "bytes32", internalType: "bytes32" }, + value: { type: "uint256", internalType: "CustomType" }, + }, + key: ["id"], + }, + } as const; + + 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 f6ab5f60ad..88787165fb 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 "./tableShorthand"; 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,16 +19,23 @@ 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, { readonly label: label }>, + scope + >; +}; export function resolveTables( tables: tables, @@ -33,7 +43,15 @@ export function resolveTables { return Object.fromEntries( Object.entries(tables).map(([label, table]) => { - return [label, resolveTable(mergeIfUndefined(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; +} 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"; 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/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 695a98790d..273c92a887 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -883,4 +883,426 @@ 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" } }, + 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 = 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."); + }); + }); }); 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, + ), ]; }), ) 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";