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

@avandar/clients

v0.1.2

Published

Typed CRUD client primitives and database-specific CRUD type helpers

Readme

@avandar/clients

Typed CRUD client primitives. Provides:

  • A base ServiceClient module — the lowest-level client primitive (just a named module). All other client builders extend it.
  • A generic ModelCrudClient — a database-agnostic CRUD client for any data model. Defines the standard interface (getById, getAll, getPage, insert, update, delete, etc.) that @avandar/query-hooks's withQueryHooks knows how to wrap.
  • A SupabaseCrudClient — a concrete Supabase implementation of the generic CRUD client, with table-name-aware types pulled from a registered database type.
  • A makeParserRegistry builder for translating between database row variants and frontend model variants.
  • A Register interface that downstream consumers augment to register their Supabase Database type.

This package separates what a model client does (CRUD operations) from where the data lives (Supabase, an HTTP API, an in-memory store, etc.).

ESM only. Requires Node 22+.

Install

pnpm add @avandar/clients
pnpm add zod @supabase/supabase-js

zod and @supabase/supabase-js are peer dependencies. zod is required: the parser registry is built on it and its types appear throughout the public API. @supabase/supabase-js is a peer because you pass a live SupabaseClient across the API boundary, so it must be a single shared copy.

Usage

import {
  createServiceClient,
  createSupabaseCrudClient,
  makeParserRegistry,
} from "@avandar/clients";

// Register your Supabase Database type once, anywhere in the codebase
declare module "@avandar/clients" {
  interface Register {
    supabaseDatabase: Database;
  }
}

const userParsers = makeParserRegistry<UserCrudSpec>().build({
  modelName: "User",
  DBReadSchema: UserDBReadSchema,
  fromDBReadToModelRead: (db) => ({ id: db.id, name: db.full_name }),
  fromModelInsertToDBInsert: (m) => ({ full_name: m.name }),
  fromModelUpdateToDBUpdate: (m) => ({ full_name: m.name }),
});

const UserClient = createSupabaseCrudClient({
  modelName: "User",
  tableName: "users",
  dbTablePrimaryKey: "id",
  parsers: userParsers,
  dbClient: supabase,
});

const users = await UserClient.getAll();
const user = await UserClient.getById({ id: "..." });

Service client

The base building block. Every client in this package is composed on top of a ServiceClient.

createServiceClient(clientName)

Creates a named @avandar/modules module with a single member, getClientName(). By convention clientName ends in "Client".

| Type | Description | | --------------- | -------------------------------------------------------- | | ServiceClient | The module type returned by createServiceClient |


Model CRUD client

A database-agnostic CRUD client for any model. Implementers provide the low-level crudFunctions (one function per CRUD operation, working in "DB" types) and parsers (converting between DB and frontend model types). The client exposes a high-level surface in frontend model types.

createModelCrudClient(options)

Builds a ModelCrudClient<M>.

| Option | Description | | ----------------------- | --------------------------------------------------------------------------------- | | modelName | Model name (used to brand the client and log lines) | | parsers | A parser registry from makeParserRegistry | | crudFunctions | The implementation of each CRUD operation against the data store | | defaultGetAllBatchSize| Page size used by getAll to paginate (default 500) | | additionalQueries | Extra query functions merged into the client; eligible for auto-generated hooks | | additionalMutations | Extra mutation functions merged into the client; eligible for auto-generated hooks |

The returned client exposes the following methods (all return promises):

| Method | Description | | ------------------------ | ----------------------------------------------------------------- | | getById({ id }) | Single read by primary key. id may be nullish (returns undefined) | | getCount({ where? }) | Total row count matching the filter | | getPage({ where?, pageSize, pageNum }) | One page of rows plus pagination metadata | | getAll({ where?, batchSize? }) | All rows, internally paginated | | getOne({ where? }) | First row matching the filter | | insert({ data, upsert?, onConflict? }) | Insert (or upsert) a single row | | bulkInsert({ data[], ... }) | Insert (or upsert) many rows | | update({ id, data }) | Update a single row | | delete({ id }) | Delete a single row | | bulkDelete({ ids[] }) | Delete many rows | | parsers | The parser registry | | crudFunctions | The raw CRUD functions, in case direct DB-type access is needed |

Types

| Type | Description | | ----------------------------- | -------------------------------------------------------------------------- | | CrudModelSpec | Generic spec: modelName, modelPrimaryKeyType, plus DBRead/DBInsert/DBUpdate and Read/Insert/Update shapes | | ModelCrudClient<M> | The full client surface for a given CrudModelSpec | | ClientReturningOnlyPromises | Record shape required for additionalQueries and additionalMutations | | UpsertOptions | { upsert?, onConflict? } shared by insert / bulkInsert |


Supabase CRUD client

A concrete CRUD client backed by Supabase. Reads DBRead/DBInsert/ DBUpdate from the database type registered through the Register interface, so callers only need to declare frontend model types.

createSupabaseCrudClient(options)

| Option | Description | | ------------------- | ------------------------------------------------------------------------------------------ | | modelName | Model name | | tableName | Supabase table name (typed against the registered database) | | dbTablePrimaryKey | Primary key column name (typed against the table row) | | parsers | Parser registry from makeParserRegistry | | dbClient | A SupabaseClient<RegisteredSupabaseDatabase> instance | | queries? | Builder that returns extra promise-returning query functions; receives dbClient, parsers, logger | | mutations? | Builder that returns extra promise-returning mutation functions; same arguments |

The returned client extends ModelCrudClient with setDBClient(newClient) for swapping out the underlying Supabase client (used to seed data with an admin client during tests).

withSupabaseClient(client, initializer)

Lower-level helper used internally to attach a setDBClient method to any ServiceClient. Exposed in case you need to build a Supabase-aware client without using the full CRUD machinery.

Types

| Type | Description | | --------------------- | ------------------------------------------------------------------------ | | SupabaseCrudModelSpec | Wrapper that derives DB types from the registered Supabase Database | | WithSupabaseClient | A ServiceClient augmented with setDBClient |


SQLite CRUD client

createSqliteCrudClient mirrors the public surface of createSupabaseCrudClient so callers can be branched between the two without changing consumer code. It is used for local-first / desktop setups where reads and writes hit a local SQLite mirror.

It does not know how to reach your database. You inject a transport, which keeps this package free of any particular IPC layer or driver and lets the client work against Electrobun IPC, better-sqlite3, a remote endpoint, or a fake in tests:

import type { SqliteTransport } from "@avandar/clients";

const transport: SqliteTransport = {
  query: ({ sql, params }) => runReturningRows(sql, params),
  run: ({ sql, params }) => runReturningNothing(sql, params),
};

const WidgetClient = createSqliteCrudClient({
  modelName: "Widget",
  tableName: "widgets",
  dbTablePrimaryKey: "id",
  parsers: widgetParsers,
  dbClient,
  transport,
});

| Type | Description | | ----------------- | ------------------------------------------------------ | | SqliteTransport | { query, run }, both taking { sql, params } |

Known limitations: JSON-typed columns are stringified on write but returned as raw strings on read, and boolean columns come back as integer 0/1. Model parsers must coerce both.


Parser registry

makeParserRegistry<M>().build(config)

Builds a ModelCrudParserRegistry<M> from:

  • a Zod schema for DBRead rows (validated on every read),
  • a fromDBReadToModelRead parser,
  • a fromModelInsertToDBInsert parser,
  • a fromModelUpdateToDBUpdate parser.

The builder hardens each parser:

  • fromDBReadToModelRead first runs the Zod schema with a per-model error map.
  • fromModelInsert / fromModelUpdate strip any keys not present in the DB schema (Supabase rejects unknown keys) and remove undefined values that may have been re-introduced by the pick.

| Type | Description | | ------------------------ | -------------------------------------------------------- | | ModelCrudParserRegistry| The shape returned from .build(...) |


Register interface

Register is an empty interface intended for declaration-merging by the consumer.

import type { Database } from "./database.types";

declare module "@avandar/clients" {
  interface Register {
    supabaseDatabase: Database;
  }
}

Once registered, tableName, DBRead, DBInsert, and DBUpdate types flow through createSupabaseCrudClient automatically.

| Type | Description | | ---------- | ------------------------------------------------------------ | | Register | Augmentation target for registering a Supabase Database |

License

MIT