@arkiv-network/sdk
Version:
TypeScript client library for Arkiv's blockchains interactions
1,581 lines • 62.2 kB
JavaScript
const require_schema = require('./schema-AVHCdblQ.cjs');
const require_errors = require('./errors-CVkdB3AT.cjs');
const require_entity = require('./entity-BlzyJ-D7.cjs');
let viem = require("viem");
//#region src/utils/logger.ts
const ROOT_NAMESPACE = "arkiv";
function getDebugEnv() {
if (typeof process !== "undefined") {
const debugEnv = process?.env?.DEBUG;
if (debugEnv) return debugEnv;
}
if (typeof globalThis !== "undefined" && "localStorage" in globalThis) try {
return globalThis.localStorage?.getItem("DEBUG") || "";
} catch {}
return "";
}
function matchesPattern(namespace, pattern) {
if (pattern === namespace) return true;
if (pattern.endsWith("*")) {
const prefix = pattern.slice(0, -1);
return namespace.startsWith(prefix);
}
return false;
}
function isEnabled(namespace) {
const debugEnv = getDebugEnv();
if (!debugEnv) return false;
const patterns = debugEnv.split(/[\s,]+/).filter((p) => p.length > 0);
if (patterns.length === 0) return false;
let enabled = false;
for (const pattern of patterns) if (pattern.startsWith("-")) {
if (matchesPattern(namespace, pattern.slice(1))) return false;
} else if (matchesPattern(namespace, pattern)) enabled = true;
return enabled;
}
const COLOR_PALETTE = [
"#0000FF",
"#008000",
"#FF0000",
"#FF00FF",
"#FFA500",
"#800080",
"#00CED1",
"#FF1493",
"#32CD32",
"#1E90FF",
"#FF4500",
"#9370DB",
"#00FA9A",
"#FFD700",
"#DC143C",
"#00BFFF",
"#FF69B4",
"#20B2AA",
"#DA70D6",
"#4682B4"
];
const isNode = typeof process !== "undefined" && process.versions?.node != null;
const ANSI_COLORS = [
"\x1B[94m",
"\x1B[92m",
"\x1B[91m",
"\x1B[95m",
"\x1B[93m",
"\x1B[96m",
"\x1B[35m",
"\x1B[36m",
"\x1B[32m",
"\x1B[34m",
"\x1B[33m",
"\x1B[31m",
"\x1B[90m",
"\x1B[37m",
"\x1B[1m"
];
const ANSI_RESET = "\x1B[0m";
const ANSI_BOLD = "\x1B[1m";
const namespaceColors = /* @__PURE__ */ new Map();
function getColorForNamespace(namespace) {
const cachedColor = namespaceColors.get(namespace);
if (cachedColor) return cachedColor;
let hash = 0;
for (let i = 0; i < namespace.length; i++) {
const char = namespace.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
const colorIndex = Math.abs(hash) % COLOR_PALETTE.length;
const color = {
css: COLOR_PALETTE[colorIndex] ?? "inherit",
ansi: ANSI_COLORS[colorIndex % ANSI_COLORS.length] ?? ""
};
namespaceColors.set(namespace, color);
return color;
}
function createDebugLogger(namespace) {
const colors = getColorForNamespace(namespace);
return (...args) => {
if (isEnabled(namespace)) if (isNode) {
const coloredNamespace = `${ANSI_BOLD}${colors.ansi}${namespace}${ANSI_RESET}`;
console.log(coloredNamespace, ...args);
} else {
const style1 = `color: ${colors.css}; font-weight: bold`;
console.log(`%c${namespace}%c`, style1, "color: inherit; font-weight: normal", ...args);
}
};
}
const getLogger = (scope) => {
return createDebugLogger(`${ROOT_NAMESPACE}:${scope}`);
};
//#endregion
//#region src/query/selection.ts
/**
* The entity fields a row can carry. Each is `$`-prefixed, like the system attributes `where`
* filters on: an attribute name cannot start with `$`, so no attribute can collide with one.
*/
const SYSTEM_FIELDS = [
"$key",
"$owner",
"$creator",
"$createdAt",
"$updatedAt",
"$expiresAt",
"$creationFlags"
];
/** The selection keys with a fixed meaning, in the order they are written into the RPC `select`. */
const SELECTION_KEYS = [
...SYSTEM_FIELDS,
"$attributeSchema",
"$attributes"
];
/**
* Converts a selection into the RPC `select` object — the same request {@link projectSelection}
* makes without a schema.
*
* Every field is opt-in — anything not explicitly selected is sent as `false`, including the key.
* Sending the full object rather than omitting the `false`s keeps what the SDK asked for legible in
* a request log, and pins the answer even if a node's defaults change.
*
* `undefined` or `"*"` selects everything.
*
* @throws If the selection asks for nothing, or names an attribute outside the name grammar.
*
* @example
* toRpcSelect() // everything
* toRpcSelect({ $key: true }) // only the key
* toRpcSelect({ $key: true, projectId: true }) // the key and one attribute
*/
function toRpcSelect(selection) {
return projectSelection(selection).select;
}
/**
* A frozen copy of a selection, so that editing the object passed to `select()` afterwards cannot
* change what the builder sends.
*/
function snapshotSelection(selection) {
if (typeof selection !== "object" || selection === null) return selection;
return Object.freeze({ ...selection });
}
/**
* Resolves a selection, through a schema or without one. A key that does not start with `$` names
* an attribute — through a schema, it must be a column.
*
* `undefined` or `"*"` selects every entity field and, through a schema, every column; without one,
* every attribute as the `$attributes` map. The `$attributeSchema` is only selected by name.
*
* Attributes are fetched by name where possible, so the ones a row does not carry stay on the node.
* A named schema's tag is fetched too, so each entity can be checked against it.
*
* @throws If the selection asks for nothing, names a `$` key that is not an entity field, names an
* attribute outside the name grammar, or — through a schema — names an attribute that is not a
* column.
*/
function projectSelection(selection, schema) {
const fields = [];
const columns = [];
let attributes = false;
let attributeSchema = false;
if (selection === void 0 || selection === "*") {
fields.push(...SYSTEM_FIELDS);
if (schema === void 0) attributes = true;
else columns.push(...Object.entries(schema.columns));
} else {
if (typeof selection !== "object" || selection === null || Array.isArray(selection)) throw new TypeError("select() takes \"*\" or an object of fields set to true");
for (const [name, wanted] of Object.entries(selection)) {
if (typeof wanted !== "boolean" && wanted !== void 0) throw new Error(name === "$attributes" ? "select() takes true or false for \"$attributes\", which reads every attribute. To read some of them, select each by name: select({ title: true, body: true })." : `select() takes true or false for "${name}", got ${typeof wanted}.`);
const fixed = SELECTION_KEYS.includes(name);
if (!fixed && name.startsWith("$")) throw new Error(`select() names "${name}", which is not an entity field. The entity fields are ${SYSTEM_FIELDS.join(", ")}, and $attributes and $attributeSchema read the attributes as a whole.`);
const column = fixed || schema === void 0 ? void 0 : require_schema.lookupColumn(schema, name);
if (!fixed) {
if (schema !== void 0 && column === void 0) throw new Error(`select() names "${name}", which is not a column of the schema. Its columns are ${require_schema.describeColumns(schema)}.`);
require_schema.validateAttributeName(name);
}
if (!wanted) continue;
if (name === "$attributes") attributes = true;
else if (name === "$attributeSchema") attributeSchema = true;
else if (fixed) fields.push(name);
else columns.push([name, column]);
}
if (fields.length === 0 && columns.length === 0 && !attributes && !attributeSchema) throw nothingSelected();
}
const select = {};
for (const field of SYSTEM_FIELDS) select[entityField(field)] = fields.includes(field);
select.attributeSchema = attributeSchema;
if (attributes) select.attributes = true;
else {
const names = new Set(columns.map(([name]) => name));
if (schema?.tag !== void 0) names.add(require_schema.SCHEMA_ATTRIBUTE);
select.attributes = names.size > 0 ? Object.fromEntries([...names].map((n) => [n, true])) : false;
}
return {
select,
fields,
columns,
attributes,
attributeSchema,
tag: schema?.tag
};
}
/**
* Lays an entity out as a row of `projection` — checking it against the schema, when there is one.
*
* @throws {SchemaMismatchError} If a named schema's entity is not tagged with its name, or the
* entity lacks a selected required column or holds one with a type other than the schema's — it
* was not written through this schema, or was patched since.
*/
function toRow(entity, projection) {
const subject = entity.key === void 0 ? "the entity" : `entity ${entity.key}`;
const { tag } = projection;
if (tag !== void 0) {
const found = attributeOf(entity, require_schema.SCHEMA_ATTRIBUTE);
if (found?.type !== "str" || found.value !== tag.value) throw new require_schema.SchemaMismatchError(require_schema.SCHEMA_ATTRIBUTE, `${subject} is ${describeTag(found)}, not ${JSON.stringify(tag.value)}, so it was not written through this schema`);
}
const row = {};
for (const field of projection.fields) row[field] = entity[entityField(field)];
for (const [name, column] of projection.columns) {
const value = attributeOf(entity, name);
if (column !== void 0) {
if (value === void 0) {
if (!column.isOptional) throw new require_schema.SchemaMismatchError(name, `${subject} has no value for this required ${column.type} column, so it does not match the schema. Declare the column .optional() if entities may leave it unset`);
} else if (value.type !== column.type) throw new require_schema.SchemaMismatchError(name, `${subject} holds a ${value.type}, not the ${column.type} the schema declares`);
}
row[name] = value;
}
if (projection.attributes) row.$attributes = withoutTag(entity.attributes ?? {}, tag);
if (projection.attributeSchema) row.$attributeSchema = withoutTag(entity.attributeSchema ?? {}, tag);
return row;
}
/** The {@link Entity} property, and RPC `select` key, a system field reads: its name without `$`. */
function entityField(field) {
return field.slice(1);
}
/**
* A copy of `record` without a named schema's tag: the tag is how a query finds the schema's
* entities, not one of their attributes, so a row never carries it — and a map read back from a
* row drops into a write without sending the reserved name.
*/
function withoutTag(record, tag) {
if (tag === void 0 || !Object.hasOwn(record, require_schema.SCHEMA_ATTRIBUTE)) return record;
const { [require_schema.SCHEMA_ATTRIBUTE]: _tag,...rest } = record;
return rest;
}
/**
* The entity's attribute `name`, or `undefined` if it has none. An own-property read: a column may
* be named `constructor` or `toString`, and an entity that leaves it unset must not find the
* inherited one instead.
*/
function attributeOf(entity, name) {
const { attributes } = entity;
return attributes !== void 0 && Object.hasOwn(attributes, name) ? attributes[name] : void 0;
}
/** How an entity's schema tag reads in a mismatch — whatever type another writer gave it. */
function describeTag(found) {
if (found === void 0) return "not tagged with a schema";
if (found.type === "str") return `tagged ${JSON.stringify(found.value)}`;
return `tagged with a value of type ${found.type}`;
}
function nothingSelected() {
return /* @__PURE__ */ new Error("select() requires at least one field to be selected. Pass select() or select(\"*\") to select everything.");
}
//#endregion
//#region src/entity/flags.ts
/** Bit positions in the `creationFlags` byte. Bits 2-7 are reserved and must be zero. */
const READONLY_BIT = 1;
const PERMISSIONLESS_EXTENSION_BIT = 2;
/**
* Packs creation flags into the `creationFlags` byte.
*
* @throws {InvalidCreationFlagsError} If a value is not a boolean.
*/
function encodeCreationFlags(flags = {}) {
for (const [name, value] of Object.entries(flags)) if (value !== void 0 && typeof value !== "boolean") throw new require_errors.InvalidCreationFlagsError(`"${name}" must be a boolean, got ${typeof value}`);
return (flags.readonly ? READONLY_BIT : 0) | (flags.permissionlessExtension ? PERMISSIONLESS_EXTENSION_BIT : 0);
}
/**
* Reads the `creationFlags` byte back.
*
* @param raw - The `creationFlags` byte.
* @throws {InvalidCreationFlagsError} Malformed input
*/
function decodeCreationFlags(raw) {
if (!Number.isInteger(raw) || raw < 0 || raw > 255) throw new require_errors.InvalidCreationFlagsError(`${raw} is not a byte`);
return {
readonly: (raw & READONLY_BIT) !== 0,
permissionlessExtension: (raw & PERMISSIONLESS_EXTENSION_BIT) !== 0,
raw
};
}
//#endregion
//#region src/utils/entities.ts
const logger$1 = getLogger("utils:entities");
/**
* Turns the attribute array a query returns into a name-keyed map of typed values.
*
* Attribute names are unique per entity, so the map loses nothing and gives callers
* `entity.attributes.level.value` instead of a linear search.
*/
function decodeAttributes(rpcAttributes) {
const attributes = {};
for (const { name, type, value } of rpcAttributes) attributes[name] = require_schema.decodeRpcValue(type, value);
return attributes;
}
/** The same, for the values-free `attributeSchema` projection. */
function decodeSchema(entries) {
const schema = {};
for (const { name, type, indexed } of entries) schema[name] = {
type: require_schema.asTypeTag(type),
indexed: typeof indexed === "boolean" ? indexed : void 0
};
return schema;
}
/**
* Builds an {@link Entity} from a query result row.
*
* A field the query did not select is absent from the response and stays `undefined` here — the
* decoder never substitutes an empty value for one that was not asked for.
*/
function entityFromRpcResult(rpcEntity) {
logger$1("entityFromRpcResult %o", rpcEntity);
return new require_entity.Entity({
key: rpcEntity.key,
owner: toAddress(rpcEntity.owner),
creator: toAddress(rpcEntity.creator),
createdAt: toBlock(rpcEntity.createdAt),
updatedAt: toBlock(rpcEntity.updatedAt),
expiresAt: toBlock(rpcEntity.expiresAt),
creationFlags: decodeFlags(rpcEntity.creationFlags),
attributeSchema: rpcEntity.attributeSchema !== void 0 ? decodeSchema(rpcEntity.attributeSchema) : void 0,
attributes: rpcEntity.attributes !== void 0 ? decodeAttributes(rpcEntity.attributes) : void 0
});
}
function toBlock(value) {
return value === void 0 ? void 0 : BigInt(value);
}
function toAddress(value) {
if (value === void 0) return void 0;
if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
logger$1("unreadable address %o", value);
return value;
}
return (0, viem.checksumAddress)(value);
}
/**
* Resolves the `creationFlags` projection, preferring the byte.
*/
function decodeFlags(value) {
if (value === void 0) return void 0;
const raw = toFlagsByte(typeof value === "number" ? value : value.raw);
if (raw !== void 0) return decodeCreationFlags(raw);
if (typeof value === "object" && (value.readonly ?? value.permissionlessExtension) !== void 0) return decodeCreationFlags(encodeCreationFlags({
readonly: value.readonly ?? false,
permissionlessExtension: value.permissionlessExtension ?? false
}));
logger$1("unreadable creationFlags %o", value);
}
/**
* A flags byte in whichever spelling arrived: a JSON number, or the `0x` QUANTITY that every other
* numeric field on {@link RpcEntity} already uses. `undefined` for anything that is not a byte.
*/
function toFlagsByte(value) {
const parsed = typeof value === "number" ? value : typeof value === "string" && /^(0x[0-9a-fA-F]+|\d+)$/.test(value) ? Number(value) : NaN;
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 255 ? parsed : void 0;
}
//#endregion
//#region src/query/errors.ts
/**
* 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 |
*/
var UnsupportedOperatorError = class extends Error {
/** The attribute the predicate names. */
attributeName;
/** The operator, as it would have been written in the query. */
operator;
/** The type of the value it was applied to. */
tag;
constructor(attributeName, operator, tag, hint) {
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.
*/
var InvalidPredicateError = class extends Error {
/** The attribute the predicate names, where there is one. */
attributeName;
constructor(message, attributeName) {
super(message);
this.name = "InvalidPredicateError";
this.attributeName = attributeName;
}
};
/** The JSON-RPC error codes `arkiv_query` answers with, and what each one means. */
const QUERY_ERROR_KINDS = {
[-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 }
* }
* }
*/
var QueryError = class extends Error {
/** Which failure this is. */
kind;
/** The JSON-RPC error code. */
code;
/** The machine-readable detail the node sent, if any — e.g. `{ position, expected, got }`. */
data;
/** The query string that was rejected. */
query;
constructor(kind, code, query, data, detail) {
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.
*/
function asQueryError(error, query) {
let code;
let data;
let detail;
let current = error;
for (let depth = 0; current != null && depth < 8; depth++) {
const link = current;
if (code === void 0 && typeof link.code === "number") code = link.code;
if (data === void 0 && link.data !== void 0) data = link.data;
if (detail === void 0 && typeof link.details === "string" && link.details !== "") detail = link.details;
current = link.cause;
}
if (detail === void 0) {
const message = data?.message;
if (typeof message === "string" && message !== "") detail = message;
}
const kind = code === void 0 ? void 0 : QUERY_ERROR_KINDS[code];
if (kind === void 0 || code === void 0) return void 0;
return new QueryError(kind, code, query, data, detail);
}
//#endregion
//#region src/query/engine.ts
const logger = getLogger("query:engine");
const MAX_LIMIT = 100;
/**
* Runs one query and decodes the page it returns.
*
* @throws If `limit` is not an integer from 1 to {@link MAX_LIMIT}, or `offset` is not a
* non-negative integer.
* @throws {QueryError} If the node rejects the query.
*/
async function runQuery(client, request) {
const { query, select, limit, offset, count } = request;
if (limit !== void 0) {
if (!Number.isInteger(limit) || limit < 1) throw new Error(`limit must be a positive integer, got ${limit}.`);
if (limit > MAX_LIMIT) throw new Error(`limit ${limit} exceeds the node maximum of ${MAX_LIMIT}. Page through the results with an offset instead.`);
}
if (offset !== void 0 && (!Number.isSafeInteger(offset) || offset < 0)) throw new Error(`offset must be a non-negative integer, got ${offset}.`);
const queryOptions = {
select: count ? {
...select,
count: true
} : select,
...limit !== void 0 && { limit: (0, viem.numberToHex)(limit) },
...offset !== void 0 && { offset: (0, viem.numberToHex)(offset) }
};
logger("arkiv_query %s %o", query, queryOptions);
const result = await client.request({
method: "arkiv_query",
params: [query, queryOptions]
}).catch((error) => {
throw asQueryError(error, query) ?? error;
});
logger("Raw result from query %o", result);
return {
entities: result.data.map((rpcEntity) => entityFromRpcResult(rpcEntity)),
blockNumber: BigInt(result.blockNumber),
count: result.count === void 0 ? void 0 : BigInt(result.count)
};
}
//#endregion
//#region src/query/expression.ts
/**
* The system attributes a query may filter on, and the type each one carries.
*
* Every other system attribute (`$createdAt`, `$updatedAt`, `$creationFlags`) is returned in
* query results — selected and read back as `createdAt`, `updatedAt` and `creationFlags` — but is
* not indexed, so it cannot appear in a predicate.
*/
const QUERYABLE_SYSTEM_ATTRIBUTES = {
$key: "key",
$owner: "addr",
$creator: "addr",
$expiresAt: "u64"
};
/** System attributes a query can ask for but not filter on — they carry no index. */
const RESULT_ONLY_SYSTEM_ATTRIBUTES = [
"$createdAt",
"$updatedAt",
"$creationFlags"
];
/**
* Checks an attribute reference, returning its fixed type when it names a system attribute.
*
* User attribute names go through the same grammar the write path enforces, so a name that could
* never have been written is rejected here rather than matching nothing.
*
* @param use - What the reference is for, `"filter"` or `"sort"`, which the error messages name.
*/
function resolveReference(name, use = "filter") {
if (typeof name !== "string" || !name.startsWith("$")) {
require_schema.validateAttributeName(name);
return;
}
const system = QUERYABLE_SYSTEM_ATTRIBUTES[name];
if (system !== void 0) return system;
if (RESULT_ONLY_SYSTEM_ATTRIBUTES.includes(name)) {
const field = name.slice(1);
throw new InvalidPredicateError(`"${name}" is returned in query results but ${use === "filter" ? "carries no index, so it cannot be filtered on" : "cannot be sorted on"}. Select it as \`${field}\` and ${use} the results in JavaScript instead.`, name);
}
throw new InvalidPredicateError(`"${name}" is not a system attribute. The queryable ones are ${Object.keys(QUERYABLE_SYSTEM_ATTRIBUTES).join(", ")}. "\$" is reserved for these, so a user attribute cannot be named this.`, name);
}
/**
* Whether a string names a type that can be indexed — every type but `bytes`.
*
* Takes a `string` rather than an `IndexableTypeTag` on purpose — the callers' static types
* already say as much, and this is the runtime check for the code that has no types.
*/
function isIndexableTag(tag) {
return Object.hasOwn(require_schema.TYPE_IDS, tag) && require_schema.isIndexable(tag);
}
/** The indexable type tags, listed for error messages: "bool, i32, u64, …". */
const INDEXABLE_TAGS = Object.keys(require_schema.TYPE_IDS).filter(require_schema.isIndexable).join(", ");
/** Freezes a node and gives it a (non-enumerable, so still `toEqual`-able) rendering. */
function node(value) {
Object.defineProperty(value, "toString", {
value: () => render(value),
enumerable: false
});
return Object.freeze(value);
}
/**
* Resolves a value a predicate compares `name` against, and checks it can be: not `bytes`, which is
* never indexed, and of the fixed type when `name` is a system attribute.
*/
function predicateValue(name, system, input) {
const value = require_schema.toValue(input, name);
if (value.type === "bytes") throw new InvalidPredicateError(`"${name}" is compared against a bytes value, and bytes are stored but never indexed, so they cannot be filtered on. Index a hash of them, as a hex str, instead.`, name);
if (system !== void 0 && value.type !== system) throw new InvalidPredicateError(`"${name}" is a ${system}, but the value is a ${value.type}. System attributes have a fixed type — write ${system}(...) around the value.`, name);
return value;
}
function comparison(name, operator, input) {
return node({
kind: "comparison",
name,
operator,
value: predicateValue(name, resolveReference(name), input)
});
}
/**
* Attribute equals value — set with this exact type, and holding this value.
*
* Bare booleans, numbers, bigints and strings take the same default types they take when written
* (`number` is an `i32`, `bigint` a `u256`, `string` a `str`); anything else names its type.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
* attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
*
* @example
* eq("status", str("open")) // status = str('open')
* eq("flagged", true) // flagged = true
* eq("$owner", addr(account)) // $owner = addr(0xd8dA…)
*/
function eq(name, value) {
return comparison(name, "=", value);
}
/**
* Attribute is greater than value, in index order — see {@link ComparisonOperator}.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
* attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
*
* @example
* gt("level", i32(10)) // level > i32(10)
* gt("$expiresAt", u64(1_200_000n)) // $expiresAt > u64(1200000)
*/
function gt(name, value) {
return comparison(name, ">", value);
}
/**
* Attribute is greater than or equal to value, in index order — see {@link ComparisonOperator}.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
* attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
*
* @example
* gte("score", dec("3.5")) // score >= dec(3.5)
*/
function gte(name, value) {
return comparison(name, ">=", value);
}
/**
* Attribute is less than value, in index order — see {@link ComparisonOperator}.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
* attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
*
* @example
* lt("level", i32(10)) // level < i32(10)
*/
function lt(name, value) {
return comparison(name, "<", value);
}
/**
* Attribute is less than or equal to value, in index order — see {@link ComparisonOperator}.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
* attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
*
* @example
* lte("score", dec("5")) // score <= dec(5)
*/
function lte(name, value) {
return comparison(name, "<=", value);
}
/**
* Attribute lies between two values, **inclusive** at both ends: it matches exactly what
* `and(gte(name, low), lte(name, high))` matches, in one predicate. Both bounds must have one type,
* and a `low` above `high` matches nothing.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the bounds have different types, the name starts with `$` but
* is not a queryable system attribute, a system attribute is compared against the wrong type, or a
* bound is `bytes`.
*
* @example
* between("score", dec("3.5"), dec("5")) // score BETWEEN dec(3.5) AND dec(5)
* between("$expiresAt", u64(100n), u64(200n))
*/
function between(name, low, high) {
const system = resolveReference(name);
const lowValue = predicateValue(name, system, low);
const highValue = predicateValue(name, system, high);
if (lowValue.type !== highValue.type) throw new InvalidPredicateError(`BETWEEN on "${name}" has a ${lowValue.type} and a ${highValue.type} bound. Both bounds must have one type, the type the attribute is compared as.`, name);
return node({
kind: "between",
name,
low: lowValue,
high: highValue
});
}
/**
* Attribute is a `str` beginning with this prefix.
*
* The match is on **raw UTF-8 bytes** with no normalization — it is the index encoding, so
* `startsWith("name", "é")` matches only the same byte sequence, not another normal form of it.
*
* @param prefix - The prefix, as a bare string or a `str` value.
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
* attribute.
* @throws {UnsupportedOperatorError} If given a value that is not a `str`, or a system attribute
* (none of which is a `str`).
*
* @example
* startsWith("desc", "ab") // desc STARTSWITH str('ab')
*/
function startsWith(name, prefix) {
const system = resolveReference(name);
const value = typeof prefix === "string" ? require_schema.str(prefix) : require_schema.toValue(prefix, name);
if (value.type !== "str") throw new UnsupportedOperatorError(name, "STARTSWITH", value.type, "Only str carries a prefix index.");
if (system !== void 0) throw new UnsupportedOperatorError(name, "STARTSWITH", system, "No queryable system attribute is a str.");
return node({
kind: "startsWith",
name,
value
});
}
/**
* The attribute is set, with any type.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If given a system attribute — every entity has all of them, so
* the test would be a constant — or a `$` name that is not one.
*
* @example
* exists("reviewedBy") // EXISTS(reviewedBy)
*/
function exists(name) {
if (resolveReference(name) !== void 0) throw new InvalidPredicateError(`"${name}" is a system attribute, so it is set on every entity and EXISTS(${name}) is always true.`, name);
return node({
kind: "exists",
name
});
}
/**
* The attribute is set with exactly this type, whatever its value.
*
* Useful when the same name is written with different types across entities — a comparison already
* asserts the type, so this is for asking about the type alone.
*
* @param tag - The type tag: `bool`, `i32`, `u64`, `u256`, `dec`, `str`, `addr` or `key` — any
* type but `bytes`, which is never indexed.
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the tag is not an indexable type, the name is a system
* attribute (whose type is fixed), or the name starts with `$` but is not a system attribute.
*
* @example
* hasType("age", "i32") // TYPEOF(age) = i32
*/
function hasType(name, tag) {
const system = resolveReference(name);
if (system !== void 0) throw new InvalidPredicateError(`"${name}" is always a ${system} — a system attribute has a fixed type, so TYPEOF(${name}) is a constant.`, name);
if (!isIndexableTag(tag)) throw new InvalidPredicateError(`"${tag}" is not an indexable attribute type. Use one of ${INDEXABLE_TAGS}.`, name);
return node({
kind: "typeOf",
name,
tag
});
}
function and(...expressions) {
return combine("and", expressions.flat());
}
function or(...expressions) {
return combine("or", expressions.flat());
}
function combine(kind, expressions) {
if (expressions.length === 0) throw new InvalidPredicateError(`${kind}() needs at least one expression. An empty ${kind === "and" ? "conjunction" : "disjunction"} has no query-language spelling — leave the ${kind}() out. A query still needs at least one filter, so to match broadly narrow by owner or creator, or read through a named schema, whose tag counts as one.`);
const only = expressions[0];
if (expressions.length === 1 && only !== void 0) return only;
return node(kind === "and" ? {
kind: "and",
expressions: Object.freeze(expressions)
} : {
kind: "or",
expressions: Object.freeze(expressions)
});
}
/**
* Checks every attribute an expression names against a schema — the runtime half of the check
* {@link SchemaRef} makes at compile time, for the callers the type system does not reach.
*
* @throws {InvalidPredicateError} If a predicate names something that is not a column of the
* schema, a column that is not indexed, or compares a column against a value of another type.
*/
function checkAgainstSchema(expression, schema) {
switch (expression.kind) {
case "and":
case "or":
for (const sub of expression.expressions) checkAgainstSchema(sub, schema);
return;
}
const { name } = expression;
if (name.startsWith("$")) return;
const column = sortableColumn(schema, name, "filter");
if (!column.isIndexed) throw new InvalidPredicateError(`"${name}" is not indexed, and only indexed attributes can be filtered on. Declare it ${column.type}().indexed() — entities written before that are indexed by their next patch of it — or select it and filter the results in JavaScript.`, name);
const columnType = column.type;
const asserted = expression.kind === "comparison" || expression.kind === "startsWith" ? expression.value.type : expression.kind === "between" ? expression.low.type : expression.kind === "typeOf" ? expression.tag : void 0;
if (asserted !== void 0 && asserted !== columnType) throw new InvalidPredicateError(`"${name}" is a ${columnType} column, but the predicate asserts a ${asserted}, so it could never match. Write ${columnType}(...) around the value.`, name);
}
/**
* The column a filter or sort through a schema names, which must be of any type but bytes. A sort
* reads it whether or not it is indexed; a filter also needs it indexed, which the caller checks.
*
* @param use - What the column is for, `"filter"` or `"sort"`, which the error messages name.
* @throws {InvalidPredicateError} If the name is not a column of the schema, or names a bytes one.
*/
function sortableColumn(schema, name, use) {
const column = require_schema.lookupColumn(schema, name);
if (column === void 0) throw new InvalidPredicateError(`"${name}" is not a column of the schema, so ${use === "filter" ? "a predicate on it can only match entities written some other way" : "it cannot be sorted on"}. Its columns are ${require_schema.describeColumns(schema)}.`, name);
if (column.type === "bytes") throw new InvalidPredicateError(use === "filter" ? `"${name}" is a bytes column, and bytes are stored but never indexed, so it cannot be filtered on. Index a hash of it, as a hex str, instead, or select it and filter the results in JavaScript.` : `"${name}" is a bytes column, and bytes cannot be sorted on. Select it and sort the results in JavaScript.`, name);
return {
type: column.type,
isIndexed: column.isIndexed
};
}
/**
* Binding strength, tightest last — `AND` binds tighter than `OR`. A leaf never needs parentheses,
* so it sits above them both.
*/
const PRECEDENCE = {
or: 1,
and: 2,
leaf: 3
};
function precedenceOf(expression) {
switch (expression.kind) {
case "or": return PRECEDENCE.or;
case "and": return PRECEDENCE.and;
default: return PRECEDENCE.leaf;
}
}
/** Renders a subexpression, parenthesising it only where the precedence would otherwise change it. */
function nested(expression, minimum) {
const rendered = render(expression);
return precedenceOf(expression) < minimum ? `(${rendered})` : rendered;
}
/**
* Renders an expression as the query string the node parses. Equivalent to `String(expression)`.
*
* Literals are always written tagged, even where the language would infer them: an untagged number
* defaults to `i32`, but spelling it `i32(10)` keeps the exact-type assertion visible in the query
* that actually went over the wire.
*/
function render(expression) {
switch (expression.kind) {
case "comparison": return `${expression.name} ${expression.operator} ${renderLiteral(expression.value)}`;
case "between": return `${expression.name} BETWEEN ${renderLiteral(expression.low)} AND ${renderLiteral(expression.high)}`;
case "startsWith": return `${expression.name} STARTSWITH ${renderLiteral(expression.value)}`;
case "exists": return `EXISTS(${expression.name})`;
case "typeOf": return `TYPEOF(${expression.name}) = ${expression.tag}`;
case "and": return expression.expressions.map((sub) => nested(sub, PRECEDENCE.and)).join(" AND ");
case "or": return expression.expressions.map((sub) => nested(sub, PRECEDENCE.or)).join(" OR ");
}
}
/** Renders a typed value as its query literal. `bool` is the one type written bare. */
function renderLiteral(value) {
switch (value.type) {
case "bool": return value.value ? "true" : "false";
case "i32": return `i32(${value.value})`;
case "u64": return `u64(${value.value})`;
case "u256": return `u256(${value.value})`;
case "dec": return `dec(${value.value})`;
case "key": return `key(${value.value})`;
case "addr": return `addr(${value.value})`;
case "str": return `str('${value.value.replaceAll("'", "''")}')`;
}
}
//#endregion
//#region src/query/order.ts
/** Whether a value is a key built by {@link asc} or {@link desc}. */
function isOrderTerm(value) {
return typeof value === "object" && value !== null && value.kind === "order";
}
/**
* Checks a key that did not come from {@link asc} or {@link desc} — parsed from a request, say —
* as they check theirs, and returns it as they would have built it.
*
* @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
* @throws {InvalidPredicateError} If the value is not a key, its direction is not `"asc"` or
* `"desc"`, or its name or type is one `asc` and `desc` reject.
*/
function checkedOrderTerm(value) {
if (!isOrderTerm(value)) throw new InvalidPredicateError("A sort key is an asc(...) or a desc(...).");
const { name, type, direction } = value;
return orderTerm(name, type, direction);
}
function orderTerm(name, type, direction) {
const system = resolveReference(name, "sort");
if (direction !== "asc" && direction !== "desc") throw new InvalidPredicateError(`A sort runs "asc" or "desc", not ${JSON.stringify(direction)}.`, name);
if (type !== void 0 && (typeof type !== "string" || !isIndexableTag(type))) throw new InvalidPredicateError(`"${name}" cannot be sorted as "${type}": a sort reads one of ${INDEXABLE_TAGS}.`, name);
if (system !== void 0 && type !== void 0 && type !== system) throw new InvalidPredicateError(`"${name}" is always a ${system}, so it cannot be sorted as a ${type}. Leave the type out — a system attribute's is fixed.`, name);
return Object.freeze({
kind: "order",
name,
type: type ?? system,
direction
});
}
function asc(name, type) {
return orderTerm(name, type, "asc");
}
function desc(name, type) {
return orderTerm(name, type, "desc");
}
/**
* Renders the `ORDER BY` clause the node parses, taking the key's type from the schema where the
* key leaves it out — the runtime half of {@link OrderCheck}, for the callers the type system does
* not reach.
*
* @throws {InvalidPredicateError} If the key has no type and there is no schema to give it one,
* or — through a schema — names a bytes column or something that is not a column, or asserts
* another type than the column's.
*/
function renderOrderBy(term, schema) {
return `ORDER BY ${term.name}:${resolveOrderType(term, schema)} ${term.direction.toUpperCase()}`;
}
function resolveOrderType(term, schema) {
const { name, type } = term;
if (name.startsWith("$")) {
if (type !== void 0) return type;
throw new InvalidPredicateError(`The key on "${name}" has no type, though a system attribute's is fixed. Build it with asc() or desc(), which give it one.`, name);
}
if (schema === void 0) {
if (type !== void 0) return type;
throw new InvalidPredicateError(`"${name}" needs its type to be sorted on: the node sorts it as one type, and without a schema there is no column to take it from. Write it, as in desc("${name}", "u256").`, name);
}
const columnType = sortableColumn(schema, name, "sort").type;
if (type !== void 0 && type !== columnType) throw new InvalidPredicateError(`"${name}" is a ${columnType} column, but the sort asserts a ${type}. Leave the type out to sort it as the column's.`, name);
return columnType;
}
//#endregion
//#region src/query/builderBase.ts
/**
* What every query builder shares: the client, the filters — `where`, `ownedBy`, `createdBy` and a
* named schema's tag — and the block to read at. `client.select()` and `client.count()` build on it,
* so the same filters always send the same query string, whichever of them runs it.
*
* Not constructed directly: create a builder with `client.select(...)` or `client.count()`.
*
* @typeParam TSchema - The schema named with `from()`, if any, which `where` is checked against.
*/
var QueryBuilderBase = class {
_client;
_schema;
_filters = [];
_ownedBy;
_createdBy;
constructor(client) {
this._client = client;
}
/**
* This builder's state in a new builder of the same class, re-typed by the caller. Every field is
* carried over, so one a subclass adds is not dropped by `from()`; the filter list, which is
* mutated in place, is copied rather than shared.
*
* A copy, not `this` re-typed: a builder typed for one schema must never be the same object as
* one typed for another, or `base.from(Posts)` and `base.from(Users)` would share their state.
*/
copy() {
const copy = Object.create(Object.getPrototypeOf(this));
return Object.assign(copy, this, { _filters: [...this._filters] });
}
/**
* Adds filters. Everything passed here, and across repeated calls, must hold.
*
* After `from()`, every attribute a filter names must be an indexed column of the schema,
* compared against a value of its type — anything else is a compile error.
*
* @param expressions - The expressions, as separate arguments or one array.
*
* @example
* builder.where(eq("category", "docs"))
* builder.where(gte("level", i32(10)), lt("level", i32(20)))
* builder.where(or(eq("status", "open"), exists("reviewedBy")))
*/
where(...expressions) {
this._filters.push(...expressions.flat());
return this;
}
/**
* Restricts the query to entities this account owns — shorthand for
* `where(eq("$owner", addr(owner)))`. Calling it again replaces the filter.
*
* @example
* builder.ownedBy("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
*/
ownedBy(owner) {
this._ownedBy = eq("$owner", require_schema.addr(owner));
return this;
}
/**
* Restricts the query to entities this account created — shorthand for
* `where(eq("$creator", addr(creator)))`. Calling it again replaces the filter.
*
* The creator never changes; the owner does, so these differ after a transfer.
*/
createdBy(creator) {
this._createdBy = eq("$creator", require_schema.addr(creator));
return this;
}
/**
* The query string this builder will send — the same text the node parses: its filters,
* `ownedBy` and `createdBy`, and a named schema's tag, ANDed.
*
* @throws {InvalidPredicateError} If no filter has been added, or — after `from()` — a filter
* does not fit the schema. A named schema's own tag filter counts as a filter.
*/
toString() {
const filters = [...this._filters];
if (this._ownedBy) filters.push(this._ownedBy);
if (this._createdBy) filters.push(this._createdBy);
const schema = this._schema;
if (schema !== void 0) {
for (const filter of filters) checkAgainstSchema(filter, schema);
if (schema.tag !== void 0) filters.unshift(eq(require_schema.SCHEMA_ATTRIBUTE, schema.tag));
}
if (filters.length === 0) throw new InvalidPredicateError("A query needs at least one filter — the language has no spelling for 'match every entity'. Add a where(...) predicate, or narrow by ownedBy(...) / createdBy(...).");
return and(filters).toString();
}
};
//#endregion
//#region src/query/countBuilder.ts
/**
* Builds and runs a count: how many entities a query matches, without fetching any of them.
*
* It filters exactly as `client.select()` does — {@link from}, `where`, `ownedBy` and `createdBy`
* are the same methods, from the same base — but has nothing to select and no pages, so
* {@link fetch} resolves to a `bigint`. Create one with `client.count()`.
*
* To show the total next to a page of rows, use `select(...).withCount()` instead, which counts in
* the same request.
*
* @typeParam TSchema - The schema named with {@link from}, if any.
*
* @example
* const total = await client.count().from(Posts).ownedBy(account.address).fetch()
*/
var CountQueryBuilder = class extends QueryBuilderBase {
/**
* Counts through a schema: `where` is checked against its indexed columns, and a named schema
* counts only entities tagged with its name — so it needs no other filter.
*
* Returns a new builder, carrying whatever this one already holds; this one is left as it was.
*
* @throws {SchemaDefinitionError} If `schema` was not built with `defineSchema`.
*
* @example
* client.count().from(Posts) // every post
*/
from(schema) {
if (!require_schema.isSchema(schema)) throw new require_schema.SchemaDefinitionError("from() takes a schema built with defineSchema(...)");
const typed = this.copy();
typed._schema = schema;
return typed;
}
/**
* Runs the count.
*
* Counts every entity the filters match at the block, whether or not it fits the schema — what
* `select().from(schema)` leaves out of its pages is still counted.
*
* @throws {InvalidPredicateError} If no filter has been added, or a filter does not fit the
* schema.
* @throws {QueryError} If the node rejects the query.
* @throws If the node returns no count — it predates the count projection.
*/
async fetch() {
const response = await runQuery(this._client, {
query: this.toString(),
select: {},
count: true
});
if (response.count === void 0) throw new Error("The node returned no count. It may predate the count projection.");
return response.count;
}
};
//#endregion
//#region src/query/queryResult.ts
/**
* One page of query results.
*
* A page is one request: to read further, run the query again with an `offset()` past this page.
*
* @typeParam TEntity - The shape of each row, inferred from the selection by `client.select()`.
* @typeParam TCount - `bigint` when the query was built with `withCount()`, and `undefined` when it
* was not.
*
* @example
* const page = await client
* .select({ $key: true })
* .where(eq("category", "docs"))
* .orderBy(asc("$key"))
* .limit(20)
* .offset(40)
* .fetch()
*/
var QueryResult = class {
/** The rows on this page — flat, as the selection lays them out. */
entities;
/**
* The block this page was read at. Every request reads the latest block, so two pages of one
* query can come from different blocks.
*/
blockNumber;
/**
* How many matching entities this page left out because they did not fit the schema — always
* `0` without a schema, or with `.from(schema, { onMismatch: "throw" })`.
*
* The node counts them toward the limit and the offset, so `entities.length + skipped` is how
* many matches this page covered: add it to the offset to reach the next page. With an explicit
* `limit()`, a page that covered fewer than the limit was the last. An empty page is not the
* end on its own: every match on it may have been skipped.
*/
skipped;
/**
* How many entities the query matches in all, regardless of the limit and offset — a `bigint`
* when the query was built with `withCount()`, and `undefined` otherwise. It includes entities
* left out for not fitting the schema, which {@link skipped} reports per page.
*/
count;
constructor(entities, blockNumber, skipped, count) {
this.entities = entities;
this.blockNumber = blockNumber;
this.skipped = skipped;
this.count = count;
}
};
//#endregion
//#region src/query/queryBuilder.ts
/**
* Builds and runs a query.
*
* The selection is fixed when the builder is created and the filter is added by chaining, so the
* result type is known before a single predicate is written. Create one with `client.select(...)`
* rather than constructing it directly — that is what infers the result shape from the selection.
*
* The query returns flat rows: the selected entity fields and attributes side by side. Naming a
* schema with {@link from} changes what the selection means for nothing but the selected
* attributes — they come back typed and checked as the schema declares — and has `where` checked
* against it, so that filtering on a column the schema does not index, or comparing one against the
* wrong type, is a compile error. A named schema also restricts the query to entities tagged with
* its name, and leaves that tag out of the rows' `$attributes` and `$attributeSchema`.
*
* Every filter added with {@link where}, {@link ownedBy} and {@link createdBy} must hold; use
* {@link or} for alternatives inside one call. {@link orderBy} sorts what they match.
*
* @typeParam TSelection - The selection, inferred by `client.select()`.
* @typeParam TSchema - The schema named with {@link from}, if any.
*
* @example
* import { createPublicClient } from "@arkiv-network/sdk"
* import { dec } from "@arkiv-network/sdk/schema"
* import { rabdi } from "@arkiv-network/sdk/chains"
* import { and, desc, eq, gt } from "@arkiv-network/sdk/query"
* import { http } from "viem"
*
* const client = createPublicClient({ chain: rabdi({ accessKey: "..." }), transport: http() })
*
* const page = await client
* .select({ $key: true, author: true, title: true })
* .from(Posts)
* .where(and(eq("category", "docs"), gt("score", dec("4.0"))))
* .orderBy(desc("score"))
* .limit(20)
* .fetch()
*
* for (const row of page.entities) console.log(row.$key, row.title.value)
*/
var SelectQueryBuilder = class extends QueryBuilderBase {
_selection;
_projection;
_onMismatch = "skip";
_limit;
_offset;
_withCount = false;
_orderBy;
/**
* @throws If the selection is not `"*"` or an object, asks for nothing, or names an attribute
* outside the name grammar.
*/
constructor(client, selection) {
super(client);
this._selection = snapshotSelection(selection);
this._projection = projectSelection(this._selection);
}
/**
* Runs the query through a schema. The selection keeps its meaning and the rows their shape;
* each selected attribute becomes a column — typed as the schema declares, and checked against
* it — and `where` is checked against the schema's indexed columns.
*
* A named schema also filters: only entities tagged with its name match, so a query through it
* sees only entities written through it, and needs no other filter. The tag is how the query finds
* them rather than one of their attributes, so rows leave it out of `$attributes` and
* `$attributeSchema`. An unnamed schema types the query without filtering it — `where` alone
* decides which entities match.
*
* A matching entity that does not fit — a selected required column missing or of another type,
* or, for a named schema, a tag that does not match — is left out of the page, or fails the fetch
* with a {@link SchemaMismatchError} with `{ onMismatch: "throw" }`. Columns the selection leaves
* out are not checked.
*
* Returns a new builder, carrying whatever this one already holds; this one is left as it was.
*
* @param schema - The schema, built with `defineSchema`.
* @param options - How to treat entities that do not fit. {@link FromOptions}
* @throws {SchemaDefinitionError} If `schema` was not built with `defineSchema`.
* @throws If the selection names something that is neither an entity field nor a column.
*
* @example
* client.select({ $key: true, title: true }).from(Posts) // rows typed { $key; title }
* client.select().from(Posts) // every field and every column
* client.select().from(Posts, { onMismatch: "throw" }) // fail on entities that do not fit
*/
from(schema, options = {}) {
if (!require_schema.isSchema(schema)) throw new require_schema.SchemaDefinitionError("from() takes a schema built with defineSchema(...)");
const { onMismatch = "skip" } = options;
if (onMismatch !== "throw" && onMismatch !== "skip") throw new TypeError(`onMismatch must be "throw" or "skip", got ${String(onMismatch)}`);
const projection = projectSelection(this._selection, schema);
const typed = this.copy();
typed._schema = schema;
typed._projection = projection;
typed._onMismatch = onMismatch;
return typed;
}
orderBy(...keys) {
if (keys.length !== 1) throw new InvalidPredicateError("orderBy() takes one key, an asc(...) or desc(...).");
this._orderBy = checkedOrderTerm(keys[0]);
return this;
}
/**
* The query string this builder will send: the filters, as {@link QueryBuilderBase.toString}
* renders them, followed by the {@link orderBy} clause, if any.
*
* @throws {InvalidPredicateError} If no filter has been added, a filter or sort key does not fit
* the schema, or — without a schema — a sort key on a user attribute has no type.
*/
toString() {
const filters = super.toString();
if (this._orderBy === void 0) return filters;
return `${filters} ${renderOrderBy(this._orderBy, this._schema)}`;
}
/**
* Sets the page size, up to the node maximum of 100. Without it the node picks the page size.
*
* @example
* builder.limit(100)
*/
limit(limit) {
this._limit = limit;
return this;
}
/**
* Skips the first `offset` matches. With {@link limit}, this takes one page: page `n` of size
* `size` is `.limit(size).offset(n * size)`.
*
* Every request reads the latest block, so an entity created or deleted between two requests
* shifts the matches after it, and a row can repeat or be missed. Sort by a key that does not
* change, such as `asc("$key")`, so that pages line up at least while nothing is written; without
* {@link orderBy}, the order is the node's own. Through a schema, the offset counts the entities a
* page skips as well as its rows.
*
* To read every match exactly once while others write, page by key instead: sort by
* `asc("$key")` and filter on `gt("$key", key(lastKey))`, with `lastKey` the last `$key` of the
* previous page. Through a schema, use `{ onMismatch: "throw" }` for that: a page of skipped
* entities returns no rows, so no key to continue from.
*
* @example
* builder.orderBy(asc("$key")).limit(20).offset(40) // the third page of 20
*/
offset(offset) {
this._offset = offset;
return this;
}
/**
* Also counts every entity the query matches, regardless of the limit and offset. The page then
* carries the total as {@link QueryResult.count}, typed `bigint`, so a paginated list can show it
* without a second query. To count without fetching rows, use `client.count()` instead.
*
* The node counts what the filters match, at the block the page was read at. Through a schema,
* that includes entities the page leaves out for not fitting it. Later pages read later blocks,
* so the count can be out of date by the time they arrive.
*
* @throws If, when fetched, the node returns no count — it predates the count projection.
*
* Returns a new builder, carrying whatever this one already holds; this one is left as it was.
*
* @example
* const page = await builder.withCount().limit(20).fetch()
* page.count // every match, not just these 20
*/
withCount() {
const counted = this.copy();
counted._withCount = true;
return counted;
}
/**
* Runs the query and returns one page: up to {@link limit} matches, past the first
* {@link offset}.
*
* @throws {InvalidPredicateError} If no filter has been added, or a filter or sort key does not
* fit the schema.
* @throws If the {@link limit} is not an integer from 1 to 100, or the {@link offset} is not a
* non-negative integer.
* @throws {SchemaMismatchError} If, through a schema, a matching entity does not fit the selected
* columns or a named schema's tag, and `from()` was given `{ onMismatch: "throw" }`.
* @throws {QueryError} If the node rejects the query.
*
* @example
* const page = await client.select({ $key: true }).where(eq("category", "docs")).fetch()
* page.entities
*/
async fetch() {
const projection = this._projection;
const response = await runQuery(this._client, {
query: this.toString(),
select: projection.select,
limit: this._limit,
offset: this._offset,
count: this._withCount
});
if (this._withCount && response.count === void 0) throw new Error("The node returned no count. It may predate the count projection.");
const rows = [];
let skipped = 0;
for (const entity of response.entities) try {
rows.push(toRow(entity, projection));
} catch (error) {
if (this._onMismatch === "skip" && error instanceof require_schema.SchemaMismatchError) skipped++;
else throw error;
}
return new QueryResult(rows, response.blockNumber, skipped, response.count);
}
};
//#endregion
Object.defineProperty(exports, 'CountQueryBuilder', {
enumerable: true,
get: function () {
return CountQueryBuilder;
}
});
Object.defineProperty(exports, 'InvalidPredicateError', {
enumerable: true,
get: function () {
return InvalidPredicateError;
}
});
Object.defineProperty(exports, 'MAX_LIMIT', {
enumerable: true,
get: function () {
return MAX_LIMIT;
}
});
Object.defineProperty(exports, 'QueryError', {
enumerable: true,
get: function () {
return QueryError;
}
});
Object.defineProperty(exports, 'QueryResult', {
enumerable: true,
get: function () {
return QueryResult;
}
});
Object.defineProperty(exports, 'SYSTEM_FIELDS', {
enumerable: true,
get: function () {
return SYSTEM_FIELDS;
}
});
Object.defineProperty(exports, 'SelectQueryBuilder', {
enumerable: true,
get: function () {
return SelectQueryBuilder;
}
});
Object.defineProperty(exports, 'UnsupportedOperatorError', {
enumerable: true,
get: function () {
return UnsupportedOperatorError;
}
});
Object.defineProperty(exports, 'and', {
enumerable: true,
get: function () {
return and;
}
});
Object.defineProperty(exports, 'asc', {
enumerable: true,
get: function () {
return asc;
}
});
Object.defineProperty(exports, 'between', {
enumerable: true,
get: function () {
return between;
}
});
Object.defineProperty(exports, 'checkAgainstSchema', {
enumerable: true,
get: function () {
return checkAgainstSchema;
}
});
Object.defineProperty(exports, 'checkedOrderTerm', {
enumerable: true,
get: function () {
return checkedOrderTerm;
}
});
Object.defineProperty(exports, 'decodeCreationFlags', {
enumerable: true,
get: function () {
return decodeCreationFlags;
}
});
Object.defineProperty(exports, 'desc', {
enumerable: true,
get: function () {
return desc;
}
});
Object.defineProperty(exports, 'encodeCreationFlags', {
enumerable: true,
get: function () {
return encodeCreationFlags;
}
});
Object.defineProperty(exports, 'entityFromRpcResult', {
enumerable: true,
get: function () {
return entityFromRpcResult;
}
});
Object.defineProperty(exports, 'eq', {
enumerable: true,
get: function () {
return eq;
}
});
Object.defineProperty(exports, 'exists', {
enumerable: true,
get: function () {
return exists;
}
});
Object.defineProperty(exports, 'getLogger', {
enumerable: true,
get: function () {
return getLogger;
}
});
Object.defineProperty(exports, 'gt', {
enumerable: true,
get: function () {
return gt;
}
});
Object.defineProperty(exports, 'gte', {
enumerable: true,
get: function () {
return gte;
}
});
Object.defineProperty(exports, 'hasType', {
enumerable: true,
get: function () {
return hasType;
}
});
Object.defineProperty(exports, 'lt', {
enumerable: true,
get: function () {
return lt;
}
});
Object.defineProperty(exports, 'lte', {
enumerable: true,
get: function () {
return lte;
}
});
Object.defineProperty(exports, 'or', {
enumerable: true,
get: function () {
return or;
}
});
Object.defineProperty(exports, 'projectSelection', {
enumerable: true,
get: function () {
return projectSelection;
}
});
Object.defineProperty(exports, 'render', {
enumerable: true,
get: function () {
return render;
}
});
Object.defineProperty(exports, 'renderOrderBy', {
enumerable: true,
get: function () {
return renderOrderBy;
}
});
Object.defineProperty(exports, 'runQuery', {
enumerable: true,
get: function () {
return runQuery;
}
});
Object.defineProperty(exports, 'startsWith', {
enumerable: true,
get: function () {
return startsWith;
}
});
Object.defineProperty(exports, 'toRow', {
enumerable: true,
get: function () {
return toRow;
}
});
Object.defineProperty(exports, 'toRpcSelect', {
enumerable: true,
get: function () {
return toRpcSelect;
}
});