UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

325 lines (292 loc) • 11.2 kB
import { describe, expect, it } from "bun:test" import { addr, bytes, dec, defineSchema, InvalidAttributeNameError, i32, SchemaMismatchError, str, u256, } from "../schema" import { Entity } from "../types/entity" import { projectSelection, toRow, toRpcSelect } from "./selection" const EVERY_FIELD = { key: true, owner: true, creator: true, createdAt: true, updatedAt: true, expiresAt: true, creationFlags: true, attributeSchema: false, attributes: true, } describe("toRpcSelect", () => { it("selects every field and attribute for no argument or a star, but not the schema", () => { // The attribute schema describes an entity's shape rather than the entity, so it is only // returned when asked for by name. expect(toRpcSelect()).toEqual(EVERY_FIELD) expect(toRpcSelect("*")).toEqual(EVERY_FIELD) expect(toRpcSelect({ $key: true, $attributeSchema: true })).toMatchObject({ attributeSchema: true, }) }) it("sends every field, so what was asked for is legible in the request", () => { // The node defaults everything but `key` to off. Spelling out the false ones costs nothing and // pins the answer even if those defaults move. expect(toRpcSelect({ $owner: true })).toEqual({ key: false, owner: true, creator: false, createdAt: false, updatedAt: false, expiresAt: false, creationFlags: false, attributeSchema: false, attributes: false, }) }) it("asks for any name without a $ as an attribute, by name", () => { expect(toRpcSelect({ $key: true, title: true, body: true })).toMatchObject({ key: true, attributes: { title: true, body: true }, }) expect(toRpcSelect({ title: true, body: false })).toMatchObject({ attributes: { title: true } }) expect(() => toRpcSelect({ "not a name": true })).toThrow(InvalidAttributeNameError) }) it("reads a name an entity field has, written without the $, as an attribute", () => { // `owner` and `attributes` are ordinary attribute names; only `$owner` is the entity field. expect(toRpcSelect({ owner: true, attributes: true })).toMatchObject({ owner: false, attributes: { owner: true, attributes: true }, }) }) it("asks for every attribute with $attributes, and their types with $attributeSchema", () => { expect(toRpcSelect({ $key: true, $attributes: true, $attributeSchema: true })).toMatchObject({ key: true, attributes: true, attributeSchema: true, }) }) it("rejects a map for $attributes, pointing at selecting attributes by name", () => { expect(() => toRpcSelect({ $attributes: { title: true } } as never)).toThrow( /select each by name/, ) }) it("rejects a $ name that is not an entity field", () => { expect(() => toRpcSelect({ $title: true })).toThrow(/"\$title", which is not an entity field/) // The fields are written with the $, so the bare RPC spelling is not one of them either. expect(() => toRpcSelect({ $schema: true })).toThrow(/not an entity field/) }) it("rejects a selection that asks for nothing", () => { expect(() => toRpcSelect({})).toThrow(/at least one field/) // The type system permits an explicit `false`; the node would answer it with empty rows. expect(() => toRpcSelect({ $key: false })).toThrow(/at least one field/) expect(() => toRpcSelect({ $key: false, title: false })).toThrow(/at least one field/) }) it("counts an attribute alone as a selection", () => { expect(() => toRpcSelect({ title: true })).not.toThrow() }) it("treats a field set to undefined as not selected", () => { expect(toRpcSelect({ $key: true, $owner: undefined })).toMatchObject({ key: true, owner: false, }) expect(() => toRpcSelect({ $key: undefined })).toThrow(/at least one field/) }) }) const Posts = defineSchema({ author: addr().indexed(), score: dec().indexed(), title: str(), body: bytes(), thumbnail: bytes().optional(), }) const KEY = `0x${"ab".repeat(32)}` as const const AUTHOR = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" describe("projectSelection", () => { it("asks for the selected entity fields, and the selected columns by name", () => { const projection = projectSelection( { $key: true, title: true, body: true, score: false }, Posts, ) expect(projection.select).toEqual({ key: true, owner: false, creator: false, createdAt: false, updatedAt: false, expiresAt: false, creationFlags: false, attributeSchema: false, attributes: { title: true, body: true }, }) expect(projection.fields).toEqual(["$key"]) expect(projection.columns.map(([name]) => name)).toEqual(["title", "body"]) }) it("selects every entity field and every column for nothing or a star", () => { for (const selection of [undefined, "*"] as const) { const { select } = projectSelection(selection, Posts) expect(select).toMatchObject({ key: true, owner: true, creationFlags: true }) // Named, not `true`: attributes beyond the schema stay on the node. expect(select.attributes).toEqual({ author: true, score: true, title: true, body: true, thumbnail: true, }) } }) it("asks for no attributes when only entity fields are selected", () => { expect(projectSelection({ $owner: true }, Posts).select.attributes).toBe(false) }) it("rejects a name that is not a column", () => { expect(() => projectSelection({ $key: true, nope: true }, Posts)).toThrow( /"nope", which is not a column/, ) }) it("reads attributes as a whole the same way through a schema as without one", () => { const projection = projectSelection({ $key: true, title: true, $attributes: true }, Posts) expect(projection.select.attributes).toBe(true) const entity = new Entity({ key: KEY, attributes: { title: str("Hi"), extra: i32(1) } }) expect(toRow(entity, projection)).toEqual({ $key: KEY, title: str("Hi"), $attributes: { title: str("Hi"), extra: i32(1) }, }) }) it("rejects a selection that asks for nothing, or a map where a boolean belongs", () => { expect(() => projectSelection({ $key: false }, Posts)).toThrow(/at least one field/) expect(() => projectSelection({ title: { x: true } } as never, Posts)).toThrow(/true or false/) }) }) describe("toRow", () => { const projection = projectSelection( { $key: true, author: true, title: true, thumbnail: true }, Posts, ) it("lays out the selected fields and columns side by side", () => { const row = toRow( new Entity({ key: KEY, owner: AUTHOR, attributes: { author: addr(AUTHOR), title: str("Hi"), thumbnail: bytes("0x01"), extra: i32(1), }, }), projection, ) expect(row).toEqual({ $key: KEY, author: addr(AUTHOR), title: str("Hi"), thumbnail: bytes("0x01"), }) }) it("keeps a column named like an entity field apart from the field", () => { const Owned = defineSchema({ owner: addr(), attributes: str() }) const row = toRow( new Entity({ key: KEY, owner: AUTHOR, attributes: { owner: addr(KEY.slice(0, 42)), attributes: str("mine") }, }), projectSelection({ $owner: true, owner: true, attributes: true }, Owned), ) expect(row).toEqual({ $owner: AUTHOR, owner: addr(KEY.slice(0, 42)), attributes: str("mine"), }) }) it("gives an optional column the entity lacks as undefined", () => { const row = toRow( new Entity({ key: KEY, attributes: { author: addr(AUTHOR), title: str("Hi") } }), projection, ) expect(row).toHaveProperty("thumbnail", undefined) }) it("rejects an entity missing a required column, naming the entity", () => { const decode = () => toRow(new Entity({ key: KEY, attributes: { author: addr(AUTHOR) } }), projection) expect(decode).toThrow(SchemaMismatchError) expect(decode).toThrow(new RegExp(`"title".*entity ${KEY} has no value`)) }) it("rejects an entity whose column holds another type than the schema declares", () => { const decode = () => toRow( new Entity({ key: KEY, attributes: { author: addr(AUTHOR), title: i32(1) } }), projection, ) expect(decode).toThrow(/holds a i32, not the str the schema declares/) }) it("reads a column named after an Object.prototype key as absent when the entity lacks it", () => { const Odd = defineSchema({ constructor: str().optional(), toString: str().optional() }) const row = toRow(new Entity({ key: KEY, attributes: {} }), projectSelection("*", Odd)) expect(row).toHaveProperty("constructor", undefined) expect(row).toHaveProperty("toString", undefined) }) }) describe("a named schema", () => { const Tagged = defineSchema("posts", { title: str(), score: dec().optional() }) const projection = projectSelection({ $key: true, title: true }, Tagged) it("fetches the tag alongside the columns, even when no column is selected", () => { expect(projection.select.attributes).toEqual({ title: true, "arkiv.schema": true }) expect(projectSelection({ $key: true }, Tagged).select.attributes).toEqual({ "arkiv.schema": true, }) }) it("checks the tag, but leaves it out of the row", () => { const row = toRow( new Entity({ key: KEY, attributes: { title: str("Hi"), "arkiv.schema": str("posts") } }), projection, ) expect(row).toEqual({ $key: KEY, title: str("Hi") }) }) it("rejects an entity tagged by another schema, or not tagged at all", () => { const other = () => toRow( new Entity({ key: KEY, attributes: { title: str("Hi"), "arkiv.schema": str("comments") } }), projection, ) expect(other).toThrow(SchemaMismatchError) expect(other).toThrow(/tagged "comments", not "posts"/) expect(() => toRow(new Entity({ key: KEY, attributes: { title: str("Hi") } }), projection), ).toThrow(/not tagged with a schema/) }) it("leaves the tag out of the attributes map and the attribute schema too", () => { const whole = projectSelection( { $key: true, $attributes: true, $attributeSchema: true }, Tagged, ) const entity = new Entity({ key: KEY, attributes: { title: str("Hi"), "arkiv.schema": str("posts") }, attributeSchema: { title: { type: "str", indexed: false }, "arkiv.schema": { type: "str", indexed: true }, }, }) expect(toRow(entity, whole)).toEqual({ $key: KEY, $attributes: { title: str("Hi") }, $attributeSchema: { title: { type: "str", indexed: false } }, }) }) it("rejects an entity whose tag is not a str, whatever its type", () => { const decode = () => toRow( new Entity({ key: KEY, attributes: { title: str("Hi"), "arkiv.schema": u256(5n) } }), projection, ) expect(decode).toThrow(SchemaMismatchError) expect(decode).toThrow(/tagged with a value of type u256, not "posts"/) }) })