UNPKG

@arkiv-network/sdk

Version:

TypeScript client library for Arkiv's blockchains interactions

165 lines (153 loc) • 6.67 kB
import type { TypeTag } from "../schema" /** * A predicate asked for an operator the query language does not define for that type. * * The operator × type matrix is part of the language, not a policy: a prefix test on a type other * than `str` is a **type error** at the node (a {@link QueryError} of kind `"type"`), not * an empty result. Catching it here turns a round trip and an RPC error code into a stack trace * pointing at the call site. * * | | `=` `<` `<=` `>` `>=` `BETWEEN` | `STARTSWITH` | * | --- | --- | --- | * | `str` | yes | yes | * | every other indexable type | yes | no | */ export class UnsupportedOperatorError extends Error { /** The attribute the predicate names. */ readonly attributeName: string /** The operator, as it would have been written in the query. */ readonly operator: string /** The type of the value it was applied to. */ readonly tag: TypeTag constructor(attributeName: string, operator: string, tag: TypeTag, hint?: string) { super( `The query language does not define ${operator} for a ${tag}, so "${attributeName} ` + `${operator} ..." cannot be expressed.${hint ? ` ${hint}` : ""}`, ) this.name = "UnsupportedOperatorError" this.attributeName = attributeName this.operator = operator this.tag = tag } } /** * A predicate or query could not be expressed in the query language for a reason other than its * operator. The main causes: * * - a `$` name that is not a queryable system attribute — one returned in results but never * indexed (`$createdAt`, `$updatedAt`, `$creationFlags`), or none at all; * - a system attribute compared against the wrong type, or tested with `exists()` / `hasType()`, * which on a system attribute would be constants; * - a comparison against a `bytes` value, which is never indexed; * - a `BETWEEN` whose bounds have different types; * - a `hasType()` tag that is not an indexable type; * - a combinator with nothing to combine; * - a query with no filter at all — a named schema's tag counts as one; * - through `.from(schema)`, a filter that does not fit the schema: a name that is not a column, a * column that is not indexed, or a value of another type than the column's; * - a sort key that cannot be sent: an `orderBy()` with no key or more than one, a key on a bytes * column or on something that is not a column of the schema, one with no type and no schema to * take it from, or one whose type is not the attribute's. */ export class InvalidPredicateError extends Error { /** The attribute the predicate names, where there is one. */ readonly attributeName: string | undefined constructor(message: string, attributeName?: string) { super(message) this.name = "InvalidPredicateError" this.attributeName = attributeName } } /** * What a node rejected a query for. Part of the frozen RPC surface, so it is safe to branch on. */ export type QueryErrorKind = /** The query string does not parse. `data` locates it: `{ position, expected, got }`. */ | "parse" /** A predicate is well-formed but ill-typed — `STARTSWITH` on a type other than `str`, `BETWEEN` bounds of different types, an unknown tag. */ | "type" /** A literal failed validation — an `i32` out of range, a bad EIP-55 checksum, more than 18 decimal places. */ | "literal" /** A node budget was exceeded — query length, predicate count, nesting depth, bitmap operations. */ | "limits" /** The JSON-RPC error codes `arkiv_query` answers with, and what each one means. */ const QUERY_ERROR_KINDS: Readonly<Record<number, QueryErrorKind>> = { [-32001]: "parse", [-32002]: "type", [-32003]: "literal", [-32004]: "limits", } /** * The node rejected a query. * * These codes are part of the frozen `arkiv_query` surface, so {@link kind} and {@link data} are * stable enough to branch on — which the underlying JSON-RPC error is not, because the codes * overlap Ethereum's own EIP-1474 numbering and get wrapped accordingly on the way up. * * @example * try { * await client.query("category = 'docs'") * } catch (error) { * if (error instanceof QueryError && error.kind === "parse") { * console.log(error.data) // { position, expected, got } * } * } */ export class QueryError extends Error { /** Which failure this is. */ readonly kind: QueryErrorKind /** The JSON-RPC error code. */ readonly code: number /** The machine-readable detail the node sent, if any — e.g. `{ position, expected, got }`. */ readonly data: unknown /** The query string that was rejected. */ readonly query: string constructor(kind: QueryErrorKind, code: number, query: string, data: unknown, detail?: string) { super(`Query rejected (${kind}, ${code})${detail ? `: ${detail}` : ""}. Query: ${query}`, { cause: data, }) this.name = "QueryError" this.kind = kind this.code = code this.data = data this.query = query } } /** * Recognises an `arkiv_query` failure in whatever the transport threw. * * The codes are dug out of the error chain rather than read off the top: viem maps this range onto * its own EIP-1474 error classes (`-32001` is its "resource not found"), so by the time an error * surfaces the Arkiv meaning is one or more `cause` links down. * * @returns The typed error, or `undefined` if this was not a query rejection. */ export function asQueryError(error: unknown, query: string): QueryError | undefined { let code: number | undefined let data: unknown let detail: string | undefined let current: unknown = error for (let depth = 0; current != null && depth < 8; depth++) { const link = current as { code?: unknown data?: unknown details?: unknown cause?: unknown } if (code === undefined && typeof link.code === "number") code = link.code if (data === undefined && link.data !== undefined) data = link.data // viem keeps the node's own message as `details`. Its `shortMessage` is the EIP-1474 label for // the code ("Requested resource not found." for -32001), which says nothing about the query. if (detail === undefined && typeof link.details === "string" && link.details !== "") { detail = link.details } current = link.cause } if (detail === undefined) { const message = (data as { message?: unknown } | undefined)?.message if (typeof message === "string" && message !== "") detail = message } const kind = code === undefined ? undefined : QUERY_ERROR_KINDS[code] if (kind === undefined || code === undefined) return undefined return new QueryError(kind, code, query, data, detail) }