From cc4f4246e52982354e398113c46442910f9b04bb Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 20 Mar 2024 15:37:32 +0000 Subject: [PATCH] feat(store, world): throw if required keys are not provided, more validations (#2481) --- packages/store/ts/config/v2/input.ts | 2 +- packages/store/ts/config/v2/schema.ts | 12 +++--- packages/store/ts/config/v2/table.test.ts | 50 +++++++++++++++++++++++ packages/store/ts/config/v2/table.ts | 27 ++++++++---- packages/world/ts/config/v2/world.ts | 2 +- 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/store/ts/config/v2/input.ts b/packages/store/ts/config/v2/input.ts index a83863fb73..1bda537786 100644 --- a/packages/store/ts/config/v2/input.ts +++ b/packages/store/ts/config/v2/input.ts @@ -26,7 +26,7 @@ export type TablesInput = { export type StoreInput = { readonly namespace?: string; - readonly tables: TablesInput; + readonly tables?: TablesInput; readonly userTypes?: UserTypes; readonly enums?: Enums; readonly codegen?: Partial; diff --git a/packages/store/ts/config/v2/schema.ts b/packages/store/ts/config/v2/schema.ts index 9c2e54a8fc..472c6df559 100644 --- a/packages/store/ts/config/v2/schema.ts +++ b/packages/store/ts/config/v2/schema.ts @@ -4,11 +4,13 @@ import { hasOwnKey, isObject } from "./generics"; import { SchemaInput } from "./input"; import { FixedArrayAbiType, fixedArrayToArray, isFixedArrayAbiType } from "@latticexyz/schema-type/internal"; -export type validateSchema = { - [key in keyof schema]: schema[key] extends FixedArrayAbiType - ? schema[key] - : conform; -}; +export type validateSchema = schema extends string + ? SchemaInput + : { + [key in keyof schema]: schema[key] extends FixedArrayAbiType + ? schema[key] + : conform; + }; export function validateSchema( schema: unknown, diff --git a/packages/store/ts/config/v2/table.test.ts b/packages/store/ts/config/v2/table.test.ts index 761c6250ec..21cff20396 100644 --- a/packages/store/ts/config/v2/table.test.ts +++ b/packages/store/ts/config/v2/table.test.ts @@ -185,6 +185,56 @@ describe("resolveTable", () => { .throws('Invalid key. Expected `("id" | "age")[]`, received `["NotAKey"]`') .type.errors(`Type '"NotAKey"' is not assignable to type '"id" | "age"'`); }); + + it("should throw if no key is provided", () => { + attest(() => + // @ts-expect-error Property 'key' is missing in type + defineTable({ + schema: { id: "address" }, + name: "", + }), + ) + .throws('Invalid key. Expected `("id")[]`, received `undefined') + .type.errors("Property 'key' is missing in type"); + }); + + it("should throw if a string is provided as key", () => { + attest(() => + defineTable({ + schema: { id: "address" }, + // @ts-expect-error Type 'string' is not assignable to type 'string[]' + key: "", + name: "", + }), + ) + .throws('Invalid key. Expected `("id")[]`, received ``') + .type.errors("Type 'string' is not assignable to type 'string[]'"); + }); + + it("should throw if a string is provided as schema", () => { + attest(() => + defineTable({ + // @ts-expect-error Type 'string' is not assignable to type 'SchemaInput'. + schema: "uint256", + key: [], + name: "", + }), + ) + .throws('Error: Expected schema, received "uint256"') + .type.errors("Type 'string' is not assignable to type 'SchemaInput'."); + }); + + it("should throw if an unknown key is provided", () => { + attest(() => + defineTable({ + schema: { id: "address" }, + key: ["id"], + name: "", + // @ts-expect-error Key `keySchema` does not exist in TableInput + keySchema: { id: "address" }, + }), + ).type.errors("Key `keySchema` does not exist in TableInput "); + }); }); // TODO: move tests to protocol parser after we add arktype diff --git a/packages/store/ts/config/v2/table.ts b/packages/store/ts/config/v2/table.ts index 66273aeee2..81ba4cea96 100644 --- a/packages/store/ts/config/v2/table.ts +++ b/packages/store/ts/config/v2/table.ts @@ -1,4 +1,4 @@ -import { ErrorMessage, conform, narrow } from "@arktype/util"; +import { ErrorMessage, conform, narrow, requiredKeyOf } from "@arktype/util"; import { isStaticAbiType } from "@latticexyz/schema-type/internal"; import { Hex } from "viem"; import { get, hasOwnKey } from "./generics"; @@ -37,9 +37,11 @@ export function isValidPrimaryKey = { - [i in keyof keys]: keys[i] extends validKeys ? keys[i] : validKeys; -}; +export type validateKeys = keys extends string[] + ? { + [i in keyof keys]: keys[i] extends validKeys ? keys[i] : validKeys; + } + : string[]; export type ValidateTableOptions = { inStoreContext: boolean }; @@ -48,14 +50,21 @@ export type validateTable< scope extends Scope = AbiTypeScope, options extends ValidateTableOptions = { inStoreContext: false }, > = { - [key in keyof input]: key extends "key" - ? validateKeys, SchemaInput>, scope>, input[key]> + [key in + | keyof input + | Exclude< + requiredKeyOf, + options["inStoreContext"] extends true ? "name" | "namespace" : "" + >]: key extends "key" + ? validateKeys, SchemaInput>, scope>, get> : key extends "schema" - ? validateSchema + ? validateSchema, scope> : key extends "name" | "namespace" ? options["inStoreContext"] extends true ? ErrorMessage<"Overrides of `name` and `namespace` are not allowed for tables in a store config"> - : narrow + : key extends keyof input + ? narrow + : never : key extends keyof TableInput ? TableInput[key] : ErrorMessage<`Key \`${key & string}\` does not exist in TableInput`>; @@ -82,7 +91,7 @@ export function validateTable( .join(" | ")})[]\`, received \`${ hasOwnKey(input, "key") && Array.isArray(input.key) ? `[${input.key.map((item) => `"${item}"`).join(", ")}]` - : "undefined" + : String(get(input, "key")) }\``, ); } diff --git a/packages/world/ts/config/v2/world.ts b/packages/world/ts/config/v2/world.ts index 784db25ce2..da5f83c5e0 100644 --- a/packages/world/ts/config/v2/world.ts +++ b/packages/world/ts/config/v2/world.ts @@ -77,7 +77,7 @@ export function resolveWorld(world: world): reso const resolvedNamespacedTables = Object.fromEntries( Object.entries(namespaces) .map(([namespaceKey, namespace]) => - Object.entries(namespace.tables).map(([tableKey, table]) => { + Object.entries(namespace.tables ?? {}).map(([tableKey, table]) => { validateTable(table, scope); return [ `${namespaceKey}__${tableKey}`,