@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
Maintainers
Readme
OSRS Wiki Bucket Builder
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:AllPagesensure 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
*andalias.*expansion to strict field lists. - Zero Runtime Dependencies — Generates query strings without making network requests.
Installation
npm install @dava96/osrs-wiki-bucket-builderQuick 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 numberCore 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();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();Comparison operators
Supported operators: =, !=, >, <, >=, <=
bucket('exchange')
.select('name', 'value')
.where('value', '>', 1000000)
.limit(10)
.run();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();.whereNull(field) / .whereNotNull(field) — null checks
bucket('infobox_item')
.select('item_name', 'weight')
.whereNotNull('weight')
.limit(10)
.run();.whereBetween(field, [min, max]) — inclusive range
bucket('exchange')
.select('name', 'value')
.whereBetween('value', [10000, 100000])
.limit(10)
.run();.whereIn(field, values) — match any value
Generates Bucket.Or() internally:
bucket('exchange')
.select('name', 'value')
.whereIn('name', ['Bronze axe', 'Iron axe', 'Steel axe'])
.run();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();Bucket.And(...) — all conditions must match
bucket('exchange')
.select('name', 'value')
.where(Bucket.And(
['value', '>', 1000],
['value', '<', 50000]
))
.run();Bucket.Not(...) — negate a condition
bucket('exchange')
.select('name', 'value')
.where(Bucket.Not(['name', 'Coins']))
.run();Bucket.Null() — represents a null value
bucket('infobox_item')
.select('item_name', 'weight')
.where('weight', '!=', Bucket.Null())
.run();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();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();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();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();.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 failedBucketResponse.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 numberInferBucketResult — 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();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 installTesting
npm testLinting & Formatting
npm run lint
npm run formatSyncing Bucket Definitions
Regenerate TypeScript definitions from the Wiki:
npm run bucketsLicense
ISC
