Skip to content

Commit

Permalink
feat(store/world): disable namespace/name override on store/world…
Browse files Browse the repository at this point in the history
… table inputs, move validation to define (#2472)
  • Loading branch information
alvrs authored Mar 19, 2024
1 parent f8f3095 commit 0a1fff9
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 98 deletions.
18 changes: 8 additions & 10 deletions packages/store/ts/config/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,20 @@ function validateX(x: unknonw): asserts x is X {
type resolveX<x> = 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<const x>(x: validateX<x>): resolveX<x> {
return resolveX(x);
function resolveX<const x extends X>(x: x): resolveX<x> {
//
}

/**
* 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<const x extends X>(x: x): resolveX<x> {
function defineX<const x>(x: validateX<x>): resolveX<x> {
validateX(x);
//
return resolveX(x);
}
```

Expand Down
2 changes: 1 addition & 1 deletion packages/store/ts/config/v2/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type TableInput = {
};

export type TablesInput = {
readonly [key: string]: TableInput;
readonly [key: string]: Omit<TableInput, "namespace" | "name">;
};

export type StoreInput = {
Expand Down
5 changes: 2 additions & 3 deletions packages/store/ts/config/v2/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ export type resolveSchema<schema, scope extends Scope> = evaluate<{
};
}>;

export function resolveSchema<schema, scope extends Scope = AbiTypeScope>(
export function resolveSchema<schema extends SchemaInput, scope extends Scope = AbiTypeScope>(
schema: schema,
scope: scope = AbiTypeScope as unknown as scope,
): resolveSchema<schema, scope> {
validateSchema(schema, scope);

return Object.fromEntries(
Object.entries(schema).map(([key, internalType]) => [
key,
Expand All @@ -59,6 +57,7 @@ export function defineSchema<schema, scope extends AbiTypeScope = AbiTypeScope>(
schema: validateSchema<schema, scope>,
scope: scope = AbiTypeScope as scope,
): resolveSchema<schema, scope> {
validateSchema(schema, scope);
return resolveSchema(schema, scope) as resolveSchema<schema, scope>;
}

Expand Down
41 changes: 37 additions & 4 deletions packages/store/ts/config/v2/store.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -548,7 +548,7 @@ describe("defineStore", () => {
userTypes: { CustomType: { type: "address", filePath: "path/to/file" } },
});

attest<true, typeof config extends Config ? true : false>();
attest<true, typeof config extends Store ? true : false>();
});

it("should use the global namespace instead for tables", () => {
Expand All @@ -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" }),
);
});
Expand All @@ -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");
});
});
27 changes: 18 additions & 9 deletions packages/store/ts/config/v2/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,13 +37,19 @@ export function validateStore(store: unknown): asserts store is StoreInput {
}
}

type keyPrefix<store> = "namespace" extends keyof store
? store["namespace"] extends ""
? ""
: `${store["namespace"] & string}__`
: "";

export type resolveStore<store> = evaluate<{
readonly tables: "tables" extends keyof store
? resolveTables<
{
[key in keyof store["tables"]]: mergeIfUndefined<
store["tables"][key],
{ namespace: get<store, "namespace"> }
[tableKey in keyof store["tables"] & string as `${keyPrefix<store>}${tableKey}`]: mergeIfUndefined<
store["tables"][tableKey],
{ namespace: get<store, "namespace">; name: tableKey }
>;
},
extendedScope<store>
Expand All @@ -56,12 +61,15 @@ export type resolveStore<store> = evaluate<{
readonly codegen: "codegen" extends keyof store ? resolveCodegen<store["codegen"]> : resolveCodegen<{}>;
}>;

export function resolveStore<const store>(store: store): resolveStore<store> {
validateStore(store);

export function resolveStore<const store extends StoreInput>(store: store): resolveStore<store> {
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 ?? {},
Expand All @@ -72,5 +80,6 @@ export function resolveStore<const store>(store: store): resolveStore<store> {
}

export function defineStore<const store>(store: validateStore<store>): resolveStore<store> {
return resolveStore(store) as resolveStore<store>;
validateStore(store);
return resolveStore(store) as unknown as resolveStore<store>;
}
1 change: 1 addition & 0 deletions packages/store/ts/config/v2/storeWithShorthands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ describe("defineStoreWithShorthands", () => {
const config = defineStoreWithShorthands({
tables: { Example: { id: "address", name: "string", age: "uint256" } },
});

const expected = {
tables: {
Example: {
Expand Down
10 changes: 6 additions & 4 deletions packages/store/ts/config/v2/storeWithShorthands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export type resolveStoreWithShorthands<store> = resolveStore<{
: store[key];
}>;

export function resolveStoreWithShorthands<const store>(store: store): resolveStoreWithShorthands<store> {
validateStoreWithShorthands(store);

export function resolveStoreWithShorthands<const store extends StoreWithShorthandsInput>(
store: store,
): resolveStoreWithShorthands<store> {
const scope = extendedScope(store);
const fullConfig = {
...store,
Expand All @@ -39,11 +39,13 @@ export function resolveStoreWithShorthands<const store>(store: store): resolveSt
}),
};

validateStore(fullConfig);
return resolveStore(fullConfig) as unknown as resolveStoreWithShorthands<store>;
}

export function defineStoreWithShorthands<const store>(
store: validateStoreWithShorthands<store>,
): resolveStoreWithShorthands<store> {
return resolveStoreWithShorthands(store) as resolveStoreWithShorthands<store>;
validateStoreWithShorthands(store);
return resolveStoreWithShorthands(store) as unknown as resolveStoreWithShorthands<store>;
}
33 changes: 17 additions & 16 deletions packages/store/ts/config/v2/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,25 @@ export function isValidPrimaryKey<schema extends SchemaInput, scope extends Scop
);
}

/** @deprecated */
export function isTableInput(input: unknown): input is TableInput {
return (
typeof input === "object" &&
input !== null &&
hasOwnKey(input, "schema") &&
hasOwnKey(input, "key") &&
Array.isArray(input["key"])
);
}

export type validateKeys<validKeys extends PropertyKey, keys> = {
[i in keyof keys]: keys[i] extends validKeys ? keys[i] : validKeys;
};

export type validateTable<input, scope extends Scope = AbiTypeScope> = {
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<getStaticAbiTypeKeys<conform<get<input, "schema">, SchemaInput>, scope>, input[key]>
: key extends "schema"
? validateSchema<input[key], scope>
: key extends "name" | "namespace"
? narrow<input[key]>
? options["inStoreContext"] extends true
? ErrorMessage<"Overrides of `name` and `namespace` are not allowed for tables in a store config">
: narrow<input[key]>
: key extends keyof TableInput
? TableInput[key]
: ErrorMessage<`Key \`${key & string}\` does not exist in TableInput`>;
Expand All @@ -67,6 +64,7 @@ export type validateTable<input, scope extends Scope = AbiTypeScope> = {
export function validateTable<input, scope extends Scope = AbiTypeScope>(
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)}\``);
Expand All @@ -88,6 +86,10 @@ export function validateTable<input, scope extends Scope = AbiTypeScope>(
}\``,
);
}

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<input extends TableInput> = {
Expand Down Expand Up @@ -142,12 +144,10 @@ export type resolveTable<input, scope extends Scope = Scope> = input extends Tab
}
: never;

export function resolveTable<input, scope extends Scope = AbiTypeScope>(
export function resolveTable<input extends TableInput, scope extends Scope = AbiTypeScope>(
input: input,
scope: scope = AbiTypeScope as unknown as scope,
): resolveTable<input, scope> {
validateTable(input, scope);

const name = input.name;
const type = input.type ?? TABLE_DEFAULTS.type;
const namespace = input.namespace ?? TABLE_DEFAULTS.namespace;
Expand Down Expand Up @@ -180,5 +180,6 @@ export function defineTable<input, scope extends Scope = AbiTypeScope>(
input: validateTable<input, scope>,
scope: scope = AbiTypeScope as unknown as scope,
): resolveTable<input, scope> {
validateTable(input, scope);
return resolveTable(input, scope) as resolveTable<input, scope>;
}
21 changes: 11 additions & 10 deletions packages/store/ts/config/v2/tableShorthand.ts
Original file line number Diff line number Diff line change
@@ -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.">;
Expand All @@ -17,9 +17,11 @@ export function isTableShorthandInput(shorthand: unknown): shorthand is TableSho
);
}

export type validateTableWithShorthand<table, scope extends Scope = AbiTypeScope> = table extends TableShorthandInput
? validateTableShorthand<table, scope>
: validateTable<table, scope>;
export type validateTableWithShorthand<
table,
scope extends Scope = AbiTypeScope,
options extends ValidateTableOptions = { inStoreContext: boolean },
> = table extends TableShorthandInput ? validateTableShorthand<table, scope> : validateTable<table, scope, options>;

// We don't use `conform` here because the restrictions we're imposing here are not native to typescript
export type validateTableShorthand<input, scope extends Scope = AbiTypeScope> = input extends SchemaInput
Expand Down Expand Up @@ -72,12 +74,10 @@ export type resolveTableShorthand<shorthand, scope extends Scope = AbiTypeScope>
: never
: never;

export function resolveTableShorthand<shorthand, scope extends Scope = AbiTypeScope>(
export function resolveTableShorthand<shorthand extends TableShorthandInput, scope extends Scope = AbiTypeScope>(
shorthand: shorthand,
scope: scope = AbiTypeScope as unknown as scope,
): resolveTableShorthand<shorthand, scope> {
validateTableShorthand(shorthand, scope);

if (isSchemaInput(shorthand, scope)) {
return {
schema: shorthand,
Expand All @@ -98,6 +98,7 @@ export function defineTableShorthand<shorthand, scope extends Scope = AbiTypeSco
shorthand: validateTableShorthand<shorthand, scope>,
scope: scope = AbiTypeScope as unknown as scope,
): resolveTableShorthand<shorthand, scope> {
validateTableShorthand(shorthand, scope);
return resolveTableShorthand(shorthand, scope) as resolveTableShorthand<shorthand, scope>;
}

Expand All @@ -110,11 +111,11 @@ export type resolveTableWithShorthand<table, scope extends Scope = AbiTypeScope>
: table;

export type resolveTablesWithShorthands<input, scope extends AbiTypeScope = AbiTypeScope> = {
[key in keyof input]: mergeIfUndefined<resolveTableWithShorthand<input[key], scope>, { name: key }>;
[key in keyof input]: resolveTableWithShorthand<input[key], scope>;
};

export type validateTablesWithShorthands<tables, scope extends Scope = AbiTypeScope> = {
[key in keyof tables]: validateTableWithShorthand<tables[key], scope>;
[key in keyof tables]: validateTableWithShorthand<tables[key], scope, { inStoreContext: true }>;
};

export function validateTablesWithShorthands<scope extends Scope = AbiTypeScope>(
Expand Down
Loading

0 comments on commit 0a1fff9

Please sign in to comment.