From b5d1ca7dc155ee238427b5760735b768d0237482 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 5 Jul 2024 15:33:06 +0100 Subject: [PATCH] start namespace helpers --- packages/store/ts/config/v2/namespace.ts | 57 ++++++++++++ packages/store/ts/config/v2/store.test.ts | 87 ----------------- packages/store/ts/config/v2/store.ts | 93 +++++++------------ packages/store/ts/config/v2/tables.ts | 2 +- packages/world/ts/actions/callFrom.ts | 2 +- packages/world/ts/config/v2/world.test.ts | 34 ++++--- packages/world/ts/config/v2/world.ts | 21 ----- .../ts/config/v2/worldWithShorthands.test.ts | 1 + .../world/ts/config/v2/worldWithShorthands.ts | 60 ++++++++---- 9 files changed, 154 insertions(+), 203 deletions(-) create mode 100644 packages/store/ts/config/v2/namespace.ts diff --git a/packages/store/ts/config/v2/namespace.ts b/packages/store/ts/config/v2/namespace.ts new file mode 100644 index 00000000000..c02cd7f0d15 --- /dev/null +++ b/packages/store/ts/config/v2/namespace.ts @@ -0,0 +1,57 @@ +import { ErrorMessage, flatMorph } from "@arktype/util"; +import { get, hasOwnKey, mergeIfUndefined } from "./generics"; +import { CONFIG_DEFAULTS } from "./defaults"; +import { NamespaceInput } from "./input"; +import { resolveTables, validateTables } from "./tables"; +import { AbiTypeScope, Scope } from "./scope"; + +export type validateNamespace = { + [key in keyof input]: key extends "tables" + ? validateTables + : key extends keyof NamespaceInput + ? NamespaceInput[key] + : ErrorMessage<`\`${key & string}\` is not a valid namespace config option.`>; +}; + +export function validateNamespace( + input: unknown, + scope: scope, +): asserts input is NamespaceInput { + if (hasOwnKey(input, "namespace") && typeof input.namespace === "string" && input.namespace.length > 14) { + throw new Error(`\`namespace\` must fit into a \`bytes14\`, but "${input.namespace}" is too long.`); + } + if (hasOwnKey(input, "tables")) { + validateTables(input.tables, scope); + } +} + +export type resolveNamespace = { + readonly namespace: "namespace" extends keyof input ? input["namespace"] : CONFIG_DEFAULTS["namespace"]; + readonly tables: "tables" extends keyof input + ? resolveTables< + { + [label in keyof input["tables"]]: mergeIfUndefined< + input["tables"][label], + { label: label; namespace: get } + >; + }, + scope + > + : {}; +}; + +export function resolveNamespace( + input: input, + scope: scope, +): resolveNamespace { + return { + namespace: input.namespace ?? CONFIG_DEFAULTS["namespace"], + tables: resolveTables( + flatMorph(input.tables ?? {}, (label, table) => { + const namespace = input.namespace; + return [label, mergeIfUndefined(table, { label, namespace })]; + }), + scope, + ), + } as never; +} diff --git a/packages/store/ts/config/v2/store.test.ts b/packages/store/ts/config/v2/store.test.ts index 67aef4bab59..a49c612481d 100644 --- a/packages/store/ts/config/v2/store.test.ts +++ b/packages/store/ts/config/v2/store.test.ts @@ -559,91 +559,4 @@ describe("defineStore", () => { }), ).type.errors("`invalidOption` is not a valid Store config option."); }); - - it("should namespace output directories for tables", () => { - const config = defineStore({ - namespace: "app", - codegen: { - namespaceDirectories: true, - }, - tables: { - NamespaceDir: { - schema: { name: "string" }, - key: [], - }, - NotNamespaceDir: { - schema: { name: "string" }, - key: [], - codegen: { - outputDirectory: "tables", - }, - }, - }, - }); - - const expected = { - sourceDirectory: "src", - tables: { - NamespaceDir: { - tableId: resourceToHex({ type: "table", namespace: "app", name: "NamespaceDir" }), - schema: { - name: { - type: "string", - internalType: "string", - }, - }, - key: [], - label: "NamespaceDir", - name: "NamespaceDir", - namespace: "app", - codegen: { - ...TABLE_CODEGEN_DEFAULTS, - dataStruct: false as boolean, - outputDirectory: "app/tables" as string, - }, - type: "table", - deploy: TABLE_DEPLOY_DEFAULTS, - }, - NotNamespaceDir: { - tableId: resourceToHex({ type: "table", namespace: "app", name: "NotNamespaceDir" }), - schema: { - name: { - type: "string", - internalType: "string", - }, - }, - key: [], - label: "NotNamespaceDir", - name: "NotNamespaceDir", - namespace: "app", - codegen: { - ...TABLE_CODEGEN_DEFAULTS, - dataStruct: false as boolean, - outputDirectory: "tables", - }, - type: "table", - deploy: TABLE_DEPLOY_DEFAULTS, - }, - }, - userTypes: {}, - enums: {}, - enumValues: {}, - namespace: "app", - codegen: { - ...CODEGEN_DEFAULTS, - namespaceDirectories: true, - }, - } as const; - - // Running attest on the whole object is hard to parse when it fails, so test the inner objects first - attest(config.codegen).equals(expected.codegen); - attest(config.tables.NamespaceDir.codegen).equals( - expected.tables.NamespaceDir.codegen, - ); - attest(config.tables.NotNamespaceDir.codegen).equals( - expected.tables.NotNamespaceDir.codegen, - ); - - attest(config).equals(expected); - }); }); diff --git a/packages/store/ts/config/v2/store.ts b/packages/store/ts/config/v2/store.ts index faa59dda639..7f797e01c72 100644 --- a/packages/store/ts/config/v2/store.ts +++ b/packages/store/ts/config/v2/store.ts @@ -1,12 +1,13 @@ -import { ErrorMessage, evaluate, flatMorph, narrow } from "@arktype/util"; -import { get, hasOwnKey, mergeIfUndefined } from "./generics"; +import { ErrorMessage, evaluate, narrow } from "@arktype/util"; +import { get, hasOwnKey } from "./generics"; import { UserTypes } from "./output"; import { CONFIG_DEFAULTS } from "./defaults"; import { StoreInput } from "./input"; -import { resolveTables, validateTables } from "./tables"; +import { validateTables } from "./tables"; import { scopeWithUserTypes, validateUserTypes } from "./userTypes"; import { mapEnums, resolveEnums, scopeWithEnums } from "./enums"; import { resolveCodegen } from "./codegen"; +import { resolveNamespace, validateNamespace } from "./namespace"; export type extendedScope = scopeWithEnums, scopeWithUserTypes>>; @@ -14,81 +15,51 @@ export function extendedScope(input: input): extendedScope { return scopeWithEnums(get(input, "enums"), scopeWithUserTypes(get(input, "userTypes"))); } -export type validateStore = { - [key in keyof store]: key extends "tables" - ? validateTables> +export type validateStore = { + [key in keyof input]: key extends "tables" + ? validateTables> : key extends "userTypes" ? UserTypes : key extends "enums" - ? narrow + ? narrow : key extends keyof StoreInput ? StoreInput[key] : ErrorMessage<`\`${key & string}\` is not a valid Store config option.`>; }; -export function validateStore(store: unknown): asserts store is StoreInput { - const scope = extendedScope(store); - if (hasOwnKey(store, "tables")) { - validateTables(store.tables, scope); - } +export function validateStore(input: unknown): asserts input is StoreInput { + const scope = extendedScope(input); + validateNamespace(input, scope); - if (hasOwnKey(store, "userTypes")) { - validateUserTypes(store.userTypes); + if (hasOwnKey(input, "userTypes")) { + validateUserTypes(input.userTypes); } } -export type resolveStore = { - readonly sourceDirectory: "sourceDirectory" extends keyof store - ? store["sourceDirectory"] +export type resolveStore = resolveNamespace & { + readonly sourceDirectory: "sourceDirectory" extends keyof input + ? input["sourceDirectory"] : CONFIG_DEFAULTS["sourceDirectory"]; - readonly tables: "tables" extends keyof store - ? resolveTables< - { - [label in keyof store["tables"]]: mergeIfUndefined< - store["tables"][label], - { label: label; namespace: get } - >; - }, - extendedScope - > - : {}; - readonly userTypes: "userTypes" extends keyof store ? store["userTypes"] : {}; - readonly enums: "enums" extends keyof store ? evaluate> : {}; - readonly enumValues: "enums" extends keyof store ? evaluate> : {}; - readonly namespace: "namespace" extends keyof store ? store["namespace"] : CONFIG_DEFAULTS["namespace"]; - readonly codegen: "codegen" extends keyof store ? resolveCodegen : resolveCodegen<{}>; + readonly userTypes: "userTypes" extends keyof input ? input["userTypes"] : {}; + readonly enums: "enums" extends keyof input ? evaluate> : {}; + readonly enumValues: "enums" extends keyof input ? evaluate> : {}; + readonly codegen: "codegen" extends keyof input ? resolveCodegen : resolveCodegen<{}>; }; -export function resolveStore(store: store): resolveStore { - // TODO: default `namespaceDirectories` to true if using top-level `namespaces` key (once its migrated to store) - const codegen = resolveCodegen(store.codegen); +export function resolveStore(input: input): resolveStore { + const scope = extendedScope(input); + const namespace = resolveNamespace(input, scope); return { - sourceDirectory: store.sourceDirectory ?? CONFIG_DEFAULTS["sourceDirectory"], - tables: resolveTables( - flatMorph(store.tables ?? {}, (label, table) => { - const namespace = store.namespace; - return [ - label, - mergeIfUndefined(table, { - namespace: namespace, - label, - codegen: mergeIfUndefined(table.codegen ?? {}, { - outputDirectory: codegen.namespaceDirectories && namespace?.length ? `${namespace}/tables` : "tables", - }), - }), - ]; - }), - extendedScope(store), - ), - userTypes: store.userTypes ?? {}, - enums: resolveEnums(store.enums ?? {}), - enumValues: mapEnums(store.enums ?? {}), - namespace: store.namespace ?? CONFIG_DEFAULTS["namespace"], - codegen, + ...namespace, + sourceDirectory: input.sourceDirectory ?? CONFIG_DEFAULTS["sourceDirectory"], + userTypes: input.userTypes ?? {}, + enums: resolveEnums(input.enums ?? {}), + enumValues: mapEnums(input.enums ?? {}), + codegen: resolveCodegen(input.codegen), } as never; } -export function defineStore(store: validateStore): resolveStore { - validateStore(store); - return resolveStore(store) as never; +export function defineStore(input: validateStore): resolveStore { + validateStore(input); + return resolveStore(input) as never; } diff --git a/packages/store/ts/config/v2/tables.ts b/packages/store/ts/config/v2/tables.ts index aed0202eb9d..85aa842a965 100644 --- a/packages/store/ts/config/v2/tables.ts +++ b/packages/store/ts/config/v2/tables.ts @@ -29,7 +29,7 @@ export type resolveTables = evaluate export function resolveTables( tables: tables, - scope: scope = AbiTypeScope as unknown as scope, + scope: scope, ): resolveTables { if (!isObject(tables)) { throw new Error(`Expected tables config, received ${JSON.stringify(tables)}`); diff --git a/packages/world/ts/actions/callFrom.ts b/packages/world/ts/actions/callFrom.ts index 7ee99401dfd..c13ce438ce8 100644 --- a/packages/world/ts/actions/callFrom.ts +++ b/packages/world/ts/actions/callFrom.ts @@ -123,7 +123,7 @@ async function retrieveSystemFunctionFromContract( worldAddress: Hex, worldFunctionSelector: Hex, ): Promise { - const table = worldConfig.tables.world__FunctionSelectors; + const table = worldConfig.tables.FunctionSelectors; const keySchema = getSchemaTypes(getKeySchema(table)); const valueSchema = getSchemaTypes(getValueSchema(table)); diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 3cfc1664642..7f9f0e051ae 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -59,6 +59,7 @@ describe("defineWorld", () => { }, }, key: ["id"], + label: "ExampleTable", name: "ExampleTable", namespace: "ExampleNS", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, @@ -122,6 +123,7 @@ describe("defineWorld", () => { }, }, key: ["id"], + label: "ExampleTable", name: "ExampleTable", namespace: "ExampleNS", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, @@ -194,8 +196,8 @@ describe("defineWorld", () => { }); attest<"namespace">(config.namespace).equals("namespace"); - attest<"AnotherOne">(config.tables.AnotherOne__Example.namespace).equals("AnotherOne"); - attest(config.tables.AnotherOne__Example.tableId).equals( + attest<"AnotherOne">(config.namespaces.AnotherOne.tables.Example.namespace).equals("AnotherOne"); + attest(config.namespaces.AnotherOne.tables.Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "AnotherOne" }), ); }); @@ -232,6 +234,7 @@ describe("defineWorld", () => { }, }, key: ["age"], + label: "Example", name: "Example", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, @@ -282,6 +285,7 @@ describe("defineWorld", () => { }, }, key: ["age"], + label: "Example", name: "Example", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, @@ -331,6 +335,7 @@ describe("defineWorld", () => { }, }, key: ["age", "id"], + label: "Example", name: "Example", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, @@ -382,6 +387,7 @@ describe("defineWorld", () => { }, }, key: ["firstKey", "firstAge"], + label: "First", name: "First", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, @@ -405,6 +411,7 @@ describe("defineWorld", () => { }, }, key: ["secondKey", "secondAge"], + label: "Second", name: "Second", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, @@ -460,6 +467,7 @@ describe("defineWorld", () => { }, }, key: ["firstKey", "firstAge"], + label: "First", name: "First", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, @@ -483,6 +491,7 @@ describe("defineWorld", () => { }, }, key: ["secondKey", "secondAge"], + label: "Second", name: "Second", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, @@ -594,6 +603,7 @@ describe("defineWorld", () => { }, }, key: ["name"], + label: "Example", name: "Example", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: true as boolean }, @@ -654,8 +664,8 @@ describe("defineWorld", () => { }); attest<"namespace">(config.namespace).equals("namespace"); - attest<"namespace">(config.tables.namespace__Example.namespace).equals("namespace"); - attest(config.tables.namespace__Example.tableId).equals( + attest<"namespace">(config.tables.Example.namespace).equals("namespace"); + attest(config.tables.Example.tableId).equals( resourceToHex({ type: "table", name: "Example", namespace: "namespace" }), ); }); @@ -672,7 +682,7 @@ describe("defineWorld", () => { }, }); - attest<"CustomNS__Example", keyof typeof config.tables>(); + attest<"Example", keyof typeof config.tables>(); }); it("should throw if namespace is overridden in top level tables", () => { @@ -691,22 +701,22 @@ describe("defineWorld", () => { ).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", () => { + it("should throw if label 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", + // @ts-expect-error "Overrides of `label` and `namespace` are not allowed for tables in a store config" + label: "NotAllowed", }, }, }), ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); }); - it.skip("should throw if name is overridden in namespaced tables", () => { + it.skip("should throw if label is overridden in namespaced tables", () => { attest(() => defineWorld({ // @ts-expect-error TODO: remove once namespaces support ships @@ -716,13 +726,13 @@ describe("defineWorld", () => { Example: { schema: { id: "address" }, key: ["id"], - name: "NotAllowed", + label: "NotAllowed", }, }, }, }, }), - ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + ).throwsAndHasTypeError("Overrides of `label` and `namespace` are not allowed for tables in a store config"); }); it.skip("should throw if namespace is overridden in namespaced tables", () => { @@ -741,7 +751,7 @@ describe("defineWorld", () => { }, }, }), - ).throwsAndHasTypeError("Overrides of `name` and `namespace` are not allowed for tables in a store config"); + ).throwsAndHasTypeError("Overrides of `label` and `namespace` are not allowed for tables in a store config"); }); it("should throw if namespaces are defined (TODO: remove once namespaces support ships)", () => { diff --git a/packages/world/ts/config/v2/world.ts b/packages/world/ts/config/v2/world.ts index a04d90ed428..382eb0624dd 100644 --- a/packages/world/ts/config/v2/world.ts +++ b/packages/world/ts/config/v2/world.ts @@ -3,8 +3,6 @@ import { UserTypes, extendedScope, get, - resolveTable, - validateTable, mergeIfUndefined, validateTables, resolveStore, @@ -14,7 +12,6 @@ import { } from "@latticexyz/store/config/v2"; import { SystemsInput, WorldInput } from "./input"; import { CONFIG_DEFAULTS, MODULE_DEFAULTS } from "./defaults"; -import { Tables } from "@latticexyz/store/internal"; import { resolveSystems } from "./systems"; import { resolveNamespacedTables, validateNamespaces } from "./namespaces"; import { resolveCodegen } from "./codegen"; @@ -64,23 +61,6 @@ export type resolveWorld = evaluate< >; export function resolveWorld(world: world): resolveWorld { - const scope = extendedScope(world); - const namespaces = world.namespaces ?? {}; - - const resolvedNamespacedTables = Object.fromEntries( - Object.entries(namespaces) - .map(([namespaceKey, namespace]) => - Object.entries(namespace.tables ?? {}).map(([tableKey, table]) => { - validateTable(table, scope); - return [ - `${namespaceKey}__${tableKey}`, - resolveTable(mergeIfUndefined(table, { namespace: namespaceKey, name: tableKey }), scope), - ]; - }), - ) - .flat(), - ) as Tables; - const resolvedStore = resolveStore(world); const modules = (world.modules ?? CONFIG_DEFAULTS.modules).map((mod) => mergeIfUndefined(mod, MODULE_DEFAULTS)); @@ -88,7 +68,6 @@ export function resolveWorld(world: world): reso return mergeIfUndefined( { ...resolvedStore, - tables: { ...resolvedStore.tables, ...resolvedNamespacedTables }, codegen: mergeIfUndefined(resolvedStore.codegen, resolveCodegen(world.codegen)), deploy: resolveDeploy(world.deploy), systems: resolveSystems(world.systems ?? CONFIG_DEFAULTS.systems), diff --git a/packages/world/ts/config/v2/worldWithShorthands.test.ts b/packages/world/ts/config/v2/worldWithShorthands.test.ts index d901b43fc92..c4df875ad7c 100644 --- a/packages/world/ts/config/v2/worldWithShorthands.test.ts +++ b/packages/world/ts/config/v2/worldWithShorthands.test.ts @@ -164,6 +164,7 @@ describe("defineWorldWithShorthands", () => { }, }, key: ["id"], + label: "Name", name: "Name", namespace: "", codegen: { ...TABLE_CODEGEN_DEFAULTS, dataStruct: false as boolean }, diff --git a/packages/world/ts/config/v2/worldWithShorthands.ts b/packages/world/ts/config/v2/worldWithShorthands.ts index 948331da673..9ea05d0d802 100644 --- a/packages/world/ts/config/v2/worldWithShorthands.ts +++ b/packages/world/ts/config/v2/worldWithShorthands.ts @@ -20,10 +20,10 @@ export type resolveWorldWithShorthands = resolveWorld<{ ? resolveTablesWithShorthands> : key extends "namespaces" ? { - [namespaceKey in keyof world[key]]: { - [namespaceProp in keyof world[key][namespaceKey]]: namespaceProp extends "tables" - ? resolveTablesWithShorthands> - : world[key][namespaceKey][namespaceProp]; + [namespaceLabel in keyof world[key]]: { + [namespaceOption in keyof world[key][namespaceLabel]]: namespaceOption extends "tables" + ? resolveTablesWithShorthands> + : world[key][namespaceLabel][namespaceOption]; }; } : world[key]; @@ -39,22 +39,23 @@ export type validateWorldWithShorthands = { 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); + for (const label of Object.keys(world.namespaces)) { + validateTablesWithShorthands(getPath(world.namespaces, [label, "tables"]) ?? {}, scope); } } } export type validateNamespacesWithShorthands = { - [namespace in keyof namespaces]: { - [key in keyof namespaces[namespace]]: key extends "tables" - ? validateTablesWithShorthands - : validateNamespace[key]; + [label in keyof namespaces]: { + [key in keyof namespaces[label]]: key extends "tables" + ? validateTablesWithShorthands + : validateNamespace[key]; }; }; @@ -62,17 +63,36 @@ export function resolveWorldWithShorthands { const scope = extendedScope(world); - const tables = mapObject(world.tables ?? {}, (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, scope) : table; - }), - })); - const fullConfig = { ...world, tables, namespaces }; + const tables = + world.tables != null + ? mapObject(world.tables, (table) => { + return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; + }) + : null; + + const namespaces = + world.namespaces != null + ? mapObject(world.namespaces, (namespace) => { + const tables = + namespace.tables != null + ? mapObject(namespace.tables, (table) => { + return isTableShorthandInput(table) ? resolveTableShorthand(table, scope) : table; + }) + : null; + return { + ...namespace, + ...(tables != null ? { tables } : null), + }; + }) + : null; + + const fullConfig = { + ...world, + ...(tables != null ? { tables } : null), + ...(namespaces != null ? { namespaces } : null), + }; + validateWorld(fullConfig); return resolveWorld(fullConfig) as never;