diff --git a/packages/store/ts/config/v2/README.md b/packages/store/ts/config/v2/README.md index 5714091cd9..b6d5c4a045 100644 --- a/packages/store/ts/config/v2/README.md +++ b/packages/store/ts/config/v2/README.md @@ -22,22 +22,20 @@ function validateX(x: unknonw): asserts x is X { type resolveX = x extends X ? { [key in keyof x]: Resolved } : never; /** - * defineX function validates the input types and calls resolveX to resolve it. - * Note: the runtime validation happens in `resolveX`. - * This is to take advantage of the type assertion in the function body. + * resolveX function does not validate the input but expects a resolved input. + * This function is used by defineX and other higher level resolution functions. */ -function defineX(x: validateX): resolveX { - return resolveX(x); +function resolveX(x: x): resolveX { + // } /** - * resolveX function does not validate the input type, but validates the runtime types. - * (This is to take advantage of the type assertion in the function body). - * This function is used by defineX and other higher level resolution functions. + * defineX function validates the input types and calls resolveX to resolve it. + * Runtime validation also acts as a type assertion to be able to call the resolvers. */ -function resolveX(x: x): resolveX { +function defineX(x: validateX): resolveX { validateX(x); - // + return resolveX(x); } ``` diff --git a/packages/store/ts/config/v2/input.ts b/packages/store/ts/config/v2/input.ts index f74d583564..a83863fb73 100644 --- a/packages/store/ts/config/v2/input.ts +++ b/packages/store/ts/config/v2/input.ts @@ -21,7 +21,7 @@ export type TableInput = { }; export type TablesInput = { - readonly [key: string]: TableInput; + readonly [key: string]: Omit; }; export type StoreInput = { diff --git a/packages/store/ts/config/v2/schema.ts b/packages/store/ts/config/v2/schema.ts index 926b95cc25..9c2e54a8fc 100644 --- a/packages/store/ts/config/v2/schema.ts +++ b/packages/store/ts/config/v2/schema.ts @@ -36,12 +36,10 @@ export type resolveSchema = evaluate<{ }; }>; -export function resolveSchema( +export function resolveSchema( schema: schema, scope: scope = AbiTypeScope as unknown as scope, ): resolveSchema { - validateSchema(schema, scope); - return Object.fromEntries( Object.entries(schema).map(([key, internalType]) => [ key, @@ -59,6 +57,7 @@ export function defineSchema( schema: validateSchema, scope: scope = AbiTypeScope as scope, ): resolveSchema { + validateSchema(schema, scope); return resolveSchema(schema, scope) as resolveSchema; } diff --git a/packages/store/ts/config/v2/store.test.ts b/packages/store/ts/config/v2/store.test.ts index 99ae22140d..e3586f7b67 100644 --- a/packages/store/ts/config/v2/store.test.ts +++ b/packages/store/ts/config/v2/store.test.ts @@ -1,9 +1,9 @@ import { describe, it } from "vitest"; import { defineStore } from "./store"; -import { Config } from "./output"; import { attest } from "@arktype/attest"; import { resourceToHex } from "@latticexyz/common"; import { CODEGEN_DEFAULTS, TABLE_CODEGEN_DEFAULTS } from "./defaults"; +import { Store } from "./output"; describe("defineStore", () => { it("should return the full config given a full config with one key", () => { @@ -548,7 +548,7 @@ describe("defineStore", () => { userTypes: { CustomType: { type: "address", filePath: "path/to/file" } }, }); - attest(); + attest(); }); it("should use the global namespace instead for tables", () => { @@ -563,8 +563,9 @@ describe("defineStore", () => { }); attest<"namespace">(config.namespace).equals("namespace"); - attest<"namespace">(config.tables.Example.namespace).equals("namespace"); - attest(config.tables.Example.tableId).equals( + attest<"namespace">(config.tables.namespace__Example.namespace).equals("namespace"); + attest<"Example">(config.tables.namespace__Example.name).equals("Example"); + attest(config.tables.namespace__Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "namespace" }), ); }); @@ -582,4 +583,36 @@ describe("defineStore", () => { "Key `invalidKey` does not exist in TableInput", ); }); + + it("should throw if name is overridden in the store context", () => { + attest(() => + defineStore({ + namespace: "CustomNamespace", + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + // @ts-expect-error "Overrides of `name` and `namespace` are not allowed for tables in a store config" + name: "NotAllowed", + }, + }, + }), + ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + }); + + it("should throw if namespace is overridden in the store context", () => { + attest(() => + defineStore({ + namespace: "CustomNamespace", + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + // @ts-expect-error "Overrides of `name` and `namespace` are not allowed for tables in a store config" + namespace: "NotAllowed", + }, + }, + }), + ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + }); }); diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index c730ae4d93..6e74f3ca54 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -2,7 +2,6 @@ import { evaluate, narrow } from "@arktype/util"; import { get, hasOwnKey, mergeIfUndefined } from "./generics"; import { UserTypes } from "./output"; import { CONFIG_DEFAULTS } from "./defaults"; -import { mapObject } from "@latticexyz/common/utils"; import { StoreInput } from "./input"; import { resolveTables, validateTables } from "./tables"; import { scopeWithUserTypes, validateUserTypes } from "./userTypes"; @@ -38,13 +37,19 @@ export function validateStore(store: unknown): asserts store is StoreInput { } } +type keyPrefix = "namespace" extends keyof store + ? store["namespace"] extends "" + ? "" + : `${store["namespace"] & string}__` + : ""; + export type resolveStore = evaluate<{ readonly tables: "tables" extends keyof store ? resolveTables< { - [key in keyof store["tables"]]: mergeIfUndefined< - store["tables"][key], - { namespace: get } + [tableKey in keyof store["tables"] & string as `${keyPrefix}${tableKey}`]: mergeIfUndefined< + store["tables"][tableKey], + { namespace: get; name: tableKey } >; }, extendedScope @@ -56,12 +61,15 @@ export type resolveStore = evaluate<{ readonly codegen: "codegen" extends keyof store ? resolveCodegen : resolveCodegen<{}>; }>; -export function resolveStore(store: store): resolveStore { - validateStore(store); - +export function resolveStore(store: store): resolveStore { return { tables: resolveTables( - mapObject(store.tables ?? {}, (table) => mergeIfUndefined(table, { namespace: store.namespace })), + Object.fromEntries( + Object.entries(store.tables ?? {}).map(([tableKey, table]) => { + const key = store.namespace ? `${store.namespace}__${tableKey}` : tableKey; + return [key, mergeIfUndefined(table, { namespace: store.namespace, name: tableKey })]; + }), + ), extendedScope(store), ), userTypes: store.userTypes ?? {}, @@ -72,5 +80,6 @@ export function resolveStore(store: store): resolveStore { } export function defineStore(store: validateStore): resolveStore { - return resolveStore(store) as resolveStore; + validateStore(store); + return resolveStore(store) as unknown as resolveStore; } diff --git a/packages/store/ts/config/v2/storeWithShorthands.test.ts b/packages/store/ts/config/v2/storeWithShorthands.test.ts index 30d80fd0c2..7f7d943336 100644 --- a/packages/store/ts/config/v2/storeWithShorthands.test.ts +++ b/packages/store/ts/config/v2/storeWithShorthands.test.ts @@ -156,6 +156,7 @@ describe("defineStoreWithShorthands", () => { const config = defineStoreWithShorthands({ tables: { Example: { id: "address", name: "string", age: "uint256" } }, }); + const expected = { tables: { Example: { diff --git a/packages/store/ts/config/v2/storeWithShorthands.ts b/packages/store/ts/config/v2/storeWithShorthands.ts index 51a5902221..dd8a271776 100644 --- a/packages/store/ts/config/v2/storeWithShorthands.ts +++ b/packages/store/ts/config/v2/storeWithShorthands.ts @@ -28,9 +28,9 @@ export type resolveStoreWithShorthands = resolveStore<{ : store[key]; }>; -export function resolveStoreWithShorthands(store: store): resolveStoreWithShorthands { - validateStoreWithShorthands(store); - +export function resolveStoreWithShorthands( + store: store, +): resolveStoreWithShorthands { const scope = extendedScope(store); const fullConfig = { ...store, @@ -39,11 +39,13 @@ export function resolveStoreWithShorthands(store: store): resolveSt }), }; + validateStore(fullConfig); return resolveStore(fullConfig) as unknown as resolveStoreWithShorthands; } export function defineStoreWithShorthands( store: validateStoreWithShorthands, ): resolveStoreWithShorthands { - return resolveStoreWithShorthands(store) as resolveStoreWithShorthands; + validateStoreWithShorthands(store); + return resolveStoreWithShorthands(store) as unknown as resolveStoreWithShorthands; } diff --git a/packages/store/ts/config/v2/table.ts b/packages/store/ts/config/v2/table.ts index 1b724f58c6..6083b8480f 100644 --- a/packages/store/ts/config/v2/table.ts +++ b/packages/store/ts/config/v2/table.ts @@ -37,28 +37,25 @@ export function isValidPrimaryKey = { [i in keyof keys]: keys[i] extends validKeys ? keys[i] : validKeys; }; -export type validateTable = { +export type ValidateTableOptions = { inStoreContext: boolean }; + +export type validateTable< + input, + scope extends Scope = AbiTypeScope, + options extends ValidateTableOptions = { inStoreContext: false }, +> = { [key in keyof input]: key extends "key" ? validateKeys, SchemaInput>, scope>, input[key]> : key extends "schema" ? validateSchema : key extends "name" | "namespace" - ? narrow + ? options["inStoreContext"] extends true + ? ErrorMessage<"Overrides of `name` and `namespace` are not allowed for tables in a store config"> + : narrow : key extends keyof TableInput ? TableInput[key] : ErrorMessage<`Key \`${key & string}\` does not exist in TableInput`>; @@ -67,6 +64,7 @@ export type validateTable = { export function validateTable( input: input, scope: scope = AbiTypeScope as unknown as scope, + options: ValidateTableOptions = { inStoreContext: false }, ): asserts input is TableInput & input { if (typeof input !== "object" || input == null) { throw new Error(`Expected full table config, got \`${JSON.stringify(input)}\``); @@ -88,6 +86,10 @@ export function validateTable( }\``, ); } + + if ((options.inStoreContext && hasOwnKey(input, "name")) || hasOwnKey(input, "namespace")) { + throw new Error("Overrides of `name` and `namespace` are not allowed for tables in a store config."); + } } export type resolveTableCodegen = { @@ -142,12 +144,10 @@ export type resolveTable = input extends Tab } : never; -export function resolveTable( +export function resolveTable( input: input, scope: scope = AbiTypeScope as unknown as scope, ): resolveTable { - validateTable(input, scope); - const name = input.name; const type = input.type ?? TABLE_DEFAULTS.type; const namespace = input.namespace ?? TABLE_DEFAULTS.namespace; @@ -180,5 +180,6 @@ export function defineTable( input: validateTable, scope: scope = AbiTypeScope as unknown as scope, ): resolveTable { + validateTable(input, scope); return resolveTable(input, scope) as resolveTable; } diff --git a/packages/store/ts/config/v2/tableShorthand.ts b/packages/store/ts/config/v2/tableShorthand.ts index 1ccfa615fd..dae5aa401e 100644 --- a/packages/store/ts/config/v2/tableShorthand.ts +++ b/packages/store/ts/config/v2/tableShorthand.ts @@ -1,11 +1,11 @@ import { ErrorMessage, conform } from "@arktype/util"; import { FixedArrayAbiType, isFixedArrayAbiType, isStaticAbiType } from "@latticexyz/schema-type/internal"; -import { get, hasOwnKey, isObject, mergeIfUndefined } from "./generics"; +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 { validateTable } from "./table"; +import { ValidateTableOptions, validateTable } from "./table"; export type NoStaticKeyFieldError = ErrorMessage<"Invalid schema. Expected an `id` field with a static ABI type or an explicit `key` option.">; @@ -17,9 +17,11 @@ export function isTableShorthandInput(shorthand: unknown): shorthand is TableSho ); } -export type validateTableWithShorthand = table extends TableShorthandInput - ? validateTableShorthand - : validateTable; +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 @@ -72,12 +74,10 @@ export type resolveTableShorthand : never : never; -export function resolveTableShorthand( +export function resolveTableShorthand( shorthand: shorthand, scope: scope = AbiTypeScope as unknown as scope, ): resolveTableShorthand { - validateTableShorthand(shorthand, scope); - if (isSchemaInput(shorthand, scope)) { return { schema: shorthand, @@ -98,6 +98,7 @@ export function defineTableShorthand, scope: scope = AbiTypeScope as unknown as scope, ): resolveTableShorthand { + validateTableShorthand(shorthand, scope); return resolveTableShorthand(shorthand, scope) as resolveTableShorthand; } @@ -110,11 +111,11 @@ export type resolveTableWithShorthand : table; export type resolveTablesWithShorthands = { - [key in keyof input]: mergeIfUndefined, { name: key }>; + [key in keyof input]: resolveTableWithShorthand; }; export type validateTablesWithShorthands = { - [key in keyof tables]: validateTableWithShorthand; + [key in keyof tables]: validateTableWithShorthand; }; export function validateTablesWithShorthands( diff --git a/packages/store/ts/config/v2/tables.ts b/packages/store/ts/config/v2/tables.ts index 6dadc6c97e..673b9b20d9 100644 --- a/packages/store/ts/config/v2/tables.ts +++ b/packages/store/ts/config/v2/tables.ts @@ -6,7 +6,7 @@ import { validateTable, resolveTable } from "./table"; export type validateTables = { [key in keyof tables]: tables[key] extends object - ? validateTable + ? validateTable : ErrorMessage<`Expected full table config.`>; }; @@ -16,7 +16,7 @@ export function validateTables( ): asserts input is TablesInput { if (isObject(input)) { for (const table of Object.values(input)) { - validateTable(table, scope); + validateTable(table, scope, { inStoreContext: true }); } return; } @@ -27,19 +27,17 @@ export type resolveTables = evaluate readonly [key in keyof tables]: resolveTable, scope>; }>; -export function resolveTables( +export function resolveTables( tables: tables, scope: scope = AbiTypeScope as unknown as scope, ): resolveTables { - validateTables(tables, scope); - if (!isObject(tables)) { throw new Error(`Expected tables config, received ${JSON.stringify(tables)}`); } return Object.fromEntries( Object.entries(tables).map(([key, table]) => { - return [key, resolveTable(mergeIfUndefined(table, { name: key }) as validateTable, scope)]; + return [key, resolveTable(mergeIfUndefined(table, { name: key }), scope)]; }), ) as unknown as resolveTables; } diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 7c7428b48b..995e11fcce 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -771,10 +771,93 @@ describe("defineWorld", () => { }); attest<"namespace">(config.namespace).equals("namespace"); - attest<"namespace">(config.tables.Example.namespace).equals("namespace"); - attest(config.tables.Example.tableId).equals( + attest<"namespace">(config.tables.namespace__Example.namespace).equals("namespace"); + attest(config.tables.namespace__Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "namespace" }), ); }); }); + + it("should use the custom name and namespace as table index", () => { + const config = defineWorld({ + namespace: "CustomNamespace", + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + }, + }, + }); + + attest<"CustomNamespace__Example", keyof typeof config.tables>(); + }); + + it("should throw if namespace is overridden in top level tables", () => { + attest(() => + defineWorld({ + namespace: "CustomNamespace", + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + // @ts-expect-error "Overrides of `name` and `namespace` are not allowed for tables in a store config" + namespace: "NotAllowed", + }, + }, + }), + ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + }); + + it("should throw if name is overridden in top level tables", () => { + attest(() => + defineWorld({ + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + // @ts-expect-error "Overrides of `name` and `namespace` are not allowed for tables in a store config" + name: "NotAllowed", + }, + }, + }), + ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + }); + + it("should throw if name is overridden in namespaced tables", () => { + attest(() => + defineWorld({ + namespaces: { + MyNamespace: { + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + // @ts-expect-error "Overrides of `name` and `namespace` are not allowed for tables in a store config" + name: "NotAllowed", + }, + }, + }, + }, + }), + ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + }); + + it("should throw if namespace is overridden in namespaced tables", () => { + attest(() => + defineWorld({ + namespaces: { + MyNamespace: { + tables: { + Example: { + schema: { id: "address" }, + key: ["id"], + // @ts-expect-error "Overrides of `name` and `namespace` are not allowed for tables in a store config" + namespace: "NotAllowed", + }, + }, + }, + }, + }), + ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + }); }); diff --git a/packages/world/ts/config/v2/world.ts b/packages/world/ts/config/v2/world.ts index 16fb12fcee..784db25ce2 100644 --- a/packages/world/ts/config/v2/world.ts +++ b/packages/world/ts/config/v2/world.ts @@ -1,16 +1,13 @@ import { conform, evaluate, narrow } from "@arktype/util"; -import { mapObject } from "@latticexyz/common/utils"; import { UserTypes, extendedScope, get, resolveTable, validateTable, - resolveCodegen as resolveStoreCodegen, mergeIfUndefined, validateTables, resolveStore, - resolveTables, Store, hasOwnKey, validateStore, @@ -73,17 +70,9 @@ export type resolveWorld = evaluate< > >; -export function resolveWorld(world: world): resolveWorld { - validateWorld(world); - +export function resolveWorld(world: world): resolveWorld { const scope = extendedScope(world); - const namespace = get(world, "namespace") ?? ""; - - const namespaces = get(world, "namespaces") ?? {}; - validateNamespaces(namespaces, scope); - - const rootTables = get(world, "tables") ?? {}; - validateTables(rootTables, scope); + const namespaces = world.namespaces ?? {}; const resolvedNamespacedTables = Object.fromEntries( Object.entries(namespaces) @@ -99,18 +88,13 @@ export function resolveWorld(world: world): resolveWorld { .flat(), ) as Tables; - const resolvedRootTables = resolveTables( - mapObject(rootTables, (table) => mergeIfUndefined(table, { namespace })), - scope, - ); + const resolvedStore = resolveStore(world); return mergeIfUndefined( { - tables: { ...resolvedRootTables, ...resolvedNamespacedTables }, - userTypes: world.userTypes ?? {}, - enums: world.enums ?? {}, - namespace, - codegen: mergeIfUndefined(resolveStoreCodegen(world.codegen), resolveCodegen(world.codegen)), + ...resolvedStore, + tables: { ...resolvedStore.tables, ...resolvedNamespacedTables }, + codegen: mergeIfUndefined(resolvedStore.codegen, resolveCodegen(world.codegen)), deployment: resolveDeployment(world.deployment), systems: resolveSystems(world.systems ?? CONFIG_DEFAULTS.systems), excludeSystems: get(world, "excludeSystems"), @@ -121,5 +105,6 @@ export function resolveWorld(world: world): resolveWorld { } export function defineWorld(world: validateWorld): resolveWorld { + validateWorld(world); return resolveWorld(world) as unknown as resolveWorld; } diff --git a/packages/world/ts/config/v2/worldWithShorthands.ts b/packages/world/ts/config/v2/worldWithShorthands.ts index 01dfa282cc..345a84d9a7 100644 --- a/packages/world/ts/config/v2/worldWithShorthands.ts +++ b/packages/world/ts/config/v2/worldWithShorthands.ts @@ -8,7 +8,6 @@ import { resolveTableShorthand, resolveTablesWithShorthands, validateTablesWithShorthands, - validateTableShorthand, Scope, } from "@latticexyz/store/config/v2"; import { mapObject } from "@latticexyz/common/utils"; @@ -59,25 +58,22 @@ export type validateNamespacesWithShorthands(world: world): resolveWorldWithShorthands { - validateWorldWithShorthands(world); - +export function resolveWorldWithShorthands( + world: world, +): resolveWorldWithShorthands { const scope = extendedScope(world); const tables = mapObject(world.tables ?? {}, (table) => { - return isTableShorthandInput(table) - ? resolveTableShorthand(table as validateTableShorthand, scope) - : table; + return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; }); const namespaces = mapObject(world.namespaces ?? {}, (namespace) => ({ ...namespace, tables: mapObject(namespace.tables ?? {}, (table) => { - return isTableShorthandInput(table) - ? resolveTableShorthand(table as validateTableShorthand, scope) - : table; + return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; }), })); const fullConfig = { ...world, tables, namespaces }; + validateWorld(fullConfig); return resolveWorld(fullConfig) as unknown as resolveWorldWithShorthands; } @@ -85,5 +81,6 @@ export function resolveWorldWithShorthands(world: world): resolveWorldWit export function defineWorldWithShorthands( world: validateWorldWithShorthands, ): resolveWorldWithShorthands { - return resolveWorldWithShorthands(world) as resolveWorldWithShorthands; + validateWorldWithShorthands(world); + return resolveWorldWithShorthands(world) as unknown as resolveWorldWithShorthands; }