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

@revenexx/app-sdk

v0.3.2

Published

Typed data client + HTTP router for revenexx Apps. Swappable adapters (mock | remote | runtime) so the same App code runs against in-memory fixtures locally and the real data plane in production.

Readme

@revenexx/app-sdk

The SDK for building revenexx Apps — typed data access and HTTP routing for the revenexx Revenue Cloud app platform.

A revenexx App declares its data model in a schema.json and its identity and permissions in a manifest.json. The platform provisions the database, API exposure and tenant isolation from those files; your function code only needs two things:

  • a data client to read and write the App's entities (@revenexx/app-sdk)
  • a router to answer the HTTP routes your App exposes (@revenexx/app-sdk/router)
npm install @revenexx/app-sdk

Data client

The client gives every entity the same methods — list, page, get, create, update, delete — and the same query shape, independent of where the data lives. Swap the adapter to move between environments:

| adapter | use it for | infrastructure | |-----------|-------------------------------------------------------|----------------| | mock | local development + unit tests against fixtures | none | | remote | integration checks against a real development tenant | none local | | runtime | production, inside the deployed function | auto-configured |

const { createClient } = require('@revenexx/app-sdk');

const db = createClient({
  adapter: 'mock',                 // 'remote' | 'runtime'
  entities: {
    markets: { table: 'acme__shop__markets', pk: 'id' },
  },
  seed: { markets: [{ id: 'm1', code: 'de', currency: 'EUR' }] },
});

await db.markets.create({ code: 'at', currency: 'EUR' });
const eur = await db.markets.list({ where: { currency: 'EUR' }, order: 'code.asc' });
const page = await db.markets.page({ limit: 20, offset: 0 });   // { items, total }

Most Apps don't hand-write the entities map: the revenexx tooling generates a typed db.generated.js from your App's schema.json + manifest.json, so createDb({ adapter, ... }) is one import away and every entity is typed.

Queries

One query shape works across all adapters:

await db.markets.list({
  where: {
    currency: 'EUR',                                  // equality
    code: ['de', 'at'],                               // IN list
    created_at: { op: 'gte', value: '2026-01-01' },   // explicit operator
  },
  select: ['id', 'code'],
  order: 'created_at.desc',
  limit: 50,
  offset: 0,
});

Operators: eq, neq, gt, gte, lt, lte, like, ilike, in, is.

Permissions

The methods available per entity follow your App manifest's permissions declarations. Calling an operation the manifest doesn't grant throws a descriptive error (and the platform would reject it anyway) — so local tests catch permission gaps before deploy.

Custom adapters

The adapter is a small interface (list/get/create/update/remove, optional page). Register a new backend before constructing the client:

const { registerAdapter, createClient } = require('@revenexx/app-sdk');

registerAdapter('sqlite', (config) => ({ kind: 'sqlite', /* … */ }));
const db = createClient({ adapter: 'sqlite', entities, file: 'dev.db' });

Router

@revenexx/app-sdk/router turns the single function entrypoint into declarative routes. Path templates use the same {param} syntax as your manifest's capability routes.

const { createApp, notFound } = require('@revenexx/app-sdk/router');
const { createDb } = require('./db.generated');

const app = createApp({ name: 'markets' });

app.get('/markets/{id}/context', async (c) => {
  const db = createDb({ adapter: 'runtime', context: c.ctx });
  const market = await db.markets.get(c.params.id);
  if (!market) throw notFound();
  const locales = await db.locales.list({ where: { market_id: market.id } });
  return c.json({ market, locales });
});

module.exports = app.handler();

What you get:

  • Literal-first matching/markets/defaults wins over /markets/{id}, regardless of registration order.
  • A clean per-request contextc.params, c.query, c.body (parsed), c.tenant, c.header(name), c.log(msg), c.json(data, status).
  • Error mapping — throw HttpError / notFound() / badRequest() / forbidden() / conflict() for explicit statuses; data-client permission errors become 403; anything else is logged and answered as 500.
  • A health routeGET / answers with the App identity, status and the registered routes (disable with createApp({ health: false })).
  • 404/405 handling — unknown paths list the available routes; known paths with a wrong method answer 405.

CRUD in one line

mountCrud wires an entity to the standard five REST routes with filtering and pagination built in:

mountCrud(app, db.markets, { path: '/markets', columns: ENTITIES.markets.columns });
// GET    /markets            list — ?currency=EUR&limit=50&offset=0&order=code.asc
// POST   /markets            create
// GET    /markets/{id}       read
// PUT    /markets/{id}       update
// DELETE /markets/{id}       delete

Nested resources scope every operation to their parent:

mountCrud(app, db.locales, {
  path: '/markets/{marketId}/locales',
  columns: ENTITIES.locales.columns,
  parent: { param: 'marketId', column: 'market_id' },
});
// lists filter by market_id, creates inject it, and a locale belonging to a
// different market answers 404 — even if the request body claims otherwise.

Options: only: ['list', 'get'] restricts the mounted routes; defaultLimit / maxLimit tune pagination bounds (defaults 50 / 200).

Testing your App

Handlers are plain functions over a context object, so tests don't need any infrastructure: build the app with a mock-adapter client, call app.handler() with a fake { req, res }, and assert on the captured JSON.

const handler = app.handler();
await handler({
  req: { method: 'GET', path: '/markets', headers: {}, query: {} },
  res: { json: (data, status = 200) => ({ data, status }) },
  log: console.log,
});

License

MIT