A strictly typed, zero-dependency TypeScript query builder for the Old School RuneScape Wiki Bucket API.
npm install @dava96/osrs-wiki-bucket-builder
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
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.
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
The .where() method supports equality, comparison operators, and chaining for implicit AND.
=)bucket('exchange')
.select('name', 'value')
.where('name', 'Dragon scimitar')
.run();
โถ Run this query
Supported: =, !=, >, <, >=, <=
bucket('exchange')
.select('name', 'value')
.where('value', '>', 1000000)
.limit(10)
.run();
โถ Run this query
.whereNot(field, value) โ exclude matchesbucket('exchange')
.select('name', 'value')
.whereNot('name', 'Coins')
.where('value', '>', 0)
.limit(10)
.run();
โถ Run this query
.whereNull() / .whereNotNull() โ null checksbucket('infobox_item')
.select('item_name', 'weight')
.whereNotNull('weight')
.limit(10)
.run();
โถ Run this query
.whereBetween(field, [min, max]) โ inclusive rangebucket('exchange')
.select('name', 'value')
.whereBetween('value', [10000, 100000])
.limit(10)
.run();
โถ Run this query
.whereIn(field, values) โ match any valuebucket('exchange')
.select('name', 'value')
.whereIn('name', ['Bronze axe', 'Iron axe', 'Steel axe'])
.run();
โถ Run this query
For complex logic, use the Bucket helper object:
import { bucket, Bucket } from '@dava96/osrs-wiki-bucket-builder';
Bucket.Or(...) โ match any conditionbucket('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 matchbucket('exchange')
.select('name', 'value')
.where(Bucket.And(
['value', '>', 1000],
['value', '<', 50000]
))
.run();
โถ Run this query
Bucket.Not(...) โ negate a conditionbucket('exchange')
.select('name', 'value')
.where(Bucket.Not(['name', 'Coins']))
.run();
โถ Run this query
Bucket.Null() โ represents a null valuebucket('infobox_item')
.select('item_name', 'weight')
.where('weight', '!=', Bucket.Null())
.run();
โถ Run this query
Join two or more buckets to combine data from different sources, similar to a SQL JOIN.
bucket('infobox_item')
.join('exchange', 'item_name', 'name')
.select('item_name', 'weight', 'exchange.value')
.limit(5)
.run();
โถ Run this query
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
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
.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();
.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();
.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);
.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 outputconst lua = bucket('exchange').select('name', 'value').run({ encodeURI: false });
// bucket('exchange').select('name', 'value').run()
.printSQL() โ raw Lua stringEquivalent to .run({ encodeURI: false }):
const lua = bucket('exchange').select('name', 'value').printSQL();
BucketResponse โ response wrapperWraps 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 queriesAutomatically 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 typeUse 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 URLGenerates 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());
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
| Method | Description |
|---|---|
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() |
| Helper | Description |
|---|---|
Bucket.And(...conditions) | Logical AND |
Bucket.Or(...conditions) | Logical OR |
Bucket.Not(condition) | Logical NOT |
Bucket.Null() | Represents a NULL value |
Bucket Usage Guide ยท Bucket API ยท Browse All Buckets ยท GitHub ยท Icons Provided by osrs-icons