UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

172 lines (150 loc) • 7.13 kB
import { describe, expect, it } from "bun:test" import { isColumn } from "./column" import { InvalidAttributeNameError, InvalidValueError, SchemaDefinitionError, TooManyAttributesError, } from "./errors" import { defineSchema, isSchema, MAX_ATTRIBUTES, SCHEMA_ATTRIBUTE } from "./schema" import { addr, bytes, dec, i32, str, u64 } from "./values" describe("columns", () => { it("start unindexed, required and without a default", () => { expect(str()).toMatchObject({ type: "str", isIndexed: false, isOptional: false, defaultValue: undefined, }) }) it("refine by returning a new column, leaving the original alone", () => { const base = dec() const refined = base.indexed().optional() expect(refined).toMatchObject({ type: "dec", isIndexed: true, isOptional: true }) expect(base).toMatchObject({ isIndexed: false, isOptional: false }) expect(Object.isFrozen(refined)).toBe(true) }) it("keep every refinement, whatever order they are chained in", () => { const a = i32().default(3).indexed().optional() const b = i32().optional().indexed().default(3) for (const column of [a, b]) { expect(column).toMatchObject({ isIndexed: true, isOptional: true, defaultValue: i32(3) }) } }) it("take a default as the constructor's argument, or as a value of the column's type", () => { expect(dec().default("4.1").defaultValue).toEqual(dec("4.1")) expect(dec().default(dec("4.1")).defaultValue).toEqual(dec("4.1")) expect(u64().default(5).defaultValue).toEqual(u64(5n)) expect(bytes().default(new Uint8Array([1])).defaultValue?.value).toBe("0x01") }) it("validate a default when it is declared, not when it is first written", () => { expect(() => dec().default("4.1.1")).toThrow(InvalidValueError) expect(() => u64().default(-1)).toThrow(/negative/) // A value of another type is not silently converted. expect(() => dec().default(i32(4) as never)).toThrow(/i32 value, not a dec/) }) it("reject a default that is missing or undefined", () => { expect(() => (str() as unknown as { default(): unknown }).default()).toThrow( SchemaDefinitionError, ) expect(() => str().default(undefined as unknown as string)).toThrow(InvalidValueError) }) it("never index bytes, even past the type system", () => { const column = bytes() as unknown as { indexed(): unknown } expect(() => column.indexed()).toThrow(/bytes column cannot be indexed/) }) }) describe("defineSchema", () => { it("keeps the columns, in the order they were declared", () => { const Posts = defineSchema({ title: str(), score: dec().indexed(), body: bytes() }) expect(isSchema(Posts)).toBe(true) expect(Object.keys(Posts.columns)).toEqual(["title", "score", "body"]) expect(isColumn(Posts.columns.score)).toBe(true) expect(Object.isFrozen(Posts.columns)).toBe(true) }) it("rejects a value where a column belongs, suggesting the default it may have meant", () => { const define = () => defineSchema({ score: dec("4.1") } as never) expect(define).toThrow(SchemaDefinitionError) expect(define).toThrow(/did you mean dec\(\)\.default\("4\.1"\)/i) expect(() => defineSchema({ n: u64(5n) } as never)).toThrow(/u64\(\)\.default\(5n\)/) }) it("rejects anything else that is not a column", () => { expect(() => defineSchema({ title: "str" } as never)).toThrow(/not a column/) expect(() => defineSchema({ title: str } as never)).toThrow(/not a column/) expect(() => defineSchema([] as never)).toThrow(SchemaDefinitionError) }) it("holds column names to the attribute-name grammar", () => { expect(() => defineSchema({ "1st": str() })).toThrow(InvalidAttributeNameError) expect(() => defineSchema({ $owner: addr() })).toThrow(/reserved for system attributes/) }) it("lets a column take a name an entity field has without the $, which a row keeps apart", () => { for (const name of ["owner", "creator", "createdAt", "expiresAt", "attributes"]) { expect(() => defineSchema({ [name]: str() })).not.toThrow() } }) it("caps the number of columns at what one operation can carry", () => { const columns = (count: number) => Object.fromEntries(Array.from({ length: count }, (_, i) => [`c${i}`, i32()])) expect(() => defineSchema(columns(MAX_ATTRIBUTES))).not.toThrow() expect(() => defineSchema(columns(MAX_ATTRIBUTES + 1))).toThrow(TooManyAttributesError) }) it("accepts a schema with no columns", () => { expect(defineSchema({}).columns).toEqual({}) }) }) describe("named schemas", () => { it("carry their name, and the str tag their entities are marked with", () => { const Posts = defineSchema("posts", { title: str() }) expect(Posts.name).toBe("posts") expect(Posts.tag).toEqual(str("posts")) expect(Object.keys(Posts.columns)).toEqual(["title"]) }) it("leave an unnamed schema untagged", () => { const Posts = defineSchema({ title: str() }) expect(Posts.name).toBeUndefined() expect(Posts.tag).toBeUndefined() }) it("reject a name that is not a valid str", () => { expect(() => defineSchema("", { title: str() })).toThrow(/non-empty string/) expect(() => defineSchema("a\ud800", { title: str() })).toThrow(SchemaDefinitionError) expect(() => defineSchema("x".repeat(129), { title: str() })).toThrow(/not a valid str tag/) }) it("reserve the tag's attribute name, named or not", () => { expect(() => defineSchema({ [SCHEMA_ATTRIBUTE]: str() })).toThrow( /the attribute a named schema tags its entities with/, ) expect(() => defineSchema("posts", { [SCHEMA_ATTRIBUTE]: str() })).toThrow( SchemaDefinitionError, ) }) it("count the tag against the attribute cap", () => { const columns = Object.fromEntries( Array.from({ length: MAX_ATTRIBUTES }, (_, i) => [`c${i}`, i32()]), ) expect(() => defineSchema(columns)).not.toThrow() expect(() => defineSchema("full", columns)).toThrow(/at most 32 attributes, got 33/) expect(() => defineSchema("full", columns)).toThrow(/tag counts as one of them/) }) }) describe("recognising schemas and columns", () => { it("goes by a registry brand, so another copy of the SDK recognises them too", () => { // What a schema built by a second copy of this module — the CJS build next to the ESM one — // looks like from here: same shape, same registry symbol, a different class. const foreign = Object.freeze( Object.defineProperty( { name: undefined, tag: undefined, columns: {} }, Symbol.for("@arkiv-network/sdk/schema"), { value: true }, ), ) expect(isSchema(foreign)).toBe(true) expect(isSchema(defineSchema({ title: str() }))).toBe(true) expect( isColumn(Object.defineProperty({}, Symbol.for("@arkiv-network/sdk/column"), { value: true })), ).toBe(true) }) it("does not take a look-alike without the brand", () => { expect(isSchema({ columns: {} })).toBe(false) expect(isColumn({ type: "str", isIndexed: false, isOptional: false })).toBe(false) }) })