npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dava96/osrs-wiki-bucket-builder

v1.0.8

Published

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

Downloads

504

Readme

OSRS Wiki Bucket Builder

npm npm downloads license GitHub stars

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

Generates valid Lua query strings that can be executed via the Wiki's action=bucket API endpoint, with full TypeScript type safety derived from the Wiki's own schema definitions.

Features

  • Strict Typing — TypeScript definitions generated from Special:AllPages ensure only valid bucket names and fields compile.
  • Inferred Response Types.select(), .join(), and .where() constrain fields to valid names and accumulate a result type that precisely describes each row.
  • Fluent API — Chain .select(), .join(), .where(), .orderBy(), .limit(), .offset().
  • Join Aliases — Multi-bucket joins with alias support and dot-notation (shop.price).
  • Wildcard Expansion — Client-side * and alias.* expansion to strict field lists.
  • Zero Runtime Dependencies — Generates query strings without making network requests.

Installation

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? This library 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? The scripts/sync_buckets.ts script fetches the schema of every bucket from the Wiki and generates TypeScript interfaces in src/generated/definitions.ts. This means 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. Without .select(), the API returns all fields.

const query = bucket('exchange')
    .select('name', 'value', 'limit')
    .limit(5)
    .run();

▶ Run this query

Wildcards

You can use * to select all fields from the main bucket, or alias.* from a joined bucket:

// All fields from exchange
bucket('exchange').select('*').limit(5).run();

// All fields from a joined bucket
bucket('infobox_item')
    .join('exchange', 'item_name', 'name')
    .select('item_name', 'exchange.*')
    .limit(5)
    .run();

Wildcards are expanded client-side into explicit field lists using the generated schema.


Filtering with Where

The .where() method filters results. It supports three calling styles:

Equality (implicit =)

bucket('exchange')
    .select('name', 'value')
    .where('name', 'Dragon scimitar')
    .run();

▶ Run this query

Comparison operators

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

bucket('exchange')
    .select('name', 'value')
    .where('value', '>', 1000000)
    .limit(10)
    .run();

▶ Run this query

Multiple conditions (implicit AND)

Chain .where() calls to combine conditions with AND:

bucket('exchange')
    .select('name', 'value')
    .where('value', '>', 10000)
    .where('value', '<', 100000)
    .limit(10)
    .run();

Convenience Filters

These shorthand methods simplify common filtering patterns:

.whereNot(field, value) — exclude matches

bucket('exchange')
    .select('name', 'value')
    .whereNot('name', 'Coins')
    .where('value', '>', 0)
    .limit(10)
    .run();

▶ Run this query

.whereNull(field) / .whereNotNull(field) — 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

Generates Bucket.Or() internally:

bucket('exchange')
    .select('name', 'value')
    .whereIn('name', ['Bronze axe', 'Iron axe', 'Steel axe'])
    .run();

▶ Run this query


Combining Conditions

For more complex logic, use the Bucket helper object to construct AND, OR, and NOT conditions directly.

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. The join fields specify how rows match between buckets (like 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

Use an alias to give joined buckets shorter names. 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.

bucket('exchange')
    .select('name', 'value')
    .orderBy('value', 'desc')
    .limit(10)
    .run();

.paginate(page, perPage)

A convenience 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(condition, callback)

Conditionally apply query modifications based on runtime values. The callback only executes when the condition is true:

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 of the builder. 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);

const expensiveQuery = topExpensive.run();
const cheapQuery = topCheap.run();

Executing Queries

.run() — URL-encoded output (default)

By default, .run() returns a URI-encoded string, ready to concatenate into a URL:

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

Pass { encodeURI: false } to get the raw Lua string for debugging or logging:

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

.printSQL() — raw Lua string (alias)

Equivalent to .run({ encodeURI: false }), returns the raw Lua without encoding:

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

BucketResponse — response wrapper

The BucketResponse class wraps the raw API response and provides convenient accessors:

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

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

const raw = await fetch(query.toUrl()).then(r => r.json());
const response = new BucketResponse(raw);

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

BucketResponse.from() — typed responses from queries

Use BucketResponse.from(query, raw) to automatically infer the result type from the query builder. No manual type parameter required:

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. Useful for typing variables, function parameters, or API response handlers:

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

const query = bucket('exchange').select('id', 'name', 'value');
type ExchangeRow = InferBucketResult<typeof query>;
// ExchangeRow = { 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 with all query parameters, 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

| 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 | | .where(...conditions) | Adds multiple conditions (implicit AND) | | .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, sourceField, targetField) | Joins another bucket | | .join(bucket, alias, sourceField, targetField) | 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 when cond is true | | .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

| Helper | Description | |---|---| | Bucket.And(...conditions) | Logical AND | | Bucket.Or(...conditions) | Logical OR | | Bucket.Not(condition) | Logical NOT | | Bucket.Null() | Represents a NULL value |

Type Exports

| Export | Purpose | |---|---| | BucketName | Union of all valid bucket names | | BucketRegistry | Maps bucket names to their field interfaces | | BUCKET_FIELDS | Runtime map of field names per bucket | | BucketResponse<T> | Response wrapper class | | BucketResponse.from() | Creates a typed response from a query builder | | InferBucketResult<T> | Extracts the inferred row type from a query | | BucketMetaFields | The page_name and page_name_sub fields auto-injected into every query | | Operator | Valid comparison operators | | ScalarValue | string \| number \| boolean |


Available Buckets

Browse all available buckets and their fields on the Wiki: Special:AllPages (Bucket namespace)

Resources

Contributing

See CONTRIBUTING.md for how to set up the project, run tests, and submit changes.

Development

Setup

git clone https://github.com/Dava96/osrs-wiki-bucket-builder.git
cd osrs-wiki-bucket-builder
npm install

Testing

npm test

Linting & Formatting

npm run lint
npm run format

Syncing Bucket Definitions

Regenerate TypeScript definitions from the Wiki:

npm run buckets

License

ISC