OSRS Wiki Bucket Builder

A strictly typed, zero-dependency TypeScript query builder for the Old School RuneScape Wiki Bucket API.

โœ“ 100% Type Safe โšก Zero Dependencies ๐Ÿ“ฆ ESM Ready
npm install @dava96/osrs-wiki-bucket-builder

Quick Start

import { bucket, BucketResponse } from '@dava96/osrs-wiki-bucket-builder';
import type { InferBucketResult } from '@dava96/osrs-wiki-bucket-builder';

const query = bucket('exchange')
    .select('id', 'name', 'value')
    .where('name', 'Abyssal whip');

// .toUrl() generates the full API URL, ready to fetch
const raw = await fetch(query.toUrl()).then(r => r.json());

// BucketResponse.from() infers the result type from the query
const response = BucketResponse.from(query, raw);
const whip = response.first();
console.log(whip?.name);  // โœ… typed as string
console.log(whip?.value); // โœ… typed as number
โ–ถ Run this query

Core Concepts

What are Buckets? Buckets are structured data tables exposed by the OSRS Wiki through the Bucket extension. Each bucket (e.g. exchange, infobox_item, storeline) contains rows and fields, similar to a SQL table.

What does this library do? It provides a fluent TypeScript API that generates the Lua query strings the Wiki API expects. You chain methods like .select(), .where(), and .join(), and the builder outputs a correctly formatted Lua string. It never makes network requests โ€” you handle fetching yourself.

How does type safety work? A sync script fetches the schema of every bucket from the Wiki and generates TypeScript interfaces. Your IDE will autocomplete bucket names and catch invalid field references at compile time.

Guide

Selecting Fields

Use .select() to pick which fields to retrieve. Supports wildcards (*, alias.*) which are expanded client-side into explicit field lists.

const query = bucket('exchange')
    .select('name', 'value', 'limit')
    .limit(5)
    .run();
โ–ถ Run this query

Filtering with Where

The .where() method supports equality, comparison operators, and chaining for implicit AND.

Equality (implicit =)

bucket('exchange')
    .select('name', 'value')
    .where('name', 'Dragon scimitar')
    .run();
โ–ถ Run this query

Comparison Operators

Supported: =, !=, >, <, >=, <=

bucket('exchange')
    .select('name', 'value')
    .where('value', '>', 1000000)
    .limit(10)
    .run();
โ–ถ Run this query

Convenience Filters

.whereNot(field, value) โ€” exclude matches

bucket('exchange')
    .select('name', 'value')
    .whereNot('name', 'Coins')
    .where('value', '>', 0)
    .limit(10)
    .run();
โ–ถ Run this query

.whereNull() / .whereNotNull() โ€” null checks

bucket('infobox_item')
    .select('item_name', 'weight')
    .whereNotNull('weight')
    .limit(10)
    .run();
โ–ถ Run this query

.whereBetween(field, [min, max]) โ€” inclusive range

bucket('exchange')
    .select('name', 'value')
    .whereBetween('value', [10000, 100000])
    .limit(10)
    .run();
โ–ถ Run this query

.whereIn(field, values) โ€” match any value

bucket('exchange')
    .select('name', 'value')
    .whereIn('name', ['Bronze axe', 'Iron axe', 'Steel axe'])
    .run();
โ–ถ Run this query

Combining Conditions

For complex logic, use the Bucket helper object:

import { bucket, Bucket } from '@dava96/osrs-wiki-bucket-builder';

Bucket.Or(...) โ€” match any condition

bucket('exchange')
    .select('name', 'value')
    .where(Bucket.Or(
        ['name', 'Bronze axe'],
        ['name', 'Iron axe'],
        ['name', 'Steel axe']
    ))
    .run();
โ–ถ Run this query

Bucket.And(...) โ€” all conditions must match

bucket('exchange')
    .select('name', 'value')
    .where(Bucket.And(
        ['value', '>', 1000],
        ['value', '<', 50000]
    ))
    .run();
โ–ถ Run this query

Bucket.Not(...) โ€” negate a condition

bucket('exchange')
    .select('name', 'value')
    .where(Bucket.Not(['name', 'Coins']))
    .run();
โ–ถ Run this query

Bucket.Null() โ€” represents a null value

bucket('infobox_item')
    .select('item_name', 'weight')
    .where('weight', '!=', Bucket.Null())
    .run();
โ–ถ Run this query

Joining Buckets

Join two or more buckets to combine data from different sources, similar to a SQL JOIN.

Basic join

bucket('infobox_item')
    .join('exchange', 'item_name', 'name')
    .select('item_name', 'weight', 'exchange.value')
    .limit(5)
    .run();
โ–ถ Run this query

Join with alias

Aliases are resolved to real bucket names in the generated Lua:

bucket('infobox_item')
    .join('exchange', 'ex', 'item_name', 'name')
    .select('item_name', 'ex.value', 'ex.limit')
    .where('ex.value', '>', 100000)
    .limit(10)
    .run();
โ–ถ Run this query

Multiple joins

Join three buckets to combine item info, GE prices, and shop data:

bucket('infobox_item')
    .join('exchange', 'item_name', 'name')
    .join('storeline', 'item_name', 'sold_item')
    .select(
        'item_name', 'weight',
        'exchange.value',
        'storeline.sold_by', 'storeline.store_sell_price'
    )
    .limit(5)
    .run();
โ–ถ Run this query

Ordering & Pagination

.orderBy(field, direction)

Sort by a selected field. The field must appear in a prior .select() call.

.paginate(page, perPage)

A helper that computes .limit() and .offset() from a 1-based page number:

const page2 = bucket('exchange')
    .select('name', 'value')
    .where('value', '>', 0)
    .orderBy('value', 'desc')
    .paginate(2, 25)
    .run();
โ–ถ Run page 2

.first()

Shorthand for .limit(1) โ€” grab just the top result:

const top = bucket('exchange')
    .select('name', 'value')
    .orderBy('value', 'desc')
    .first()
    .run();

Conditional Logic โ€” .when()

Conditionally apply query modifications based on runtime values:

const isMembers = true;
const query = bucket('infobox_item')
    .select('item_name', 'value', 'members')
    .when(isMembers, (q) => q.where('members', true))
    .limit(10)
    .run();

Reusing Queries โ€” .clone()

Creates an independent deep copy. Changes to the clone don't affect the original:

const base = bucket('exchange')
    .select('name', 'value')
    .where('value', '>', 0);

const topExpensive = base.clone().orderBy('value', 'desc').limit(5);
const topCheap = base.clone().orderBy('value', 'asc').limit(5);

Executing Queries

.run() โ€” URL-encoded output (default)

const query = bucket('exchange').select('name', 'value').run();
const url = `https://oldschool.runescape.wiki/api.php?action=bucket&format=json&query=${query}`;

.run({ encodeURI: false }) โ€” raw Lua output

const lua = bucket('exchange').select('name', 'value').run({ encodeURI: false });
// bucket('exchange').select('name', 'value').run()

.printSQL() โ€” raw Lua string

Equivalent to .run({ encodeURI: false }):

const lua = bucket('exchange').select('name', 'value').printSQL();

BucketResponse โ€” response wrapper

Wraps the raw API response with convenient accessors:

import { bucket, BucketResponse } from '@dava96/osrs-wiki-bucket-builder';

const query = bucket('exchange').select('name', 'value').first();
const raw = await fetch(query.toUrl()).then(r => r.json());
const response = new BucketResponse(raw);

response.results  // Array of matching rows
response.first()  // First result or undefined
response.query    // The Lua query echoed by the API
response.error    // Error message if the query failed

BucketResponse.from() โ€” typed responses from queries

Automatically infers the result type from the query builder โ€” no manual type parameter needed:

const query = bucket('exchange').select('name', 'value');
const raw = await fetch(query.toUrl()).then(r => r.json());
const response = BucketResponse.from(query, raw);

response.first()?.name;  // โœ… typed as string
response.first()?.value; // โœ… typed as number

InferBucketResult โ€” extract the result type

Use InferBucketResult<typeof query> to extract the inferred row type without executing the query:

import type { InferBucketResult } from '@dava96/osrs-wiki-bucket-builder';

const query = bucket('exchange').select('id', 'name', 'value');
type ExchangeRow = InferBucketResult<typeof query>;
// { id: number; name: string; value: number; page_name: string; page_name_sub: string }

.toUrl() โ€” generate the full API URL

Generates the complete Wiki API URL, ready to pass to fetch():

const url = bucket('exchange')
    .select('name', 'value')
    .where('value', '>', 100000)
    .limit(10)
    .toUrl();

const data = await fetch(url).then(r => r.json());

Full Example

Multi-join with aliases, mixed conditions, ordering, and pagination โ€” all in one query:

import { bucket, Bucket } from '@dava96/osrs-wiki-bucket-builder';

const query = bucket('infobox_item')
    .join('exchange', 'ex', 'item_name', 'name')
    .join('storeline', 'shop', 'item_name', 'sold_item')
    .select(
        'item_name', 'weight',
        'ex.value', 'ex.limit',
        'shop.sold_by', 'shop.store_sell_price'
    )
    .where('ex.value', '>', 1000)
    .whereNotNull('shop.sold_by')
    .orderBy('ex.value', 'desc')
    .limit(10)
    .run();
โ–ถ Run this query

API Reference

MethodDescription
bucket(name)Creates a new query builder for the given bucket
.select(...fields)Picks fields to retrieve. Supports dot-notation and wildcards
.where(field, value)Filters by equality
.where(field, op, value)Filters with a comparison operator
.whereNot(field, value)Shorthand for .where(field, '!=', value)
.whereNull(field)Filters for NULL values
.whereNotNull(field)Filters for non-NULL values
.whereBetween(field, [a, b])Inclusive range filter
.whereIn(field, values)Matches any value from the list
.join(bucket, src, target)Joins another bucket
.join(bucket, alias, src, target)Joins with an alias
.orderBy(field, direction)Sorts by 'asc' or 'desc'
.limit(n)Sets max rows (1โ€“5000, default 500)
.offset(n)Sets row offset for pagination
.paginate(page, perPage)Computes limit/offset from page number
.first()Shorthand for .limit(1)
.when(cond, fn)Conditionally applies fn
.clone()Deep copies the builder
.run(options?)Returns the Lua query string (URI-encoded by default)
.printSQL()Returns the raw Lua query string
.toUrl()Generates the full Wiki API URL, ready to fetch()

Bucket Helpers

HelperDescription
Bucket.And(...conditions)Logical AND
Bucket.Or(...conditions)Logical OR
Bucket.Not(condition)Logical NOT
Bucket.Null()Represents a NULL value

Resources

Bucket Usage Guide ยท Bucket API ยท Browse All Buckets ยท GitHub ยท Icons Provided by osrs-icons