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

@egi/smart-db-server

v0.5.0

Published

Validated HTTP server and browser client for registered SmartDB models

Readme

@egi/smart-db-server

Validated HTTP access to explicitly registered @egi/smart-db models. The package contains a framework-neutral dispatcher, an optional Express router, and a browser-safe fetch adapter suitable for @egi/smart-table.

Installation

npm install @egi/smart-db-server @egi/smart-db express

Express is only required by applications importing @egi/smart-db-server/express. Browser-only consumers can import @egi/smart-db-server/client without a Node server dependency.

Minimal server

import express from "express";
import {SmartDbBetterSqlite3} from "@egi/smart-db/drivers/smart-db-better-sqlite3";
import {SmartDbServer} from "@egi/smart-db-server";
import {createSmartDbRouter} from "@egi/smart-db-server/express";
import {TaskModel, TaskDictionary} from "./models";

const database = new SmartDbBetterSqlite3(
    {filename: "tasks.sqlite"},
    {connectOnly: true}
);
await database.databaseReady();

const server = new SmartDbServer({
    authorize: async () => true,
    database,
    dictionary: TaskDictionary,
    models: {
        TaskModel: {
            model: TaskModel,
            policy: {
                allowDelete: true,
                scope: "global",
                readableFields: ["id", "title", "done"],
                createFields: ["title", "done"],
                updateFields: ["title", "done"]
            }
        }
    }
});

const app = express();
app.use("/api", createSmartDbRouter({
    context: () => ({principal: {}}),
    server
}));
app.listen(3000);

The router provides POST /api/:operation/:model. See demo/ for a complete SQLite application and browser client.

Authorization and model capabilities

The server validates a request before opening a transaction. It then calls the server-wide authorization gate with a domain-neutral target and the same SmartDbTransaction used by the operation:

const server = new SmartDbServer({
    authorize: async (context, target, transaction) => {
        // Map the generic target to the host application's permission model.
        // target.mutationIntents contains create/update/delete intent derived
        // from validated data, including mixed save requests.
        return permissions.allow(
            transaction,
            context.principal,
            target.name,
            target.operation,
            target.mutationIntents
        );
    },
    // ...
});

The authorization target never contains the raw body or application-specific permission types. Invalid requests and static model-policy failures do not open a transaction. A request that reaches authorization uses exactly one transaction through authorization, scopes, hooks, commands, reads, and writes.

Model mutation capabilities are explicit:

  • omit createFields to disable create;
  • omit updateFields to disable update;
  • set allowDelete: true to enable primary-key delete;
  • additionally set allowPredicateDelete: true to enable predicate delete;
  • set allowPredicateUpdate: true to enable predicate update.

All capabilities default to disabled except read, which is constrained by readableFields and the mandatory scope choice.

Views and other models without a primary key may be registered with a read-only policy. Their policies must omit all create, update, and delete capabilities; filters, ordering, projections, limits, and row-level scopes remain available for reads. The browser client's read() method accepts these keyless model classes, while its mutation helpers reject them before issuing a request.

Custom commands

Custom commands inherit from SmartDbServerCommand and implement both body validation and execution. The default authorize() permits the command after the server-wide authorization gate; override it for command-specific policy. Both authorization layers and perform() receive the same transaction.

import {SmartDbTransaction} from "@egi/smart-db";
import {
    SmartDbServerCommand,
    SmartDbServerContext,
    SmartDbServerError,
    SmartDbServerResult,
    SmartDbServerResultDraft
} from "@egi/smart-db-server";

interface Principal {
    username: string;
}

class EchoCommand extends SmartDbServerCommand<{message: string}, Principal> {
    public async perform(
        _transaction: SmartDbTransaction,
        _operation: string,
        body: {message: string},
        _context: SmartDbServerContext<Principal>
    ): Promise<SmartDbServerResultDraft> {
        return SmartDbServerResult.success(body);
    }

    public validateBody(body: unknown): {message: string} {
        if (!body || typeof body != "object" || typeof (body as Record<string, unknown>).message != "string") {
            throw new SmartDbServerError("Invalid echo body");
        }
        return {message: (body as {message: string}).message};
    }
}

server.registerCommand("echo", new EchoCommand());

Browser client

import {SmartDbServerClient} from "@egi/smart-db-server/client";

const data = new SmartDbServerClient({baseUrl: "/api"});
const rows = await data.read(TaskModel, {orderBy: ["title ASC"]});
const filtered = await data.read(TaskModel, {
    orderBy: [{operation: "COALESCE", value: ["status", "title"]}],
    where: {
        status: {operation: "NOT IN", value: ["done", "cancelled"]},
        title: {operation: "NOT LIKE", value: "Draft%"},
        dueDate: {operation: ">=", value: "2026-01-01"}
    }
});
const summaries = await data.read(TaskModel, {
    fields: ["id", "title"],
    limit: {limit: 50, offset: 0},
    orderBy: ["id ASC"]
});
await data.create(TaskModel, [new TaskModel({title: "New task"})]);
await data.update(TaskModel, [new TaskModel({id: 1, done: true})]);
await data.delete(TaskModel, [{id: 2}]);
await data.deleteWhere(TaskModel, {status: "obsolete"});
const stats = await data.command<{count: number}, Record<string, never>>(
    "stats",
    "count",
    {}
);

Read projections must be non-empty subsets of the model policy's readableFields. Projected rows remain plain objects and are typed as Pick<Row, Field>[]; complete reads continue to hydrate model instances. Client limits cannot exceed the server's maxRows, and callers should include orderBy when using offsets so pagination is deterministic. Read predicates support SmartDB's validated EQ, NE, GT, GE, LT, LE, IN, NOT_IN, BETWEEN, IS_NULL, IS_NOT_NULL, LIKE, and NOT_LIKE operations, plus nested and/or clauses. SQL literals, subqueries, and arbitrary expressions remain unavailable. Ordering accepts readable field names and validated COALESCE expressions containing readable fields and scalar fallbacks. Predicate deletes require a non-empty predicate and the model policy's allowDelete and allowPredicateDelete capabilities.

The server rejects unknown properties, unsafe keys, unauthorized fields, oversized queries and batches, missing mutation keys, and reads exceeding the configured maximum. It never accepts SmartDbSqlOptions or SqlWhere directly from the network.

Localizable failures

A failure result carries the browser-safe SmartDbServerMessageDescriptor contract: an optional messageCd translation code and scalar messageParams, so a host can present the failure in the user's language while message remains English developer detail that hosts must not show to end users.

SmartDbServerResult.success() supplies status 200 and SmartDbServerResult.failure() supplies status 500 unless explicitly overridden. SmartDbServerResult.hydrate() validates untrusted JSON and throws SmartDbServerResultHydrationError for malformed envelopes. The browser client wraps parsing and hydration failures in SmartDbServerProtocolError; it never fabricates an application failure result.

throw new SmartDbServerError("Domain code already exists.", 409, -21, {
    messageCd: "serverError.systemDomainAdmin.domainCodeExists",
    messageParams: {domainCd: "LION"}
});

Failures that do not already carry a messageCd — including framework validation errors and unrecognized throwables such as database driver errors — are offered to the optional normalizeError configuration hook before the result is produced. The hook returns a SmartDbServerError carrying the host's own status, application code and translation code, or undefined to keep the default handling under which an unrecognized throwable becomes a generic 500 without internal detail. A hook that throws is ignored.

normalizeError: (error) => {
    if ((error as {code?: string}).code == "23505") {
        return new SmartDbServerError("Duplicate key.", 409, -21, {
            messageCd: "serverError.db.duplicateKey"
        });
    }
    return undefined;
}

Both fields survive the Express transport and the browser client, which throws a typed SmartDbServerClientError carrying the complete result and HTTP status. All browser-safe request, response, model, normalization, message, and client-error types are exported from @egi/smart-db-server/client.

Lossless numbers

Models generated with SmartDB's lossless-number extractor options carry a losslessNumberType discriminator. Marked decimal and big-integer values remain canonical JSON strings through requests, database binds, responses, hooks, and the browser client. The server rejects JavaScript numbers, native bigint, and calculation objects for these wire fields and rejects LIKE predicates.

The server installs decimal.js and normalizes marked values with SmartDB's shared canonicalizer. Decimal rounding defaults to ROUND_HALF_UP and can be configured for trusted server input:

import {DecimalRoundingMode} from "@egi/smart-db";

const server = new SmartDbServer({
    // ...
    decimalRoundingMode: DecimalRoundingMode.ROUND_HALF_EVEN
});

Generated models canonicalize values before the browser client sees them. For plain-object decimal writes, configure the same opt-in normalizer explicitly; the rounding mode remains local and is never sent over the wire:

import {DecimalRoundingMode} from "@egi/smart-db";
import {normalizeDecimal} from "@egi/smart-db/lossless-numbers";
import {SmartDbServerClient} from "@egi/smart-db-server/client";

const data = new SmartDbServerClient({
    baseUrl: "/api",
    decimalNormalizer: normalizeDecimal,
    decimalRoundingMode: DecimalRoundingMode.ROUND_HALF_EVEN
});

Primary keys remain JSON numbers for compatibility. Registration requires an unmarked numeric key, and every key is checked at runtime to be an integral JavaScript safe integer. Deploy the lossless-aware server before deploying a client generated with lossless options; older servers treat the string fields as ordinary strings and cannot enforce these semantics.

SQLite fractional lossless decimals require a column with TEXT affinity. Run the asynchronous storage audit after the database is ready and before starting the HTTP listener. The constructor cannot run this asynchronous preflight automatically; applications must explicitly await it:

await database.databaseReady();
await server.auditStorage();
app.listen(port);

The audit rejects REAL, NUMERIC, missing, and ambiguous affinity metadata. allowUnsafeSqliteAffinity is a migration-only override; configure onStorageAuditWarning to route its startup warning into application logging. The runtime response check remains a final safeguard, but converting an already rounded SQLite REAL value to TEXT cannot recover lost digits.

PlainDate fields

SmartDB 4.2 models can opt date-only columns into Temporal.PlainDate with attribute metadata type: "PlainDate". SmartDbServer accepts exactly YYYY-MM-DD on the HTTP wire and uses SmartDB's strict plainDate parser, so timestamps, offsets, calendar annotations, invalid dates, and MySQL zero dates are rejected. null remains JSON null.

import {plainDate} from "@egi/smart-db";

const dueDate = plainDate.from("2026-08-08");
JSON.stringify({dueDate}); // {"dueDate":"2026-08-08"}

Validated writes, trusted scopes, and write hooks use runtime PlainDate objects. Both beforeWrite current/pending values and afterWrite result rows therefore have matching date semantics. SmartDbServer converts them back to canonical strings only when constructing a result.

The browser client serializes generated PlainDate models without configuration. Complete reads are passed through the generated model's from() factory and revive PlainDate objects; projected reads remain plain wire objects whose date fields are canonical strings.

A JavaScript Date represents an instant and is never converted implicitly. Applications migrating an instant must choose the timezone explicitly before transport:

const calendarDate = plainDate.fromDate(instant, "Europe/Zurich");

SmartDB owns SQLite, MySQL/MariaDB, PostgreSQL, and Oracle date binds and hydration. SmartDbServer adds no database-specific PlainDate conversion.