@arkiv-network/sdk 0.8.1-dev.1 → 0.9.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -61
- package/dist/chains/index.d.ts +17 -17
- package/dist/changeOwnership-Cy9gfyjN.d.cts +1160 -0
- package/dist/changeOwnership-Cy9gfyjN.d.cts.map +1 -0
- package/dist/changeOwnership-gXf-lBqI.d.ts +1160 -0
- package/dist/changeOwnership-gXf-lBqI.d.ts.map +1 -0
- package/dist/entity/index.cjs +4 -10
- package/dist/entity/index.d.cts +3 -3
- package/dist/entity/index.d.ts +3 -3
- package/dist/entity/index.js +3 -4
- package/dist/entity-BM1unrQf.cjs +41 -0
- package/dist/{entity-DJ0Gml9c.cjs → entity-BgaFJSaV.cjs} +4 -4
- package/dist/entity-CFAI1KyG.js +36 -0
- package/dist/entity-CFAI1KyG.js.map +1 -0
- package/dist/{entity-CniBgBSd.js → entity-CGqx_AU0.js} +4 -4
- package/dist/entity-CGqx_AU0.js.map +1 -0
- package/dist/{payload-AfUfSctb.d.ts → expirationTime-C7PZK49K.d.ts} +8 -37
- package/dist/expirationTime-C7PZK49K.d.ts.map +1 -0
- package/dist/expirationTime-CkIGNAg8.cjs +140 -0
- package/dist/{payload-D167BlJR.d.cts → expirationTime-DOB8V4B8.d.cts} +8 -37
- package/dist/expirationTime-DOB8V4B8.d.cts.map +1 -0
- package/dist/{expiry-Cgdb_dbq.js → expirationTime-UKPuA-LM.js} +62 -27
- package/dist/expirationTime-UKPuA-LM.js.map +1 -0
- package/dist/expiry-B48I-yTA.d.cts +40 -0
- package/dist/expiry-B48I-yTA.d.cts.map +1 -0
- package/dist/expiry-S2PUL8Hg.d.ts +40 -0
- package/dist/expiry-S2PUL8Hg.d.ts.map +1 -0
- package/dist/index-C-mMF5WZ.d.cts +735 -0
- package/dist/index-C-mMF5WZ.d.cts.map +1 -0
- package/dist/{index-BkqSnGJH.d.ts → index-C4zZqJ9S.d.ts} +5 -27
- package/dist/index-C4zZqJ9S.d.ts.map +1 -0
- package/dist/{index-CLhsFyVH.d.cts → index-CcVWYB6v.d.cts} +5 -27
- package/dist/index-CcVWYB6v.d.cts.map +1 -0
- package/dist/index-DCx2XC6S.d.ts +489 -0
- package/dist/index-DCx2XC6S.d.ts.map +1 -0
- package/dist/index-DLb7BfIw.d.ts +735 -0
- package/dist/index-DLb7BfIw.d.ts.map +1 -0
- package/dist/index-DzriOUm8.d.cts +489 -0
- package/dist/index-DzriOUm8.d.cts.map +1 -0
- package/dist/index.cjs +162 -146
- package/dist/index.d.cts +180 -133
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +180 -133
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +93 -83
- package/dist/index.js.map +1 -1
- package/dist/params-BuIrJA9a.cjs +45 -0
- package/dist/params-DDJOnsNi.js +22 -0
- package/dist/params-DDJOnsNi.js.map +1 -0
- package/dist/query/index.cjs +9 -4
- package/dist/query/index.d.cts +6 -6
- package/dist/query/index.d.ts +6 -6
- package/dist/query/index.js +4 -5
- package/dist/query-D0KtuFh2.cjs +1625 -0
- package/dist/query-DpRDozmj.js +1417 -0
- package/dist/query-DpRDozmj.js.map +1 -0
- package/dist/schema/index.cjs +46 -0
- package/dist/schema/index.d.cts +2 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +3 -0
- package/dist/{attr-BXTkpnoc.cjs → schema-B9AyXcgU.cjs} +513 -246
- package/dist/{attr-tmyREHtw.js → schema-DY4sqehl.js} +427 -244
- package/dist/schema-DY4sqehl.js.map +1 -0
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.ts +5 -5
- package/dist/types/index.js +1 -1
- package/dist/utils/index.cjs +3 -5
- package/dist/utils/index.d.cts +3 -3
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/index.js +3 -3
- package/package.json +9 -12
- package/src/actions/public/getEntity.test.ts +69 -0
- package/src/actions/public/getEntity.ts +34 -20
- package/src/actions/public/predictEntityKeys.ts +8 -7
- package/src/actions/public/query.test.ts +58 -0
- package/src/actions/public/query.ts +41 -13
- package/src/actions/wallet/createEntity.ts +34 -63
- package/src/actions/wallet/executeBatch.test.ts +38 -0
- package/src/actions/wallet/executeBatch.ts +26 -9
- package/src/actions/wallet/extendEntity.ts +9 -8
- package/src/actions/wallet/patchEntity.ts +43 -52
- package/src/clients/createPublicClient.test.ts +10 -2
- package/src/clients/createWalletClient.ts +4 -1
- package/src/clients/decorators/arkivPublic.ts +123 -80
- package/src/clients/decorators/arkivWallet.ts +113 -65
- package/src/entity/create.test.ts +25 -21
- package/src/entity/expiry.ts +8 -6
- package/src/entity/flags.ts +1 -1
- package/src/entity/index.ts +2 -4
- package/src/entity/key.ts +3 -3
- package/src/entity/operations.ts +1 -1
- package/src/errors.ts +2 -11
- package/src/index.ts +18 -6
- package/src/query/builderBase.ts +166 -0
- package/src/query/countBuilder.test.ts +91 -0
- package/src/query/countBuilder.ts +67 -0
- package/src/query/engine.ts +9 -3
- package/src/query/errors.ts +24 -14
- package/src/query/expression.test.ts +129 -21
- package/src/query/expression.ts +323 -94
- package/src/query/index.ts +26 -4
- package/src/query/injection.test.ts +5 -11
- package/src/query/queryBuilder.test.ts +467 -58
- package/src/query/queryBuilder.ts +191 -98
- package/src/query/queryResult.ts +35 -9
- package/src/query/selection.test.ts +278 -24
- package/src/query/selection.ts +427 -86
- package/src/schema/attributes.test.ts +266 -0
- package/src/schema/attributes.ts +212 -0
- package/src/{attr → schema}/codec.test.ts +37 -5
- package/src/{attr → schema}/codec.ts +40 -11
- package/src/schema/column.ts +193 -0
- package/src/{attr → schema}/errors.ts +67 -10
- package/src/schema/index.ts +76 -0
- package/src/{attr → schema}/names.ts +4 -2
- package/src/schema/schema.test.ts +171 -0
- package/src/schema/schema.ts +250 -0
- package/src/{attr → schema}/types.ts +36 -30
- package/src/{attr → schema}/values.test.ts +62 -3
- package/src/{attr → schema}/values.ts +246 -65
- package/src/types/entity.test.ts +1 -44
- package/src/types/entity.ts +9 -55
- package/src/types/events.ts +1 -1
- package/src/types/index.ts +11 -4
- package/src/types/rpcSchema.ts +22 -16
- package/src/utils/arkivTransactions.test.ts +356 -116
- package/src/utils/arkivTransactions.ts +30 -38
- package/src/utils/entities.ts +10 -9
- package/src/utils/expirationTime.ts +5 -5
- package/src/utils/index.ts +0 -1
- package/src/utils/logger.test.ts +45 -0
- package/src/utils/logger.ts +5 -39
- package/src/utils/revert.test.ts +25 -0
- package/src/utils/revert.ts +28 -3
- package/dist/attr/index.cjs +0 -34
- package/dist/attr/index.d.cts +0 -2
- package/dist/attr/index.d.ts +0 -2
- package/dist/attr/index.js +0 -3
- package/dist/attr-tmyREHtw.js.map +0 -1
- package/dist/entity-B0t6sN7F.cjs +0 -71
- package/dist/entity-BIp_df9u.js +0 -67
- package/dist/entity-BIp_df9u.js.map +0 -1
- package/dist/entity-CniBgBSd.js.map +0 -1
- package/dist/expiry-BxOlvVWC.d.ts +0 -79
- package/dist/expiry-BxOlvVWC.d.ts.map +0 -1
- package/dist/expiry-CG9GhVXv.cjs +0 -135
- package/dist/expiry-Cgdb_dbq.js.map +0 -1
- package/dist/expiry-DRV29F1r.d.cts +0 -79
- package/dist/expiry-DRV29F1r.d.cts.map +0 -1
- package/dist/flags-DCD26uhe.cjs +0 -43
- package/dist/flags-DUT2gHEU.js +0 -33
- package/dist/flags-DUT2gHEU.js.map +0 -1
- package/dist/index-BkqSnGJH.d.ts.map +0 -1
- package/dist/index-C7fbakoI.d.cts +0 -278
- package/dist/index-C7fbakoI.d.cts.map +0 -1
- package/dist/index-CIcHPx2j.d.cts +0 -424
- package/dist/index-CIcHPx2j.d.cts.map +0 -1
- package/dist/index-CLhsFyVH.d.cts.map +0 -1
- package/dist/index-Cd2aaWme.d.ts +0 -424
- package/dist/index-Cd2aaWme.d.ts.map +0 -1
- package/dist/index-Cq6fa8AI.d.ts +0 -278
- package/dist/index-Cq6fa8AI.d.ts.map +0 -1
- package/dist/index-DaNeJUw8.d.ts +0 -955
- package/dist/index-DaNeJUw8.d.ts.map +0 -1
- package/dist/index-Dv16LO2s.d.cts +0 -955
- package/dist/index-Dv16LO2s.d.cts.map +0 -1
- package/dist/payload-AfUfSctb.d.ts.map +0 -1
- package/dist/payload-D167BlJR.d.cts.map +0 -1
- package/dist/payload-DE7wMztL.js +0 -92
- package/dist/payload-DE7wMztL.js.map +0 -1
- package/dist/payload-DqFweqiO.cjs +0 -108
- package/dist/query-9oojUdM7.cjs +0 -1128
- package/dist/query-Ber-lOJQ.js +0 -968
- package/dist/query-Ber-lOJQ.js.map +0 -1
- package/src/actions/public/getEntityCount.ts +0 -10
- package/src/attr/attributes.test.ts +0 -120
- package/src/attr/attributes.ts +0 -196
- package/src/attr/index.ts +0 -50
- package/src/types/mimeTypes.ts +0 -27
- package/src/utils/compression.test.ts +0 -32
- package/src/utils/compression.ts +0 -102
- package/src/utils/payload.ts +0 -36
- /package/src/{attr → schema}/decimal.ts +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"query-Ber-lOJQ.js","names":["enabledNamespaces: Set<string>","disabledNamespaces: Set<string>","logger","attributes: Record<string, ReturnType<typeof decodeRpcValue>>","schema: Record<string, ReturnType<typeof asTypeTag>>","QUERY_ERROR_KINDS: Readonly<Record<number, QueryErrorKind>>","code: number | undefined","data: unknown","detail: string | undefined","current: unknown","queryOptions: RpcQueryOptions","ORDERED_TAGS: ReadonlySet<TypeTag>","RANGE_OPERATORS: ReadonlySet<ComparisonOperator>","select: RpcSelect","fetchPage","page: QueryResult<TEntity> | undefined"],"sources":["../src/utils/logger.ts","../src/errors.ts","../src/utils/entities.ts","../src/query/errors.ts","../src/query/engine.ts","../src/query/expression.ts","../src/query/selection.ts","../src/query/queryResult.ts","../src/query/queryBuilder.ts"],"sourcesContent":["const ROOT_NAMESPACE = \"arkiv\"\n\n// Debug logger type - returns a function that can be called to log messages\ntype DebugLogger = (...args: unknown[]) => void\n\n// Get DEBUG environment variable (works in both Node.js and browser)\nfunction getDebugEnv(): string {\n if (typeof process !== \"undefined\") {\n const debugEnv = process?.env?.DEBUG\n if (debugEnv) {\n return debugEnv\n }\n }\n // Browser: check localStorage\n if (typeof globalThis !== \"undefined\" && \"localStorage\" in globalThis) {\n try {\n const storage = (globalThis as { localStorage?: Storage }).localStorage\n return storage?.getItem(\"DEBUG\") || \"\"\n } catch {\n // localStorage might not be available\n }\n }\n return \"\"\n}\n\n// Parse debug patterns and check if a namespace matches\nfunction matchesPattern(namespace: string, pattern: string): boolean {\n // Exact match\n if (pattern === namespace) return true\n\n // Wildcard match: \"arkiv:*\" matches \"arkiv:query\", \"arkiv:rpc\", etc.\n if (pattern.endsWith(\"*\")) {\n const prefix = pattern.slice(0, -1)\n return namespace.startsWith(prefix)\n }\n\n // Negative match: \"-arkiv:query\" excludes \"arkiv:query\"\n if (pattern.startsWith(\"-\")) {\n const excludePattern = pattern.slice(1)\n return !matchesPattern(namespace, excludePattern)\n }\n\n return false\n}\n\n// Check if a namespace is enabled for debugging\nfunction isEnabled(namespace: string): boolean {\n const debugEnv = getDebugEnv()\n if (!debugEnv) return false\n\n // Split by comma/space and check each pattern\n const patterns = debugEnv.split(/[\\s,]+/).filter((p) => p.length > 0)\n\n // If no patterns, nothing is enabled\n if (patterns.length === 0) return false\n\n // Check if any pattern matches (or if explicitly excluded)\n let enabled = false\n for (const pattern of patterns) {\n if (pattern.startsWith(\"-\")) {\n // Exclude pattern - if it matches, disable\n if (matchesPattern(namespace, pattern)) {\n return false\n }\n } else if (matchesPattern(namespace, pattern)) {\n enabled = true\n }\n }\n\n return enabled\n}\n\n// Store enabled namespaces (for programmatic enable/disable)\nconst enabledNamespaces: Set<string> = new Set()\nconst disabledNamespaces: Set<string> = new Set()\n\n// Color palette for namespace coloring (distinct, readable colors)\nconst COLOR_PALETTE = [\n \"#0000FF\", // blue\n \"#008000\", // green\n \"#FF0000\", // red\n \"#FF00FF\", // magenta\n \"#FFA500\", // orange\n \"#800080\", // purple\n \"#00CED1\", // dark turquoise\n \"#FF1493\", // deep pink\n \"#32CD32\", // lime green\n \"#1E90FF\", // dodger blue\n \"#FF4500\", // orange red\n \"#9370DB\", // medium purple\n \"#00FA9A\", // medium spring green\n \"#FFD700\", // gold\n \"#DC143C\", // crimson\n \"#00BFFF\", // deep sky blue\n \"#FF69B4\", // hot pink\n \"#20B2AA\", // light sea green\n \"#DA70D6\", // orchid\n \"#4682B4\", // steel blue\n]\n\n// Detect if we're in Node.js (for ANSI colors) or browser (for CSS colors)\nconst isNode = typeof process !== \"undefined\" && process.versions?.node != null\n\n// ANSI color codes for Node.js (bright, distinct colors)\nconst ANSI_COLORS = [\n \"\\x1b[94m\", // bright blue\n \"\\x1b[92m\", // bright green\n \"\\x1b[91m\", // bright red\n \"\\x1b[95m\", // bright magenta\n \"\\x1b[93m\", // bright yellow\n \"\\x1b[96m\", // bright cyan\n \"\\x1b[35m\", // magenta\n \"\\x1b[36m\", // cyan\n \"\\x1b[32m\", // green\n \"\\x1b[34m\", // blue\n \"\\x1b[33m\", // yellow\n \"\\x1b[31m\", // red\n \"\\x1b[90m\", // bright black (gray)\n \"\\x1b[37m\", // white\n \"\\x1b[1m\", // bold (as a color option)\n]\n\nconst ANSI_RESET = \"\\x1b[0m\"\nconst ANSI_BOLD = \"\\x1b[1m\"\n\n// Cache colors for namespaces (same namespace = same color)\nconst namespaceColors = new Map<string, { css: string; ansi: string }>()\n\n// Generate a consistent color for a namespace based on its hash\nfunction getColorForNamespace(namespace: string): { css: string; ansi: string } {\n // Check cache first\n const cachedColor = namespaceColors.get(namespace)\n if (cachedColor) {\n return cachedColor\n }\n\n // Simple hash function to convert string to number\n let hash = 0\n for (let i = 0; i < namespace.length; i++) {\n const char = namespace.charCodeAt(i)\n hash = (hash << 5) - hash + char\n hash = hash & hash // Convert to 32-bit integer\n }\n\n // Use absolute value and modulo to get index\n const colorIndex = Math.abs(hash) % COLOR_PALETTE.length\n const cssColor = COLOR_PALETTE[colorIndex] ?? \"inherit\"\n const ansiColor = ANSI_COLORS[colorIndex % ANSI_COLORS.length] ?? \"\"\n\n const color = { css: cssColor, ansi: ansiColor }\n\n // Cache the color\n namespaceColors.set(namespace, color)\n\n return color\n}\n\n// Create a debug logger function\nfunction createDebugLogger(namespace: string): DebugLogger {\n // Get the color for this namespace (consistent across calls)\n const colors = getColorForNamespace(namespace)\n\n return (...args: unknown[]) => {\n // Check if enabled via environment variable or programmatically\n const envEnabled = isEnabled(namespace)\n const progEnabled = enabledNamespaces.has(namespace) && !disabledNamespaces.has(namespace)\n\n if (envEnabled || progEnabled) {\n if (isNode) {\n // Node.js: use ANSI color codes\n const coloredNamespace = `${ANSI_BOLD}${colors.ansi}${namespace}${ANSI_RESET}`\n console.log(coloredNamespace, ...args)\n } else {\n // Browser: use CSS styling with %c\n // The format is: console.log(\"%cstyled%cnormal\", style1, style2, ...args)\n // This colors the namespace, then resets for the rest\n const style1 = `color: ${colors.css}; font-weight: bold`\n const style2 = \"color: inherit; font-weight: normal\"\n // Combine namespace with a space, then log with styles\n console.log(`%c${namespace}%c`, style1, style2, ...args)\n }\n }\n }\n}\n\nexport const getLogger = (scope: string): DebugLogger => {\n const namespace = `${ROOT_NAMESPACE}:${scope}`\n return createDebugLogger(namespace)\n}\n\nexport const enableDebug = (namespaces: string): void => {\n // Parse and add namespaces to enabled set\n const patterns = namespaces.split(/[\\s,]+/).filter((p) => p.length > 0)\n for (const pattern of patterns) {\n if (pattern.startsWith(\"-\")) {\n // Remove from enabled, add to disabled\n const namespace = pattern.slice(1)\n enabledNamespaces.delete(namespace)\n disabledNamespaces.add(namespace)\n } else {\n enabledNamespaces.add(pattern)\n disabledNamespaces.delete(pattern)\n }\n }\n}\n\nexport const disableDebug = (): void => {\n enabledNamespaces.clear()\n disabledNamespaces.clear()\n}\n","import type { Hex } from \"viem\"\n\nexport class EntityMutationError extends Error {\n readonly txHash: Hex | undefined\n\n constructor(message: string, options?: { cause?: unknown; txHash?: Hex | undefined }) {\n super(message, { cause: options?.cause })\n this.name = \"EntityMutationError\"\n this.txHash = options?.txHash\n }\n}\nexport class NoMoreResultsError extends Error {\n constructor() {\n super(\"No more results — the last page carried no cursor. Check hasNextPage() before next().\")\n this.name = \"NoMoreResultsError\"\n }\n}\n\nexport class NoEntityFoundError extends Error {\n /** The key that matched nothing, when the lookup was by key. */\n readonly entityKey: string | undefined\n\n constructor(entityKey?: string) {\n super(\n entityKey === undefined\n ? \"No entity found\"\n : `No live entity with key ${entityKey}. It was never created, or it has been deleted or ` +\n \"has expired.\",\n )\n this.name = \"NoEntityFoundError\"\n this.entityKey = entityKey\n }\n}\n\nexport class EmptyPatchError extends Error {\n readonly entityKey: string\n\n constructor(entityKey: string) {\n super(\n `The patch for entity ${entityKey} has nothing to apply. Pass \"set\" to write attributes, ` +\n '\"unset\" to remove them, or \"payload\"/\"contentType\" to replace the contents.',\n )\n this.name = \"EmptyPatchError\"\n this.entityKey = entityKey\n }\n}\n\nexport class InvalidContentTypeError extends Error {\n constructor(contentType: string) {\n super(\n `Invalid content type \"${contentType}\". Must follow RFC 2045 MIME grammar and be lowercase only (e.g. \"text/plain\").`,\n )\n this.name = \"InvalidContentTypeError\"\n }\n}\n","import { checksumAddress, type Hex, hexToBytes } from \"viem\"\nimport type { AttributeSchema, Attributes } from \"../attr\"\n// Internal seam: the JSON-RPC decoder is not part of the package's public surface.\nimport { asTypeTag, decodeRpcValue } from \"../attr/codec\"\nimport {\n decodeCreationFlags,\n encodeCreationFlags,\n type ResolvedCreationFlags,\n} from \"../entity/flags\"\nimport { Entity, type EntityFields } from \"../types/entity\"\nimport type { RpcEntity } from \"../types/rpcSchema\"\nimport { getLogger } from \"./logger\"\n\nconst logger = getLogger(\"utils:entities\")\n\n/**\n * Turns the attribute array a query returns into a name-keyed map of typed values.\n *\n * Attribute names are unique per entity, so the map loses nothing and gives callers\n * `entity.attributes.level.value` instead of a linear search.\n */\nfunction decodeAttributes(rpcAttributes: NonNullable<RpcEntity[\"attributes\"]>): Attributes {\n const attributes: Record<string, ReturnType<typeof decodeRpcValue>> = {}\n for (const { name, type, value } of rpcAttributes) {\n attributes[name] = decodeRpcValue(type, value)\n }\n return attributes\n}\n\n/** The same, for the values-free `attributeSchema` projection. */\nfunction decodeSchema(entries: NonNullable<RpcEntity[\"attributeSchema\"]>): AttributeSchema {\n const schema: Record<string, ReturnType<typeof asTypeTag>> = {}\n for (const { name, type } of entries) {\n schema[name] = asTypeTag(type)\n }\n return schema\n}\n\n/**\n * Builds an {@link Entity} from a query result row.\n *\n * A field the query did not select is absent from the response and stays `undefined` here — the\n * decoder never substitutes an empty value for one that was not asked for.\n */\nexport function entityFromRpcResult(rpcEntity: RpcEntity): Entity {\n logger(\"entityFromRpcResult %o\", rpcEntity)\n\n const fields: EntityFields = {\n key: rpcEntity.key,\n owner: toAddress(rpcEntity.owner),\n creator: toAddress(rpcEntity.creator),\n createdAt: toBlock(rpcEntity.createdAt),\n updatedAt: toBlock(rpcEntity.updatedAt),\n expiresAt: toBlock(rpcEntity.expiresAt),\n creationFlags: decodeFlags(rpcEntity.creationFlags),\n contentType: rpcEntity.contentType,\n payload: rpcEntity.payload !== undefined ? hexToBytes(rpcEntity.payload) : undefined,\n attributeSchema:\n rpcEntity.attributeSchema !== undefined ? decodeSchema(rpcEntity.attributeSchema) : undefined,\n attributes:\n rpcEntity.attributes !== undefined ? decodeAttributes(rpcEntity.attributes) : undefined,\n }\n\n return new Entity(fields)\n}\n\nfunction toBlock(value: string | undefined): bigint | undefined {\n return value === undefined ? undefined : BigInt(value)\n}\n\nfunction toAddress(value: Hex | undefined): Hex | undefined {\n if (value === undefined) return undefined\n if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {\n logger(\"unreadable address %o\", value)\n return value\n }\n return checksumAddress(value)\n}\n\n/**\n * Resolves the `creationFlags` projection, preferring the byte.\n */\nfunction decodeFlags(\n value: NonNullable<RpcEntity[\"creationFlags\"]> | undefined,\n): ResolvedCreationFlags | undefined {\n if (value === undefined) return undefined\n\n const raw = toFlagsByte(typeof value === \"number\" ? value : value.raw)\n if (raw !== undefined) return decodeCreationFlags(raw)\n\n if (\n typeof value === \"object\" &&\n (value.readonly ?? value.permissionlessExtension) !== undefined\n ) {\n return decodeCreationFlags(\n encodeCreationFlags({\n readonly: value.readonly ?? false,\n permissionlessExtension: value.permissionlessExtension ?? false,\n }),\n )\n }\n\n logger(\"unreadable creationFlags %o\", value)\n return undefined\n}\n\n/**\n * A flags byte in whichever spelling arrived: a JSON number, or the `0x` QUANTITY that every other\n * numeric field on {@link RpcEntity} already uses. `undefined` for anything that is not a byte.\n */\nfunction toFlagsByte(value: unknown): number | undefined {\n const parsed =\n typeof value === \"number\"\n ? value\n : typeof value === \"string\" && /^(0x[0-9a-fA-F]+|\\d+)$/.test(value)\n ? Number(value)\n : Number.NaN\n return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0xff ? parsed : undefined\n}\n","import type { TypeTag } from \"../attr\"\n\n/**\n * A predicate asked for an operator the query language does not define for that type.\n *\n * The operator × type matrix is part of the language, not a policy: a range comparison on an\n * equality-only type is a **parse error** at the node, not an empty result. Catching it here turns\n * a round trip and an RPC error code into a stack trace pointing at the call site.\n *\n * | | `=` `!=` | `<` `<=` `>` `>=` | `STARTSWITH` |\n * | --- | --- | --- | --- |\n * | `bool` | yes | no | no |\n * | `i32` `u64` `u256` `dec` | yes | yes | no |\n * | `str` | yes | no | yes |\n * | `addr` `key` `bytes32` | yes | no | no |\n */\nexport class UnsupportedOperatorError extends Error {\n /** The attribute the predicate names. */\n readonly attributeName: string\n /** The operator, as it would have been written in the query. */\n readonly operator: string\n /** The type of the value it was applied to. */\n readonly tag: TypeTag\n\n constructor(attributeName: string, operator: string, tag: TypeTag, hint?: string) {\n super(\n `The query language does not define ${operator} for a ${tag}, so \"${attributeName} ` +\n `${operator} ...\" cannot be expressed.${hint ? ` ${hint}` : \"\"}`,\n )\n this.name = \"UnsupportedOperatorError\"\n this.attributeName = attributeName\n this.operator = operator\n this.tag = tag\n }\n}\n\n/**\n * A predicate could not be expressed in the query language for a reason other than its operator —\n * a system attribute that is returned in results but cannot be filtered on, a system attribute\n * compared against the wrong type, or a combinator with nothing to combine.\n */\nexport class InvalidPredicateError extends Error {\n /** The attribute the predicate names, where there is one. */\n readonly attributeName: string | undefined\n\n constructor(message: string, attributeName?: string) {\n super(message)\n this.name = \"InvalidPredicateError\"\n this.attributeName = attributeName\n }\n}\n\n/**\n * What a node rejected a query for. Part of the frozen RPC surface, so it is safe to branch on —\n * a `\"cursor\"` failure means start the query again, a `\"block\"` failure means the node no longer\n * retains that height, and the rest mean the query itself needs fixing.\n */\nexport type QueryErrorKind =\n /** The query string does not parse. `data` locates it: `{ position, expected, got }`. */\n | \"parse\"\n /** A predicate is well-formed but ill-typed — a range operator on an equality-only type, an unknown tag. */\n | \"type\"\n /** A literal failed validation — an `i32` out of range, a bad EIP-55 checksum, more than 18 decimal places. */\n | \"literal\"\n /** A node budget was exceeded — query length, predicate count, nesting depth, bitmap operations. */\n | \"limits\"\n /** The cursor expired, is malformed, or belongs to a different query, block or selection. */\n | \"cursor\"\n /** `atBlock` is outside the range the node retains. */\n | \"block\"\n\n/** The JSON-RPC error codes `arkiv_query` answers with, and what each one means. */\nconst QUERY_ERROR_KINDS: Readonly<Record<number, QueryErrorKind>> = {\n [-32001]: \"parse\",\n [-32002]: \"type\",\n [-32003]: \"literal\",\n [-32004]: \"limits\",\n [-32005]: \"cursor\",\n [-32006]: \"block\",\n}\n\n/**\n * The node rejected a query.\n *\n * These codes are part of the frozen `arkiv_query` surface, so {@link kind} and {@link data} are\n * stable enough to branch on — which the underlying JSON-RPC error is not, because the codes\n * overlap Ethereum's own EIP-1474 numbering and get wrapped accordingly on the way up.\n *\n * @example\n * try {\n * await result.next()\n * } catch (error) {\n * if (error instanceof QueryError && error.kind === \"cursor\") {\n * // The page is gone — start the query over.\n * }\n * }\n */\nexport class QueryError extends Error {\n /** Which failure this is. */\n readonly kind: QueryErrorKind\n /** The JSON-RPC error code. */\n readonly code: number\n /** The machine-readable detail the node sent, if any — e.g. `{ position, expected, got }`. */\n readonly data: unknown\n /** The query string that was rejected. */\n readonly query: string\n\n constructor(kind: QueryErrorKind, code: number, query: string, data: unknown, detail?: string) {\n super(`Query rejected (${kind}, ${code})${detail ? `: ${detail}` : \"\"}. Query: ${query}`, {\n cause: data,\n })\n this.name = \"QueryError\"\n this.kind = kind\n this.code = code\n this.data = data\n this.query = query\n }\n}\n\n/**\n * Recognises an `arkiv_query` failure in whatever the transport threw.\n *\n * The codes are dug out of the error chain rather than read off the top: viem maps this range onto\n * its own EIP-1474 error classes (`-32001` is its \"resource not found\"), so by the time an error\n * surfaces the Arkiv meaning is one or more `cause` links down.\n *\n * @returns The typed error, or `undefined` if this was not a query rejection.\n */\nexport function asQueryError(error: unknown, query: string): QueryError | undefined {\n let code: number | undefined\n let data: unknown\n let detail: string | undefined\n\n let current: unknown = error\n for (let depth = 0; current != null && depth < 8; depth++) {\n const link = current as {\n code?: unknown\n data?: unknown\n shortMessage?: unknown\n cause?: unknown\n }\n if (code === undefined && typeof link.code === \"number\") code = link.code\n if (data === undefined && link.data !== undefined) data = link.data\n if (detail === undefined && typeof link.shortMessage === \"string\") detail = link.shortMessage\n current = link.cause\n }\n\n const kind = code === undefined ? undefined : QUERY_ERROR_KINDS[code]\n if (kind === undefined || code === undefined) return undefined\n return new QueryError(kind, code, query, data, detail)\n}\n","import { numberToHex } from \"viem\"\nimport type { ArkivClient } from \"../clients/baseClient\"\nimport type { Entity } from \"../types/entity\"\nimport type { RpcQueryOptions, RpcSelect } from \"../types/rpcSchema\"\nimport { entityFromRpcResult } from \"../utils/entities\"\nimport { getLogger } from \"../utils/logger\"\nimport { asQueryError } from \"./errors\"\n\nconst logger = getLogger(\"query:engine\")\n\nexport const MAX_LIMIT = 200\n\n/** One `arkiv_query` call. */\nexport type QueryRequest = {\n /** The query expression, already rendered. */\n query: string\n /** What to return. */\n select: RpcSelect\n /** Page size, up to {@link MAX_LIMIT}. Defaults to the node's own page size. */\n limit?: number | undefined\n /** Cursor from a previous page. */\n cursor?: string | undefined\n /** Block height to read at. Defaults to the head. */\n atBlock?: bigint | undefined\n}\n\n/** One page of results. */\nexport type QueryResponse = {\n entities: Entity[]\n /** The block the page was read at — the same for every page of a paginated query. */\n blockNumber: bigint\n /** The cursor for the next page, or `undefined` when this was the last one. */\n cursor: string | undefined\n}\n\n/**\n * Runs one query and decodes the page it returns.\n *\n * @throws {QueryError} If the node rejects the query.\n */\nexport async function runQuery(client: ArkivClient, request: QueryRequest): Promise<QueryResponse> {\n const { query, select, limit, cursor, atBlock } = request\n\n if (limit !== undefined) {\n if (!Number.isInteger(limit) || limit < 1) {\n throw new Error(`limit must be a positive integer, got ${limit}.`)\n }\n if (limit > MAX_LIMIT) {\n throw new Error(\n `limit ${limit} exceeds the node maximum of ${MAX_LIMIT}. Page through the results with ` +\n \"the cursor instead.\",\n )\n }\n }\n\n const queryOptions: RpcQueryOptions = {\n select,\n ...(atBlock !== undefined && { atBlock: numberToHex(atBlock) }),\n ...(limit !== undefined && { limit: numberToHex(limit) }),\n ...(cursor !== undefined && { cursor }),\n }\n\n logger(\"arkiv_query %s %o\", query, queryOptions)\n\n const result = await client\n .request({ method: \"arkiv_query\", params: [query, queryOptions] })\n .catch((error: unknown) => {\n throw asQueryError(error, query) ?? error\n })\n\n logger(\"Raw result from query %o\", result)\n\n return {\n entities: result.data.map((rpcEntity) => entityFromRpcResult(rpcEntity)),\n blockNumber: BigInt(result.blockNumber),\n cursor: result.cursor,\n }\n}\n","import {\n type ArkivValue,\n type StrValue,\n str,\n type TypeTag,\n toValue,\n type UserTypeTag,\n type ValueInput,\n validateAttributeName,\n} from \"../attr\"\n// Internal seam: the typeId table is the protocol's, not part of the package's public surface.\nimport { TYPE_IDS } from \"../attr/types\"\nimport { InvalidPredicateError, UnsupportedOperatorError } from \"./errors\"\n\n/**\n * The comparison operators the query language defines. `<` `<=` `>` `>=` need an ordered type —\n * `i32`, `u64`, `u256` or `dec`; every type accepts `=` and `!=`.\n */\nexport type ComparisonOperator = \"=\" | \"!=\" | \"<\" | \"<=\" | \">\" | \">=\"\n\n/** A comparison between an attribute and a typed literal. */\nexport type ComparisonNode = {\n readonly kind: \"comparison\"\n readonly name: string\n readonly operator: ComparisonOperator\n readonly value: ArkivValue\n}\n\n/** A `STARTSWITH` prefix test, the language's only pattern operator. */\nexport type StartsWithNode = {\n readonly kind: \"startsWith\"\n readonly name: string\n readonly value: StrValue\n}\n\n/** An `EXISTS(name)` test — true when the attribute is set with any type. */\nexport type ExistsNode = {\n readonly kind: \"exists\"\n readonly name: string\n}\n\n/** A `TYPEOF(name) = tag` test. */\nexport type TypeOfNode = {\n readonly kind: \"typeOf\"\n readonly name: string\n readonly tag: UserTypeTag\n}\n\n/** The complement of an expression, evaluated against the active-entity set. */\nexport type NotNode = {\n readonly kind: \"not\"\n readonly expression: Expression\n}\n\n/** A conjunction. */\nexport type AndNode = {\n readonly kind: \"and\"\n readonly expressions: readonly Expression[]\n}\n\n/** A disjunction. */\nexport type OrNode = {\n readonly kind: \"or\"\n readonly expressions: readonly Expression[]\n}\n\n/**\n * A query expression: an immutable tree built by the combinators below, whose `toString()` is the\n * exact query string sent over the wire.\n *\n * @example\n * const q = and(gte(\"level\", i32(10)), or(eq(\"status\", str(\"open\")), not(exists(\"closedAt\"))))\n * String(q) // level >= i32(10) AND (status = str('open') OR NOT EXISTS(closedAt))\n */\nexport type Expression =\n | ComparisonNode\n | StartsWithNode\n | ExistsNode\n | TypeOfNode\n | NotNode\n | AndNode\n | OrNode\n\n/**\n * The system attributes a query may filter on, and the type each one carries.\n *\n * Every other system attribute (`$createdAt`, `$updatedAt`, `$creationFlags`, `$contentType`,\n * `$payload`) is returned in query results but is not indexed, so it cannot appear in a predicate.\n */\nconst QUERYABLE_SYSTEM_ATTRIBUTES = {\n $key: \"key\",\n $owner: \"addr\",\n $creator: \"addr\",\n $expiresAt: \"u64\",\n} as const satisfies Record<string, TypeTag>\n\ntype SystemAttributeName = keyof typeof QUERYABLE_SYSTEM_ATTRIBUTES\n\n/** System attributes a query can ask for but not filter on — they carry no index. */\nconst RESULT_ONLY_SYSTEM_ATTRIBUTES = [\n \"$createdAt\",\n \"$updatedAt\",\n \"$creationFlags\",\n \"$contentType\",\n \"$payload\",\n]\n\n/** The types that carry an ordered index, and so accept `<` `<=` `>` `>=`. */\nconst ORDERED_TAGS: ReadonlySet<TypeTag> = new Set<TypeTag>([\"i32\", \"u64\", \"u256\", \"dec\"])\n\nconst RANGE_OPERATORS: ReadonlySet<ComparisonOperator> = new Set<ComparisonOperator>([\n \"<\",\n \"<=\",\n \">\",\n \">=\",\n])\n\n/**\n * Checks an attribute reference, returning its fixed type when it names a system attribute.\n *\n * User attribute names go through the same grammar the write path enforces, so a name that could\n * never have been written is rejected here rather than matching nothing.\n */\nfunction resolveReference(name: string): TypeTag | undefined {\n if (typeof name !== \"string\" || !name.startsWith(\"$\")) {\n validateAttributeName(name)\n return undefined\n }\n const system = QUERYABLE_SYSTEM_ATTRIBUTES[name as SystemAttributeName]\n if (system !== undefined) return system\n\n if (RESULT_ONLY_SYSTEM_ATTRIBUTES.includes(name)) {\n throw new InvalidPredicateError(\n `\"${name}\" is returned in query results but carries no index, so it cannot be filtered ` +\n \"on. Select it and filter the results in JavaScript instead.\",\n name,\n )\n }\n throw new InvalidPredicateError(\n `\"${name}\" is not a system attribute. The queryable ones are ` +\n `${Object.keys(QUERYABLE_SYSTEM_ATTRIBUTES).join(\", \")}. ` +\n '\"$\" is reserved for these, so a user attribute cannot be named this.',\n name,\n )\n}\n\n/**\n * Whether a string names a type a client may set. `bytes` is excluded: it backs `$payload`, which\n * is neither settable nor queryable.\n *\n * Takes a `string` rather than a `UserTypeTag` on purpose — the callers' static types already say\n * as much, and this is the runtime check for the code that has no types.\n */\nfunction isUserTypeTag(tag: string): tag is UserTypeTag {\n // `Object.hasOwn` rather than `in`, which would also accept every Object.prototype key.\n return Object.hasOwn(TYPE_IDS, tag) && tag !== \"bytes\"\n}\n\n/** Freezes a node and gives it a (non-enumerable, so still `toEqual`-able) rendering. */\nfunction node<T extends Expression>(value: T): T {\n Object.defineProperty(value, \"toString\", {\n value: () => render(value),\n enumerable: false,\n })\n return Object.freeze(value)\n}\n\nfunction comparison(name: string, operator: ComparisonOperator, input: ValueInput): ComparisonNode {\n const system = resolveReference(name)\n const value = toValue(input, name)\n\n if (system !== undefined && value.type !== system) {\n throw new InvalidPredicateError(\n `\"${name}\" is a ${system}, but the value is a ${value.type}. ` +\n `System attributes have a fixed type — write ${system}(...) around the value.`,\n name,\n )\n }\n if (RANGE_OPERATORS.has(operator) && !ORDERED_TAGS.has(value.type)) {\n throw new UnsupportedOperatorError(\n name,\n operator,\n value.type,\n `Only i32, u64, u256 and dec carry an ordered index; ${value.type} is equality-only.`,\n )\n }\n return node({ kind: \"comparison\", name, operator, value })\n}\n\n/**\n * Attribute equals value — set with this exact type, and holding this value.\n *\n * Bare booleans, numbers, bigints and strings take the same default types they take when written\n * (`number` is an `i32`, `bigint` a `u256`, `string` a `str`); anything else names its type.\n *\n * @throws {InvalidPredicateError} If the name is not a queryable attribute, or a system attribute\n * is compared against the wrong type.\n *\n * @example\n * eq(\"status\", str(\"open\")) // status = str('open')\n * eq(\"flagged\", true) // flagged = true\n * eq(\"$owner\", addr(account)) // $owner = addr(0xd8dA…)\n */\nexport function eq(name: string, value: ValueInput): ComparisonNode {\n return comparison(name, \"=\", value)\n}\n\n/**\n * Attribute is set with this type and holds a **different** value.\n *\n * This is typed value-negation, not the complement: an entity that never set the attribute, or set\n * it with another type, does not match. Use `not(eq(...))` for the full complement.\n *\n * @example\n * ne(\"status\", str(\"closed\")) // status != str('closed')\n * not(eq(\"status\", str(\"closed\"))) // NOT status = str('closed') — also matches entities with no status\n */\nexport function ne(name: string, value: ValueInput): ComparisonNode {\n return comparison(name, \"!=\", value)\n}\n\n/**\n * Attribute is greater than value. Ordered types only (`i32`, `u64`, `u256`, `dec`).\n *\n * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.\n *\n * @example\n * gt(\"level\", i32(10)) // level > i32(10)\n * gt(\"$expiresAt\", u64(1_200_000n)) // $expiresAt > u64(1200000)\n */\nexport function gt(name: string, value: ValueInput): ComparisonNode {\n return comparison(name, \">\", value)\n}\n\n/**\n * Attribute is greater than or equal to value. Ordered types only (`i32`, `u64`, `u256`, `dec`).\n *\n * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.\n *\n * @example\n * gte(\"score\", dec(\"3.5\")) // score >= dec(3.5)\n */\nexport function gte(name: string, value: ValueInput): ComparisonNode {\n return comparison(name, \">=\", value)\n}\n\n/**\n * Attribute is less than value. Ordered types only (`i32`, `u64`, `u256`, `dec`).\n *\n * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.\n *\n * @example\n * lt(\"level\", i32(10)) // level < i32(10)\n */\nexport function lt(name: string, value: ValueInput): ComparisonNode {\n return comparison(name, \"<\", value)\n}\n\n/**\n * Attribute is less than or equal to value. Ordered types only (`i32`, `u64`, `u256`, `dec`).\n *\n * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.\n *\n * @example\n * lte(\"score\", dec(\"5\")) // score <= dec(5)\n */\nexport function lte(name: string, value: ValueInput): ComparisonNode {\n return comparison(name, \"<=\", value)\n}\n\n/**\n * Attribute is a `str` beginning with this prefix.\n *\n * The match is on **raw UTF-8 bytes** with no normalization — it is the index encoding, so\n * `startsWith(\"name\", \"é\")` matches only the same byte sequence, not another normal form of it.\n *\n * @param prefix - The prefix, as a bare string or a `str` value.\n * @throws {InvalidPredicateError} If the name is not a queryable attribute.\n * @throws {UnsupportedOperatorError} If given a value that is not a `str`, or a system attribute\n * (none of which is a `str`).\n *\n * @example\n * startsWith(\"desc\", \"ab\") // desc STARTSWITH str('ab')\n */\nexport function startsWith(name: string, prefix: string | StrValue): StartsWithNode {\n const system = resolveReference(name)\n const value = typeof prefix === \"string\" ? str(prefix) : toValue(prefix, name)\n if (value.type !== \"str\") {\n throw new UnsupportedOperatorError(\n name,\n \"STARTSWITH\",\n value.type,\n \"Only str carries a prefix index.\",\n )\n }\n if (system !== undefined) {\n throw new UnsupportedOperatorError(\n name,\n \"STARTSWITH\",\n system,\n \"No queryable system attribute is a str.\",\n )\n }\n return node({ kind: \"startsWith\", name, value })\n}\n\n/**\n * The attribute is set, with any type.\n *\n * @throws {InvalidPredicateError} If given a system attribute — every entity has all of them, so\n * the test would be a constant.\n *\n * @example\n * exists(\"reviewedBy\") // EXISTS(reviewedBy)\n * not(exists(\"closedAt\")) // NOT EXISTS(closedAt)\n */\nexport function exists(name: string): ExistsNode {\n if (resolveReference(name) !== undefined) {\n throw new InvalidPredicateError(\n `\"${name}\" is a system attribute, so it is set on every entity and EXISTS(${name}) is ` +\n \"always true.\",\n name,\n )\n }\n return node({ kind: \"exists\", name })\n}\n\n/**\n * The attribute is set with exactly this type, whatever its value.\n *\n * Useful when the same name is written with different types across entities — a comparison already\n * asserts the type, so this is for asking about the type alone.\n *\n * @param tag - The type tag: `bool`, `i32`, `u64`, `u256`, `dec`, `bytes32`, `str`, `addr` or\n * `key`.\n * @throws {InvalidPredicateError} If the tag is not a settable type, or the name is a system\n * attribute (whose type is fixed).\n *\n * @example\n * hasType(\"age\", \"i32\") // TYPEOF(age) = i32\n */\nexport function hasType(name: string, tag: UserTypeTag): TypeOfNode {\n const system = resolveReference(name)\n if (system !== undefined) {\n throw new InvalidPredicateError(\n `\"${name}\" is always a ${system} — a system attribute has a fixed type, so ` +\n `TYPEOF(${name}) is a constant.`,\n name,\n )\n }\n if (!isUserTypeTag(tag)) {\n throw new InvalidPredicateError(\n `\"${tag}\" is not a settable attribute type. Use one of ` +\n \"bool, i32, u64, u256, dec, bytes32, str, addr, key.\",\n name,\n )\n }\n return node({ kind: \"typeOf\", name, tag })\n}\n\n/**\n * The complement of an expression, evaluated against the set of live entities — unlike `!=`, it\n * matches entities that never set the attribute at all.\n *\n * @example\n * not(exists(\"closedAt\")) // NOT EXISTS(closedAt)\n * not(and(eq(\"a\", 1), eq(\"b\", 2))) // NOT (a = i32(1) AND b = i32(2))\n */\nexport function not(expression: Expression): NotNode {\n return node({ kind: \"not\", expression })\n}\n\n/**\n * All of these must hold.\n *\n * A single expression is returned unwrapped, so building a conjunction from a filtered list does\n * not litter the query with redundant nesting.\n *\n * @param expressions - The expressions, as separate arguments or one array.\n * @throws {InvalidPredicateError} If given nothing to combine.\n *\n * @example\n * and(gte(\"level\", i32(10)), eq(\"status\", \"open\"))\n * and(filters) // an array works too\n */\nexport function and(expressions: readonly Expression[]): Expression\nexport function and(...expressions: Expression[]): Expression\nexport function and(...expressions: (Expression | readonly Expression[])[]): Expression {\n return combine(\"and\", expressions.flat())\n}\n\n/**\n * At least one of these must hold.\n *\n * A single expression is returned unwrapped.\n *\n * @param expressions - The expressions, as separate arguments or one array.\n * @throws {InvalidPredicateError} If given nothing to combine.\n *\n * @example\n * or(eq(\"status\", \"open\"), not(exists(\"closedAt\")))\n */\nexport function or(expressions: readonly Expression[]): Expression\nexport function or(...expressions: Expression[]): Expression\nexport function or(...expressions: (Expression | readonly Expression[])[]): Expression {\n return combine(\"or\", expressions.flat())\n}\n\nfunction combine(kind: \"and\" | \"or\", expressions: Expression[]): Expression {\n if (expressions.length === 0) {\n throw new InvalidPredicateError(\n `${kind}() needs at least one expression. An empty ${kind === \"and\" ? \"conjunction\" : \"disjunction\"} ` +\n `has no query-language spelling — leave the ${kind}() out, or omit the filter entirely to match everything.`,\n )\n }\n // A one-element conjunction *is* its element; keeping the wrapper would only add parentheses.\n const only = expressions[0]\n if (expressions.length === 1 && only !== undefined) return only\n return node(\n kind === \"and\"\n ? { kind: \"and\", expressions: Object.freeze(expressions) }\n : { kind: \"or\", expressions: Object.freeze(expressions) },\n )\n}\n\n/**\n * Binding strength, tightest last — `NOT` binds tighter than `AND`, which binds tighter than `OR`.\n * A leaf never needs parentheses, so it sits above them all.\n */\nconst PRECEDENCE = { or: 1, and: 2, not: 3, leaf: 4 } as const\n\nfunction precedenceOf(expression: Expression): number {\n switch (expression.kind) {\n case \"or\":\n return PRECEDENCE.or\n case \"and\":\n return PRECEDENCE.and\n case \"not\":\n return PRECEDENCE.not\n default:\n return PRECEDENCE.leaf\n }\n}\n\n/** Renders a subexpression, parenthesising it only where the precedence would otherwise change it. */\nfunction nested(expression: Expression, minimum: number): string {\n const rendered = render(expression)\n return precedenceOf(expression) < minimum ? `(${rendered})` : rendered\n}\n\n/**\n * Renders an expression as the query string the node parses. Equivalent to `String(expression)`.\n *\n * Literals are always written tagged, even where the language would infer them: an untagged number\n * defaults to `i32`, but spelling it `i32(10)` keeps the exact-type assertion visible in the query\n * that actually went over the wire.\n */\nexport function render(expression: Expression): string {\n switch (expression.kind) {\n case \"comparison\":\n return `${expression.name} ${expression.operator} ${renderLiteral(expression.value)}`\n case \"startsWith\":\n return `${expression.name} STARTSWITH ${renderLiteral(expression.value)}`\n case \"exists\":\n return `EXISTS(${expression.name})`\n case \"typeOf\":\n return `TYPEOF(${expression.name}) = ${expression.tag}`\n case \"not\":\n return `NOT ${nested(expression.expression, PRECEDENCE.not)}`\n case \"and\":\n return expression.expressions.map((sub) => nested(sub, PRECEDENCE.and)).join(\" AND \")\n case \"or\":\n return expression.expressions.map((sub) => nested(sub, PRECEDENCE.or)).join(\" OR \")\n }\n}\n\n/** Renders a typed value as its query literal. `bool` is the one type written bare. */\nfunction renderLiteral(value: ArkivValue): string {\n switch (value.type) {\n case \"bool\":\n return value.value ? \"true\" : \"false\"\n case \"i32\":\n return `i32(${value.value})`\n case \"u64\":\n return `u64(${value.value})`\n case \"u256\":\n return `u256(${value.value})`\n case \"dec\":\n return `dec(${value.value})`\n case \"bytes32\":\n return `bytes32(${value.value})`\n case \"key\":\n return `key(${value.value})`\n case \"addr\":\n return `addr(${value.value})`\n case \"str\":\n // Single-quoted, with '' as the escape for a quote — the language has no backslash escapes.\n return `str('${value.value.replaceAll(\"'\", \"''\")}')`\n }\n}\n","import { validateAttributeName } from \"../attr\"\nimport type { Entity } from \"../types/entity\"\nimport type { RpcSelect } from \"../types/rpcSchema\"\n\n/**\n * The entity fields a query can select. Each maps one-to-one onto a field of {@link Entity}.\n *\n * `attributes` takes a third form beyond `true`/`false`: a map of names, to fetch only those.\n */\nexport type SelectionFields = {\n key?: boolean\n owner?: boolean\n creator?: boolean\n createdAt?: boolean\n updatedAt?: boolean\n expiresAt?: boolean\n creationFlags?: boolean\n contentType?: boolean\n payload?: boolean\n /** Attribute names and types, without their values. */\n attributeSchema?: boolean\n /** `true` for every attribute, or a map of names for a subset. */\n attributes?: boolean | Readonly<Record<string, boolean>>\n}\n\n/** The selection keys, in the order they are written into the RPC `select` object. */\nconst SELECTION_KEYS = [\n \"key\",\n \"owner\",\n \"creator\",\n \"createdAt\",\n \"updatedAt\",\n \"expiresAt\",\n \"creationFlags\",\n \"contentType\",\n \"payload\",\n \"attributeSchema\",\n \"attributes\",\n] as const satisfies readonly (keyof SelectionFields)[]\n\n/**\n * Requires at least one property of `T` to be present.\n * `{}` is not assignable, which is how `select({})` is rejected at compile time.\n */\ntype AtLeastOne<T, K extends keyof T = keyof T> = {\n [P in K]: Required<Pick<T, P>> & Partial<Omit<T, P>>\n}[K]\n\n/**\n * A non-empty entity selection — the object form accepted by `select()`. At least one field must be\n * selected.\n */\nexport type EntitySelection = AtLeastOne<SelectionFields>\n\n/** Argument accepted by `select()` — a non-empty {@link EntitySelection}, or `\"*\"` for everything. */\nexport type SelectArg = EntitySelection | \"*\"\n\n/** Whether a selection value asks for the field: `true`, or a non-empty map of attribute names. */\ntype Selected<V> = V extends false | undefined ? false : true\n\n/**\n * The result of a selection `S`: an {@link Entity} narrowed to exactly the selected fields (each\n * non-`undefined`). The shape is flat — `select({ owner: true })` yields `{ owner: Hex }`, read as\n * `entity.owner`. Accessing an unselected field is a compile error.\n *\n * `toText()` / `toJson()` operate on the payload, so they appear only when `payload` was selected —\n * calling them otherwise would always throw.\n */\nexport type ProjectedEntity<S> = {\n readonly [K in keyof S as Selected<S[K]> extends true ? K : never]: K extends keyof Entity\n ? NonNullable<Entity[K]>\n : never\n} & (S extends { payload: true } ? Pick<Entity, \"toText\" | \"toJson\"> : unknown)\n\n/** A selection of every field, used to type the result of `select()` / `select(\"*\")`. */\ntype AllSelected = { [K in (typeof SELECTION_KEYS)[number]]: true }\n\n/** The projected entity type when everything is selected — every field, plus the methods. */\nexport type FullEntity = ProjectedEntity<AllSelected>\n\n/**\n * Converts a {@link SelectArg} into the RPC `select` object.\n *\n * Every field is opt-in — anything not explicitly selected is sent as `false`, including the key.\n * Sending the full object rather than omitting the `false`s keeps what the SDK asked for legible in\n * a request log, and pins the answer even if a node's defaults change.\n *\n * `undefined` or `\"*\"` selects everything.\n *\n * @throws If the selection asks for nothing.\n *\n * @example\n * toRpcSelect() // everything\n * toRpcSelect({ key: true }) // only the key\n * toRpcSelect({ attributes: { projectId: true } }) // one named attribute\n */\nexport function toRpcSelect(selection?: SelectArg): RpcSelect {\n if (selection === undefined || selection === \"*\") {\n return Object.fromEntries(SELECTION_KEYS.map((field) => [field, true])) as RpcSelect\n }\n\n const select: RpcSelect = {}\n let any = false\n for (const field of SELECTION_KEYS) {\n const value = selection[field]\n if (field === \"attributes\" && typeof value === \"object\" && value !== null) {\n // A named subset. An empty map asks for no attributes at all, which is what `false` means.\n const names = Object.entries(value).filter(([, wanted]) => wanted)\n // Checked rather than forwarded: a name outside the grammar cannot exist on any entity, so\n // asking for one is a typo that would otherwise come back as a silently absent attribute.\n for (const [name] of names) validateAttributeName(name)\n select.attributes = names.length > 0 ? Object.fromEntries(names) : false\n any ||= names.length > 0\n continue\n }\n select[field] = value === true\n any ||= value === true\n }\n\n // Rejects both `{}` and a selection whose fields are all explicitly `false` — the type system\n // permits `select({ key: false })`, and the node would answer it with a page of empty objects.\n if (!any) {\n throw new Error(\n 'select() requires at least one field to be selected. Pass select() or select(\"*\") to select everything.',\n )\n }\n return select\n}\n","import { NoMoreResultsError } from \"../errors\"\nimport type { Entity } from \"../types/entity\"\n\n/** Fetches the page a cursor points at. */\ntype FetchPage<TEntity> = (cursor: string) => Promise<QueryResult<TEntity>>\n\n/**\n * One page of query results.\n *\n * A page is immutable: {@link next} returns the following page rather than mutating this one, so a\n * page that has been read stays readable.\n *\n * @typeParam TEntity - The shape of each entity, inferred from the selection by `client.select()`.\n *\n * @example\n * let page = await client.select({ key: true }).where(eq(\"category\", \"docs\")).limit(100).fetch()\n * while (page.hasNextPage()) {\n * page = await page.next()\n * }\n */\nexport class QueryResult<TEntity = Entity> {\n /** The entities on this page. */\n readonly entities: readonly TEntity[]\n /**\n * The block this page was read at.\n *\n * Every page of one walk reads the same block — the cursor is bound to it — so results cannot\n * shift under a paginating reader.\n */\n readonly blockNumber: bigint\n /** The cursor for the next page, or `undefined` when this was the last one. */\n readonly cursor: string | undefined\n\n private readonly fetchPage: FetchPage<TEntity>\n\n constructor(\n entities: readonly TEntity[],\n blockNumber: bigint,\n cursor: string | undefined,\n fetchPage: FetchPage<TEntity>,\n ) {\n this.entities = entities\n this.blockNumber = blockNumber\n this.cursor = cursor\n this.fetchPage = fetchPage\n }\n\n /**\n * Whether another page follows.\n *\n * The node omits the cursor once nothing remains, so this is an answer rather than the guess a\n * short final page would be — a full last page is reported correctly.\n */\n hasNextPage(): boolean {\n return this.cursor !== undefined\n }\n\n /**\n * Fetches the next page.\n *\n * @throws {NoMoreResultsError} If this was the last page — check {@link hasNextPage} first.\n * @throws {QueryError} If the node rejects the request; `kind === \"cursor\"` means the cursor\n * expired and the walk has to start again.\n */\n // Async so that \"no more pages\" arrives as a rejection like every other failure here, rather\n // than as a synchronous throw the caller's `.catch` would miss.\n async next(): Promise<QueryResult<TEntity>> {\n if (this.cursor === undefined) {\n throw new NoMoreResultsError()\n }\n return this.fetchPage(this.cursor)\n }\n}\n","import type { Hex } from \"viem\"\nimport { addr } from \"../attr\"\nimport type { ArkivClient } from \"../clients/baseClient\"\nimport type { Entity } from \"../types/entity\"\nimport type { RpcSelect } from \"../types/rpcSchema\"\nimport { type QueryRequest, runQuery } from \"./engine\"\nimport { InvalidPredicateError } from \"./errors\"\nimport { and, type Expression, eq, type or } from \"./expression\"\nimport { QueryResult } from \"./queryResult\"\nimport { type SelectArg, toRpcSelect } from \"./selection\"\n\n/**\n * Builds and runs a query.\n *\n * The selection is fixed when the builder is created and the filter is added by chaining, so the\n * result type is known before a single predicate is written. Create one with `client.select(...)`\n * rather than constructing it directly — that is what infers the entity shape from the selection.\n *\n * Every filter added with {@link where}, {@link ownedBy} and {@link createdBy} must hold; use\n * {@link or} for alternatives inside one call.\n *\n * @typeParam TEntity - The projected entity shape, inferred from the selection by `client.select()`.\n *\n * @example\n * import { createPublicClient } from \"@arkiv-network/sdk\"\n * import { i32 } from \"@arkiv-network/sdk/attr\"\n * import { tiramisu } from \"@arkiv-network/sdk/chains\"\n * import { eq, gte, or } from \"@arkiv-network/sdk/query\"\n * import { http } from \"viem\"\n *\n * const client = createPublicClient({ chain: tiramisu, transport: http() })\n *\n * const page = await client\n * .select({ key: true, attributes: true })\n * .where(gte(\"level\", i32(10)), or(eq(\"status\", \"open\"), eq(\"status\", \"review\")))\n * .ownedBy(owner)\n * .limit(100)\n * .fetch()\n *\n * for (const entity of page.entities) console.log(entity.key, entity.attributes)\n */\nexport class SelectQueryBuilder<TEntity = Entity> {\n private readonly _client: ArkivClient\n private readonly _select: RpcSelect\n private readonly _filters: Expression[] = []\n private _ownedBy: Expression | undefined\n private _createdBy: Expression | undefined\n private _limit: number | undefined\n private _cursor: string | undefined\n private _atBlock: bigint | undefined\n\n constructor(client: ArkivClient, selection?: SelectArg) {\n this._client = client\n this._select = toRpcSelect(selection)\n }\n\n /**\n * Adds filters. Everything passed here, and across repeated calls, must hold.\n *\n * @param expressions - The expressions, as separate arguments or one array.\n *\n * @example\n * builder.where(eq(\"category\", \"docs\"))\n * builder.where(gte(\"level\", i32(10)), lt(\"level\", i32(20)))\n * builder.where(or(eq(\"status\", \"open\"), not(exists(\"closedAt\"))))\n */\n where(expressions: readonly Expression[]): this\n where(...expressions: Expression[]): this\n where(...expressions: (Expression | readonly Expression[])[]): this {\n this._filters.push(...expressions.flat())\n return this\n }\n\n /**\n * Restricts the results to entities this account owns — shorthand for\n * `where(eq(\"$owner\", addr(owner)))`. Calling it again replaces the filter.\n *\n * @example\n * builder.ownedBy(\"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\")\n */\n ownedBy(owner: Hex): this {\n this._ownedBy = eq(\"$owner\", addr(owner))\n return this\n }\n\n /**\n * Restricts the results to entities this account created — shorthand for\n * `where(eq(\"$creator\", addr(creator)))`. Calling it again replaces the filter.\n *\n * The creator never changes; the owner does, so these differ after a transfer.\n */\n createdBy(creator: Hex): this {\n this._createdBy = eq(\"$creator\", addr(creator))\n return this\n }\n\n /**\n * Sets the page size, up to the node maximum of 200. Without it the node picks the page size.\n *\n * @example\n * builder.limit(100)\n */\n limit(limit: number): this {\n this._limit = limit\n return this\n }\n\n /**\n * Starts from a cursor returned by an earlier page.\n *\n * Pagination normally goes through {@link QueryResult.next}, which carries the cursor for you;\n * this is for resuming a walk in a later process. A cursor is bound to the query, block and\n * selection it came from, so it must be used with an identically-built query.\n */\n cursor(cursor: string): this {\n this._cursor = cursor\n return this\n }\n\n /**\n * Reads the state as of a given block rather than the head. The block must be within the range\n * the node retains.\n *\n * @example\n * builder.atBlock(1_297_000n)\n */\n atBlock(block: bigint): this {\n this._atBlock = block\n return this\n }\n\n /**\n * The query string this builder will send — the same text the node parses.\n *\n * @throws {InvalidPredicateError} If no filter has been added.\n */\n toString(): string {\n const filters = [...this._filters]\n if (this._ownedBy) filters.push(this._ownedBy)\n if (this._createdBy) filters.push(this._createdBy)\n if (filters.length === 0) {\n throw new InvalidPredicateError(\n \"A query needs at least one filter — the language has no spelling for 'match every \" +\n 'entity\". Add a where(...) predicate, or narrow by ownedBy(...) / createdBy(...).',\n )\n }\n return and(filters).toString()\n }\n\n /**\n * Runs the query and returns the first page.\n *\n * @throws {InvalidPredicateError} If no filter has been added.\n * @throws {QueryError} If the node rejects the query.\n *\n * @example\n * const page = await client.select({ key: true }).where(eq(\"category\", \"docs\")).fetch()\n * page.entities // this page\n * await page.next() // the next one\n */\n // Async so that a builder with no filter rejects like every other failure here, rather than\n // throwing synchronously out of a call the caller is awaiting.\n async fetch(): Promise<QueryResult<TEntity>> {\n // Snapshot the request here, so the pages of one walk are all pages of the *same* query. The\n // builder stays mutable and reusable; a cursor is bound to the query, block and selection it\n // was issued for, so re-reading a since-edited builder would page with a mismatched cursor.\n const request = Object.freeze({\n query: this.toString(),\n select: this._select,\n limit: this._limit,\n atBlock: this._atBlock,\n })\n return fetchPage<TEntity>(this._client, request, this._cursor)\n }\n\n /**\n * Walks every page, yielding one entity at a time.\n *\n * @example\n * for await (const entity of client.select({ key: true }).where(eq(\"category\", \"docs\"))) {\n * console.log(entity.key)\n * }\n */\n async *[Symbol.asyncIterator](): AsyncGenerator<TEntity> {\n let page: QueryResult<TEntity> | undefined = await this.fetch()\n while (page) {\n yield* page.entities\n page = page.hasNextPage() ? await page.next() : undefined\n }\n }\n}\n\n/**\n * Fetches one page and wires up the next.\n *\n * Free-standing rather than a method: a page holds this closure for its lifetime, and closing over\n * a frozen request keeps the builder — and everything it references — out of that reference.\n */\nasync function fetchPage<TEntity>(\n client: ArkivClient,\n request: Readonly<Omit<QueryRequest, \"cursor\">>,\n cursor: string | undefined,\n): Promise<QueryResult<TEntity>> {\n const response = await runQuery(client, { ...request, cursor })\n\n return new QueryResult<TEntity>(\n // Every row decodes to a full Entity; the static narrowing to the selected fields comes from\n // `client.select()`'s inference and nothing at runtime depends on it.\n response.entities as unknown as TEntity[],\n response.blockNumber,\n response.cursor,\n (next) => fetchPage<TEntity>(client, request, next),\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test"
|
|
2
|
-
import { encodeAttributes, encodeMutations, MAX_ATTRIBUTES, resolveAttributes } from "./attributes"
|
|
3
|
-
import { InvalidAttributeNameError, TooManyAttributesError } from "./errors"
|
|
4
|
-
import { isValidAttributeName, validateAttributeName } from "./names"
|
|
5
|
-
import { dec, i32, str, u256 } from "./values"
|
|
6
|
-
|
|
7
|
-
describe("resolveAttributes", () => {
|
|
8
|
-
it("types bare values and leaves tagged ones alone", () => {
|
|
9
|
-
const resolved = resolveAttributes({
|
|
10
|
-
level: 10,
|
|
11
|
-
balance: 1_000_000n,
|
|
12
|
-
name: "Bob",
|
|
13
|
-
flagged: true,
|
|
14
|
-
score: dec("3.5"),
|
|
15
|
-
})
|
|
16
|
-
expect(resolved.level).toEqual(i32(10))
|
|
17
|
-
expect(resolved.balance).toEqual(u256(1_000_000n))
|
|
18
|
-
expect(resolved.name).toEqual(str("Bob"))
|
|
19
|
-
expect(resolved.flagged.type).toBe("bool")
|
|
20
|
-
expect(resolved.score).toEqual(dec("3.5"))
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("reports the attribute count when there are too many", () => {
|
|
24
|
-
const tooMany = Object.fromEntries(
|
|
25
|
-
Array.from({ length: MAX_ATTRIBUTES + 1 }, (_, i) => [`a${i}`, i]),
|
|
26
|
-
)
|
|
27
|
-
expect(() => resolveAttributes(tooMany)).toThrow(TooManyAttributesError)
|
|
28
|
-
expect(() => resolveAttributes(tooMany)).toThrow(/at most 32 attributes, got 33/)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
describe("the attribute cap", () => {
|
|
33
|
-
const attributes = (count: number) =>
|
|
34
|
-
Object.fromEntries(Array.from({ length: count }, (_, i) => [`a${i}`, i]))
|
|
35
|
-
|
|
36
|
-
it("counts the payload and content-type cells, which the engine also counts", () => {
|
|
37
|
-
// Under the cap on its own, over it once the two system cells are appended. Letting this
|
|
38
|
-
// through would spend gas on a transaction the engine answers with TooManyAttributes(34, 32).
|
|
39
|
-
expect(() =>
|
|
40
|
-
encodeAttributes(attributes(MAX_ATTRIBUTES), {
|
|
41
|
-
payload: new Uint8Array([1]),
|
|
42
|
-
contentType: "application/json",
|
|
43
|
-
}),
|
|
44
|
-
).toThrow(/at most 32 attributes, got 34/)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it("leaves room for them rather than rejecting a full-but-legal operation", () => {
|
|
48
|
-
expect(
|
|
49
|
-
encodeAttributes(attributes(MAX_ATTRIBUTES - 2), {
|
|
50
|
-
payload: new Uint8Array([1]),
|
|
51
|
-
contentType: "application/json",
|
|
52
|
-
}),
|
|
53
|
-
).toHaveLength(MAX_ATTRIBUTES)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it("counts tombstones in a patch — a removal occupies a cell like any other", () => {
|
|
57
|
-
expect(() =>
|
|
58
|
-
encodeMutations({
|
|
59
|
-
set: attributes(MAX_ATTRIBUTES - 1),
|
|
60
|
-
unset: ["gone_a", "gone_b"],
|
|
61
|
-
}),
|
|
62
|
-
).toThrow(/at most 32 attributes, got 33/)
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe("encodeAttributes", () => {
|
|
67
|
-
it("sorts strictly ascending by the encoded name, whatever order they were written in", () => {
|
|
68
|
-
const encoded = encodeAttributes({ zebra: 1, apple: 2, mango: 3 })
|
|
69
|
-
const names = encoded.map((attribute) => attribute.name)
|
|
70
|
-
expect(names).toEqual([...names].sort())
|
|
71
|
-
expect(encoded).toHaveLength(3)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it("carries each value's typeId", () => {
|
|
75
|
-
const ids = encodeAttributes({ level: i32(1), score: dec("1"), tag: "x" }).map((a) => a.typeId)
|
|
76
|
-
expect([...ids].sort()).toEqual([2, 5, 8]) // i32, dec, str
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it("accepts an empty attribute set", () => {
|
|
80
|
-
expect(encodeAttributes({})).toEqual([])
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe("validateAttributeName", () => {
|
|
85
|
-
it("accepts the Ident32 grammar, in either case", () => {
|
|
86
|
-
for (const name of ["a", "tag", "Tag", "projectId", "num_attr", "a.b-c_D9", "a".repeat(32)]) {
|
|
87
|
-
expect(isValidAttributeName(name)).toBe(true)
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it("treats names as case-sensitive, so two casings are two attributes", () => {
|
|
92
|
-
const resolved = resolveAttributes({ level: 1, Level: 2 })
|
|
93
|
-
expect(resolved.level).toEqual(i32(1))
|
|
94
|
-
expect(resolved.Level).toEqual(i32(2))
|
|
95
|
-
expect(encodeAttributes({ level: 1, Level: 2 })).toHaveLength(2)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it("rejects names outside the grammar, pointing at the offending character", () => {
|
|
99
|
-
expect(() => validateAttributeName("")).toThrow(InvalidAttributeNameError)
|
|
100
|
-
expect(() => validateAttributeName("1tag")).toThrow(/starts with "1"/)
|
|
101
|
-
expect(() => validateAttributeName("_tag")).toThrow(/starts with "_"/)
|
|
102
|
-
expect(() => validateAttributeName("has space")).toThrow(/" " at position 3/)
|
|
103
|
-
expect(() => validateAttributeName("héllo")).toThrow(/"é" at position 1/)
|
|
104
|
-
expect(() => validateAttributeName("a:b")).toThrow(/":" at position 1/)
|
|
105
|
-
expect(() => validateAttributeName("a".repeat(33))).toThrow(/33 bytes long/)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it("reserves $ for the system attributes the engine sets", () => {
|
|
109
|
-
expect(() => validateAttributeName("$owner")).toThrow(/reserved for system attributes/)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it("rejects names the query language could never parse back, whatever their casing", () => {
|
|
113
|
-
for (const reserved of ["and", "OR", "Not", "key", "str", "u256", "EXISTS", "Bytes32"]) {
|
|
114
|
-
expect(() => validateAttributeName(reserved)).toThrow(/reserved by the query language/)
|
|
115
|
-
}
|
|
116
|
-
// Only the exact word is reserved — a name containing it is fine.
|
|
117
|
-
expect(isValidAttributeName("android")).toBe(true)
|
|
118
|
-
expect(isValidAttributeName("sortKey")).toBe(true)
|
|
119
|
-
})
|
|
120
|
-
})
|
package/src/attr/attributes.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { type AbiAttribute, encodeAbiAttribute, encodeTombstone } from "./codec"
|
|
2
|
-
import { ConflictingMutationError, InvalidValueError, TooManyAttributesError } from "./errors"
|
|
3
|
-
import { validateAttributeName } from "./names"
|
|
4
|
-
import type { AnyArkivValue, ArkivValue, TypeTag } from "./types"
|
|
5
|
-
import { makeValue } from "./types"
|
|
6
|
-
import { str, toValue, type ValueInput } from "./values"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The most attribute cells one operation may carry.
|
|
10
|
-
*/
|
|
11
|
-
export const MAX_ATTRIBUTES = 32
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The largest payload an entity may carry, in bytes. The payload travels as the `$payload` system
|
|
15
|
-
* cell, so this is a bound on that one attribute rather than on the operation as a whole.
|
|
16
|
-
*/
|
|
17
|
-
export const MAX_PAYLOAD_BYTES = 128 * 1024
|
|
18
|
-
|
|
19
|
-
/** The system attribute holding an entity's opaque payload. */
|
|
20
|
-
const PAYLOAD_CELL = "$payload"
|
|
21
|
-
/** The system attribute holding an entity's content type. */
|
|
22
|
-
const CONTENT_TYPE_CELL = "$contentType"
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The attributes written on an entity, as a plain object keyed by attribute name.
|
|
26
|
-
*
|
|
27
|
-
* Values may be tagged ({@link ArkivValue}) or bare where the type is unambiguous — see
|
|
28
|
-
* {@link toValue} for the bare-form defaults.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* const attributes = {
|
|
32
|
-
* level: i32(10),
|
|
33
|
-
* balance: u256(1_000_000n),
|
|
34
|
-
* score: dec("3.5"),
|
|
35
|
-
* parent: key(parentKey),
|
|
36
|
-
* name: "Bob", // bare string -> str
|
|
37
|
-
* flagged: true, // bare boolean -> bool
|
|
38
|
-
* } satisfies AttributeInputs
|
|
39
|
-
*/
|
|
40
|
-
export type AttributeInputs = Readonly<Record<string, ValueInput>>
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The attributes read back from an entity: the same shape, with every value typed.
|
|
44
|
-
*
|
|
45
|
-
* A value read from here drops straight back into an {@link AttributeInputs} — the vocabulary is the
|
|
46
|
-
* same in both directions.
|
|
47
|
-
*/
|
|
48
|
-
export type Attributes = Readonly<Record<string, AnyArkivValue>>
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The names and types of an entity's attributes, with no values — what the `attributeSchema`
|
|
52
|
-
* projection returns.
|
|
53
|
-
*
|
|
54
|
-
* Useful for asking what shape the data has without paying to transfer it, and for telling apart
|
|
55
|
-
* two entities that use the same name with different types.
|
|
56
|
-
*/
|
|
57
|
-
export type AttributeSchema = Readonly<Record<string, TypeTag>>
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The typed form of an attribute map: names validated, bare values resolved to their default type.
|
|
61
|
-
*
|
|
62
|
-
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
63
|
-
* @throws {MissingValueError} If a value is `undefined` or `null` — omit the name instead.
|
|
64
|
-
* @throws {InvalidValueError} If a value does not fit the type it names or defaults to.
|
|
65
|
-
* @throws {TooManyAttributesError} If there are more than {@link MAX_ATTRIBUTES} attributes.
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* resolveAttributes({ level: 10, name: "Bob" })
|
|
69
|
-
* // { level: { type: "i32", value: 10 }, name: { type: "str", value: "Bob" } }
|
|
70
|
-
*/
|
|
71
|
-
export function resolveAttributes(attributes: AttributeInputs): Record<string, ArkivValue> {
|
|
72
|
-
const entries = Object.entries(attributes)
|
|
73
|
-
if (entries.length > MAX_ATTRIBUTES) {
|
|
74
|
-
throw new TooManyAttributesError(entries.length, MAX_ATTRIBUTES)
|
|
75
|
-
}
|
|
76
|
-
const resolved: Record<string, ArkivValue> = {}
|
|
77
|
-
for (const [name, input] of entries) {
|
|
78
|
-
validateAttributeName(name)
|
|
79
|
-
resolved[name] = toValue(input, name)
|
|
80
|
-
}
|
|
81
|
-
return resolved
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The payload and content type, which travel to the engine as system attributes.
|
|
86
|
-
*
|
|
87
|
-
* They are separate fields on the SDK's surface because that is what they are to an application —
|
|
88
|
-
* an entity's contents and its type — but on the wire an operation carries no dedicated field for
|
|
89
|
-
* either: `$payload` is the one `bytes`-typed cell, and `$contentType` is a `str`.
|
|
90
|
-
*/
|
|
91
|
-
export type SystemCells = {
|
|
92
|
-
payload?: Uint8Array | undefined
|
|
93
|
-
contentType?: string | undefined
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Encodes an attribute map, plus the payload and content-type system cells, into the ABI attribute
|
|
98
|
-
* array an operation carries.
|
|
99
|
-
*
|
|
100
|
-
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
101
|
-
* @throws {MissingValueError} If a value is `undefined` or `null`.
|
|
102
|
-
* @throws {InvalidValueError} If a value does not fit its type.
|
|
103
|
-
* @throws {TooManyAttributesError} If there are more than {@link MAX_ATTRIBUTES} attributes.
|
|
104
|
-
*/
|
|
105
|
-
export function encodeAttributes(
|
|
106
|
-
attributes: AttributeInputs = {},
|
|
107
|
-
system: SystemCells = {},
|
|
108
|
-
): AbiAttribute[] {
|
|
109
|
-
const cells: [string, AnyArkivValue][] = Object.entries(resolveAttributes(attributes))
|
|
110
|
-
|
|
111
|
-
// Written whenever the field is given, empty or not: an empty payload is a payload, and dropping
|
|
112
|
-
// it would mean the entity does not hold what the caller passed. Absence is for the operations
|
|
113
|
-
// that genuinely leave a cell alone, not for a value that happens to be empty.
|
|
114
|
-
if (system.payload !== undefined) {
|
|
115
|
-
if (system.payload.length > MAX_PAYLOAD_BYTES) {
|
|
116
|
-
throw new InvalidValueError(
|
|
117
|
-
"bytes",
|
|
118
|
-
`<${system.payload.length} bytes>`,
|
|
119
|
-
`${system.payload.length} bytes exceeds the ${MAX_PAYLOAD_BYTES}-byte payload limit`,
|
|
120
|
-
"Store the bulk elsewhere and keep a reference to it on the entity.",
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
cells.push([PAYLOAD_CELL, makeValue("bytes", toHexBytes(system.payload))])
|
|
124
|
-
}
|
|
125
|
-
if (system.contentType !== undefined) {
|
|
126
|
-
cells.push([CONTENT_TYPE_CELL, str(system.contentType)])
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (cells.length > MAX_ATTRIBUTES) {
|
|
130
|
-
throw new TooManyAttributesError(cells.length, MAX_ATTRIBUTES)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return cells.map(([name, value]) => encodeAbiAttribute(name, value)).sort(byName)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export type MutationInputs = SystemCells & {
|
|
137
|
-
/** Attributes to write. Existing ones are overwritten; new ones are added. */
|
|
138
|
-
set?: AttributeInputs | undefined
|
|
139
|
-
/** Attribute names to remove. Each becomes a tombstone on the wire. */
|
|
140
|
-
unset?: readonly string[] | undefined
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Encodes a patch's mutations into the ABI attribute array the operation carries.
|
|
145
|
-
*
|
|
146
|
-
* @throws {ConflictingMutationError} If a name appears in both `set` and `unset`.
|
|
147
|
-
* @throws {InvalidAttributeNameError} If a name violates the attribute-name grammar.
|
|
148
|
-
* @throws {MissingValueError} If a value is `undefined` or `null`.
|
|
149
|
-
* @throws {InvalidValueError} If a value does not fit its type.
|
|
150
|
-
* @throws {TooManyAttributesError} If the mutations exceed {@link MAX_ATTRIBUTES}.
|
|
151
|
-
* @throws {TypeError} If `unset` is not an array.
|
|
152
|
-
*/
|
|
153
|
-
export function encodeMutations({
|
|
154
|
-
set,
|
|
155
|
-
unset,
|
|
156
|
-
payload,
|
|
157
|
-
contentType,
|
|
158
|
-
}: MutationInputs): AbiAttribute[] {
|
|
159
|
-
const written = encodeAttributes(set, { payload, contentType })
|
|
160
|
-
const tombstones = resolveUnset(unset, set).map(encodeTombstone)
|
|
161
|
-
const mutations = [...written, ...tombstones]
|
|
162
|
-
if (mutations.length > MAX_ATTRIBUTES) {
|
|
163
|
-
throw new TooManyAttributesError(mutations.length, MAX_ATTRIBUTES)
|
|
164
|
-
}
|
|
165
|
-
return mutations.sort(byName)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function resolveUnset(unset: readonly string[] = [], set: AttributeInputs = {}): string[] {
|
|
169
|
-
if (!Array.isArray(unset)) {
|
|
170
|
-
throw new TypeError(`"unset" must be an array of attribute names`)
|
|
171
|
-
}
|
|
172
|
-
const names = new Set(unset)
|
|
173
|
-
for (const name of names) {
|
|
174
|
-
validateAttributeName(name)
|
|
175
|
-
if (Object.hasOwn(set, name)) {
|
|
176
|
-
throw new ConflictingMutationError(name)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return [...names]
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* The engine requires attributes **strictly ascending by their `bytes32` name**: that ordering is
|
|
184
|
-
* what the canonical serialization hashes into the state root, and it doubles as the uniqueness
|
|
185
|
-
* check.
|
|
186
|
-
*/
|
|
187
|
-
function byName(a: AbiAttribute, b: AbiAttribute): number {
|
|
188
|
-
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Raw bytes as lowercase `0x` hex, without viem's hex-string ambiguity. */
|
|
192
|
-
function toHexBytes(bytes: Uint8Array): `0x${string}` {
|
|
193
|
-
let out = "0x"
|
|
194
|
-
for (const byte of bytes) out += byte.toString(16).padStart(2, "0")
|
|
195
|
-
return out as `0x${string}`
|
|
196
|
-
}
|
package/src/attr/index.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export type { AttributeInputs, AttributeSchema, Attributes } from "./attributes"
|
|
2
|
-
export { MAX_ATTRIBUTES, MAX_PAYLOAD_BYTES } from "./attributes"
|
|
3
|
-
export { DECIMAL_SCALE, DECIMAL_UNIT } from "./decimal"
|
|
4
|
-
export {
|
|
5
|
-
ConflictingMutationError,
|
|
6
|
-
InvalidAttributeNameError,
|
|
7
|
-
InvalidValueError,
|
|
8
|
-
MissingValueError,
|
|
9
|
-
TooManyAttributesError,
|
|
10
|
-
UnknownAttributeTypeError,
|
|
11
|
-
UntypedValueError,
|
|
12
|
-
} from "./errors"
|
|
13
|
-
export { isValidAttributeName, MAX_NAME_BYTES, validateAttributeName } from "./names"
|
|
14
|
-
export type {
|
|
15
|
-
AddrValue,
|
|
16
|
-
AnyArkivValue,
|
|
17
|
-
ArkivValue,
|
|
18
|
-
BoolValue,
|
|
19
|
-
Bytes32Value,
|
|
20
|
-
BytesValue,
|
|
21
|
-
DecValue,
|
|
22
|
-
I32Value,
|
|
23
|
-
KeyValue,
|
|
24
|
-
StrValue,
|
|
25
|
-
TypeTag,
|
|
26
|
-
U64Value,
|
|
27
|
-
U256Value,
|
|
28
|
-
UserTypeTag,
|
|
29
|
-
} from "./types"
|
|
30
|
-
export { isArkivValue } from "./types"
|
|
31
|
-
export type { ValueInput } from "./values"
|
|
32
|
-
export {
|
|
33
|
-
addr,
|
|
34
|
-
bool,
|
|
35
|
-
bytes32,
|
|
36
|
-
dec,
|
|
37
|
-
decFromUnits,
|
|
38
|
-
decUnits,
|
|
39
|
-
I32_MAX,
|
|
40
|
-
I32_MIN,
|
|
41
|
-
i32,
|
|
42
|
-
key,
|
|
43
|
-
MAX_STRING_BYTES,
|
|
44
|
-
str,
|
|
45
|
-
toValue,
|
|
46
|
-
U64_MAX,
|
|
47
|
-
U256_MAX,
|
|
48
|
-
u64,
|
|
49
|
-
u256,
|
|
50
|
-
} from "./values"
|
package/src/types/mimeTypes.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
export type MimeType =
|
|
2
|
-
| "text/plain"
|
|
3
|
-
| "text/html"
|
|
4
|
-
| "text/css"
|
|
5
|
-
| "text/csv"
|
|
6
|
-
| "text/xml"
|
|
7
|
-
| "text/javascript"
|
|
8
|
-
| "application/json"
|
|
9
|
-
| "application/xml"
|
|
10
|
-
| "application/pdf"
|
|
11
|
-
| "application/zip"
|
|
12
|
-
| "application/gzip"
|
|
13
|
-
| "application/octet-stream"
|
|
14
|
-
| "application/javascript"
|
|
15
|
-
| "application/x-www-form-urlencoded"
|
|
16
|
-
| "image/png"
|
|
17
|
-
| "image/jpeg"
|
|
18
|
-
| "image/webp"
|
|
19
|
-
| "image/gif"
|
|
20
|
-
| "image/svg+xml"
|
|
21
|
-
| "audio/mpeg"
|
|
22
|
-
| "audio/wav"
|
|
23
|
-
| "audio/ogg"
|
|
24
|
-
| "video/mp4"
|
|
25
|
-
| "video/webm"
|
|
26
|
-
| "video/ogg"
|
|
27
|
-
| "multipart/form-data"
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test"
|
|
2
|
-
import { compress, decompress } from "./compression"
|
|
3
|
-
|
|
4
|
-
describe("compression", () => {
|
|
5
|
-
it("should compress data", async () => {
|
|
6
|
-
const originalData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
7
|
-
const compressed = await compress(originalData)
|
|
8
|
-
|
|
9
|
-
expect(compressed).toBeInstanceOf(Uint8Array)
|
|
10
|
-
expect(compressed.length).toBeGreaterThan(0)
|
|
11
|
-
// Compressed data should be different from original
|
|
12
|
-
expect(compressed).not.toEqual(originalData)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it("should compress data with large payload into smaller size", async () => {
|
|
16
|
-
const originalData = new Uint8Array(1000).fill(1)
|
|
17
|
-
const compressed = await compress(originalData)
|
|
18
|
-
|
|
19
|
-
expect(compressed).toBeInstanceOf(Uint8Array)
|
|
20
|
-
expect(compressed.length).toBeGreaterThan(0)
|
|
21
|
-
expect(compressed.length).toBeLessThan(originalData.length)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it("should decompress data", async () => {
|
|
25
|
-
const originalData = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
26
|
-
const compressed = await compress(originalData)
|
|
27
|
-
const decompressed = await decompress(compressed)
|
|
28
|
-
|
|
29
|
-
expect(decompressed).toBeInstanceOf(Uint8Array)
|
|
30
|
-
expect(decompressed).toEqual(originalData)
|
|
31
|
-
})
|
|
32
|
-
})
|