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

@nubitio/crud

v1.1.1

Published

Declarative CRUD engine with field DSL, forms, datagrids, RBAC, conditional logic and pluggable adapters (Hydra/REST).

Readme

@nubitio/crud

The Nubit CRUD engine: declarative resource pages with a data grid, dialog form, field DSL, RBAC, conditional rules, URL routing, audit trail, and pluggable backend adapters.

Install

npm install @nubitio/crud @nubitio/core @nubitio/ui

Peer dependencies

"@tanstack/react-query": "^5",
"i18next": "^23",
"luxon": "^3",
"react": "^19",
"react-dom": "^19",
"react-i18next": "^14",
"react-router-dom": "^6"

Setup

import '@nubitio/ui/tokens.css'; // design tokens
import '@nubitio/ui/style.css'; // UI primitives styles
import '@nubitio/crud/style.css'; // grid + form styles

Quick start

import { SchemaCrudPage, defineResource, textField, numberField } from '@nubitio/crud';
import { HydraAdapter } from '@nubitio/crud';

const products = defineResource('/api/products', {
  title: 'Products',
  adapter: HydraAdapter,
  fields: [
    textField('name').label('Name').required(),
    numberField('price').label('Price').format('currency'),
  ],
});

export function ProductsPage() {
  return <SchemaCrudPage resource={products} />;
}

Key exports

Engine

| Export | Description | | ---------------- | -------------------------------------------------------------------- | | SchemaCrudPage | Main component — resolves schema, applies rules, renders grid + form | | CrudPage | Lower-level page if you manage fields manually | | defineResource | Create a typed ResourceConfig for a REST/Hydra resource |

Field DSL

textField, numberField, dateField, datetimeField, entityField, enumField, selectField, checkboxField, switchField, moneyField, currencyField, imageField, textareaField, passwordField

Each returns a chainable FieldBuilder with .label(), .required(), .visibleWhen(), .disabledWhen(), .onChange(), .formatter(), and more.

Money fields

moneyField() is for amounts a backend publishes as { amount, currency, scale } — a resource declaring x-crud.format: money gets one automatically. The value is never converted to a JavaScript number: the control edits the decimal as text and submits { amount, currency }, and grid totals are summed in integer minor units.

Prefer it over currencyField(), which is a plain number with two decimals and right alignment. Both still work; only moneyField keeps a grid footer equal to the total the database holds.

Backend adapters

| Export | Description | | ---------------- | ---------------------------------------------------- | | HydraAdapter | API Platform / JSON-LD + Hydra (default) | | RestAdapter | Plain REST with { data, total } or array responses | | BackendAdapter | Interface to implement a custom adapter |

Extension points

| Export | Description | | ------------------------ | --------------------------------------------------- | | ResourceSchemaProvider | Supply a custom field schema for a resource | | ResourceStoreProvider | Supply a custom data store factory | | SmartCrudRolesProvider | Inject RBAC role claims for field-level permissions | | defineFieldContract | Type-safe field contract for SchemaCrudPage |

Permissions

When the backend publishes granular permissions, GET /api/me carries them and the toolbar follows:

<SessionPermissionsProvider permissions={session.permissions} limits={session.limits}>
  <App />
</SessionPermissionsProvider>
defineResource('/api/invoices', { permissionPrefix: 'invoice' });

usePermissions then resolves in this order: an explicit permissions.canX override, the session's granular permissions, the HTTP methods the resource supports, platform defaults. The session sits above the method inference because the methods say what the resource has and the session says what this user may do — only the second can hide a button that would come back a 403.

This decides what the UI offers, never what the API allows. The backend evaluates the same permissions in a voter, so a client that ignores the list gets a 403 rather than a result. An empty list means the backend published none (the module is off) and everything stays visible.

Issued documents

A backend resource declaring #[Printable] publishes x-printable, and the record view gets a print action from it:

<PrintButton
  issueUrl={schema.printable.issueUrl}
  recordId={row.id}
  allowReissue={schema.printable.allowReissue}
/>

Printing and issuing a correction are two separate actions on purpose. Printing is routine; a correction supersedes a document somebody may already be holding, so it confirms first. DocumentHistoryPanel lists every copy ever issued — including the superseded ones, which is the point: "which version did the customer receive?" is a question the archive exists to answer.

Reprinting returns the bytes that were issued, never a fresh render. That is enforced by the server; the client simply does not undermine it.

Spreadsheet import

A resource declaring #[Importable] publishes x-importable:

<ImportPanel uploadUrl={schema.importable.uploadUrl} onApplied={() => grid.reload()} />

Upload analyses the file and shows what applying would do — rows to insert, rows to update, and every error by spreadsheet line — without writing anything. The apply button stays disabled while any row is invalid, because the server refuses a partial import and a button that looks available but always fails is worse than one that explains itself.

Large grids

A resource that publishes x-grid-scale tells the client how it expects to be read:

const { state, observe, goNext, goPrevious } = useCursorPagination();

Cursor pages cannot be addressed by number — each is defined by the last row of the one before it — so the walk follows the links the server produced. That rules out jumping to page 400 and showing "page 12 of 500", and there is no way to fake either without asking the database for exactly the count the cursor was adopted to avoid.

GridData carries two extra facts for these resources. nextPageUrl is where the next page begins, and totalIsEstimate says the total is approximate or unknown — render it as "about N", never as a precise figure somebody might reconcile against. Without it, member.length would claim the page is the whole table: a grid reporting "10 rows" for three years of movements.

Grid export

defineResource('/api/products', { permissions: { canExport: true } });

That renders an Export button in the grid's utility toolbar. Pressing it exports every row matching the grid's current filters and sort with pagination dropped — not the page on screen — and saves the file under the name the server sent in Content-Disposition.

The button only appears when the resource's store implements the optional ResourceStore.export(options): Promise<ResourceExportResult>. HydraAdapter does (it requests the xlsx format as a blob, which nubitio/admin-bundle's nubit_admin.export.enabled serves for every resource); RestAdapter does not, so canExport: true against a plain REST backend is inert by design rather than rendering a button that 404s.

A failed export surfaces in the grid's own error row (grid.exportError) and logs the underlying cause to the console. To back a custom backend, implement export() on your own store:

const store: ResourceStore = {
  load: (options) => /* … */,
  export: async (options) => ({ blob, filename: 'products.xlsx' }),
};