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

@~lyre/model

v0.1.0

Published

Eloquent/Lyre-style model layer for SvelteKit + Drizzle. A base Repository carrying all CRUD + a fluent chainable query API, driven by a per-model spec and a query-string grammar (dynamic filtering/sorting/search/relations/pagination), with auto-generated

Readme

@~lyre/model

Eloquent/Lyre-style model layer for SvelteKit + Drizzle (Node/TypeScript). A base Repository carries all CRUD + a fluent chainable query API, driven by a per-model spec and a query-string grammar (dynamic filtering, sorting, search, relations, pagination). Models auto-expose a REST API + remote-function factory, and a lyre CLI scaffolds a whole model from one command.

Ported from the Laravel Lyre package.

Install

npm install @~lyre/model drizzle-orm

drizzle-orm is a peer dependency (^0.45.0). The package works with any Drizzle Postgres driver — postgres-js and node-postgres alike.

Setup

Register the app's database handle once at startup; nothing else is required.

// src/hooks.server.ts
import { createPlatformHook } from '@~lyre/model';
import { getDb } from '$lib/server/db';
import * as schema from '$lib/server/db/schema';

export const handle = createPlatformHook({
  db: getDb,          // the app's handle factory — the package imports no db module
  schema,             // auto-registers a REST model per table
  prefix: '/api',
  version: 'v1',
  auto: {
    only: ['widgets', 'reports'],        // ALLOWLIST — never expose a whole schema blindly
    tenantColumns: ['tenant_id'],        // e.g. ['app_slug', 'tenant_id'] when scoping differs
    requireTenantScope: 'throw'          // refuse to register a table with no scoping column
  }
});

Outside a hook, call setDatabaseProvider(getDb) directly.

Multi-tenant safety. auto.only is an allowlist; prefer it over exclude so a new table is never exposed by accident. requireTenantScope makes an unscoped table a startup error rather than a silent cross-tenant leak.

Design

  • Schema stays the source of truth. Drizzle schema files remain authoritative for drizzle-kit migrations. registerSchema() introspects a table into structural metadata; the authored ModelSpec overlays only what can't be inferred (relations, searchable fields, status, tenancy). No column is declared twice.
  • The app owns the database. This package imports no db module. The host registers a handle factory once — createPlatformHook({ db: getDb }) or setDatabaseProvider(getDb) — and every Repository uses it. An explicit RepoContext.db still wins per request, which is how multi-database apps route per-tenant/per-app pools. Db is a structural type, so postgres-js and node-postgres both satisfy it.
  • Identifiers are inferred, not assumed. idType comes from the id column's Drizzle type (uuid / numeric / text), so a bigserial primary key on a table that also has a slug resolves find('42') by id, not slug.
  • Tenant scoping is real. When a model declares tenantColumn, every query is scoped to ctx.tenantId. Escape via .withoutTenant() / findAny().
  • Relations without relations(). The Drizzle schema declares none, so relations live in the spec and execute via batched second queries (eager load) and correlated subqueries (whereHas / withCount / doesntHave).

Defining a model

import { products, collections, productVariants } from './schema';
import { defineModel, Repository, type RepoContext } from '@~lyre/model';

export const ProductModel = defineModel({
  name: 'product',
  table: products,
  slugColumn: 'slug',
  nameColumn: 'title',
  tenantColumn: 'tenantId',
  status: { column: 'status', active: 'active' },
  searchable: ['title', 'description'],
  relations: {
    collection: { type: 'belongsTo', table: collections, foreignKey: 'collectionId' },
    variants: { type: 'hasMany', table: productVariants, foreignKey: 'productId' }
  },
  with: ['collection', 'variants']
});

export const Products = (ctx?: RepoContext) => new Repository(ProductModel, ctx);

Querying

await Products({ tenantId }).all();
await Products({ tenantId }).find('glass-feeding-bottle-set');        // uuid | slug aware
await Products({ tenantId }).searchQuery('bottle').status('active').paginate(9, 1).paginateResult();
await Products({ tenantId }).relations('variants', 'collection').withCount('variants').all();

// From a URL / search params (Lyre grammar):
await Products({ tenantId })
  .fromQuery('?search=bottle&status=active&with=variants&withcount=variants&per_page=9')
  .all();

Query-string grammar

filter=col,val · range=col,min,max (nested rel.col) · relation=path,value · relation_in=rel,v1,v2 · with=rel,rel.nested · withcount=rel · search=kw (+search-relations=rel,col) · status=a,b · order=col,asc|desc · per_page/page · unpaginated · limit/offset · startswith=abc · wherenull=col · doesnthave=rel · random · first · ?<relation>=value.

Writes

create, batchCreate, firstOrCreate, updateOrCreate, update(idsOrSlugs, values) (bulk via comma), delete(idsOrSlugs) (bulk; soft-delete when opted in). Tenant id is auto-injected on create.

REST API

A single catch-all route resolves any registered model:

GET    /api/products?search=bottle&status=active&per_page=9&with=variants
GET    /api/products/<id|slug>?with=collection
GET    /api/products/<id>/variants
POST   /api/products
PUT    /api/products/<id[,id2]>
DELETE /api/products/<id[,id2]>

Every response uses the envelope { status, message, result, code }. See resourceResponse() / handleResource().

For idiomatic in-app use, makeResourceRemote(spec, { query, command, resolveContext }) returns typed SvelteKit remote functions.

CLI

pnpm lyre make:model widget --tenant --soft-delete --policy
pnpm lyre register
pnpm db:generate && pnpm db:migrate

make:model emits separate files: a Drizzle table (schema template), the model spec, a repository, a types/DTO file, and (optionally) a policy — then register regenerates the model registry barrel. The Drizzle table stays owned by the developer / drizzle-kit.

Status / limitations

  • Eager load, whereHas, withCount, doesntHave, nested-range filters, and tenant scoping are implemented and validated live.
  • Ordering by a relation column (order=rel.col) is parsed but not yet joined — own-column ordering only for now.
  • Soft delete is opt-in per model (softDelete: true + a deletedAt column).
  • belongsToMany (pivot) relations are specced but eager-loading is single-level belongsTo/hasMany today.