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

@web-ts-toolkit/access-router

v0.2.0

Published

Access-policy Express routers and in-memory data services for Mongoose-backed APIs

Readme

@web-ts-toolkit/access-router

Access-policy Express routers and in-memory data services for Mongoose-backed APIs.

Installation

pnpm add @web-ts-toolkit/access-router express mongoose

Usage

import acl from '@web-ts-toolkit/access-router';

acl.set('globalPermissions', (req) => {
  return req.headers.user === 'admin' ? ['isAdmin'] : [];
});

const router = acl.createDataRouter('fruit', {
  basePath: '/fruit',
  data: [{ id: 'apple', name: 'Apple', public: true }],
  identifier: 'id',
  routeGuard: {
    list: true,
    read: true,
  },
  permissionSchema: {
    id: true,
    name: 'isAdmin',
    public: true,
  },
});

List Responses

List endpoints now return a stable envelope:

{
  "data": [],
  "meta": {
    "returnedCount": 0,
    "skip": 0,
    "limit": 25,
    "page": 1,
    "pageSize": 25,
    "hasPreviousPage": false
  }
}

When include_count=true is enabled, meta also includes total pagination information:

{
  "data": [],
  "meta": {
    "returnedCount": 0,
    "totalCount": 100,
    "skip": 25,
    "limit": 25,
    "page": 2,
    "pageSize": 25,
    "totalPages": 4,
    "hasNextPage": true,
    "hasPreviousPage": true
  }
}

Notes:

  • returnedCount is the number of rows in this response.
  • totalCount is only included when include_count=true.
  • include_extra_headers=true can still add the total count header, but it does not change the response body shape.

When include_extra_headers=true is enabled, the response can also include these headers:

  • wtt-returned-count
  • wtt-page
  • wtt-page-size
  • wtt-has-previous-page

And when include_count=true is also enabled:

  • wtt-total-count
  • wtt-total-pages
  • wtt-has-next-page

Request Validation

Public router endpoints now validate request path params, known query params, and top-level request body shapes before calling the service layer.

Examples:

  • GET /users/:id?try_list=false validates id and try_list
  • GET /pets?include_count=true&limit=10 validates boolean and pagination query params
  • POST /users/__mutation requires a top-level data field
  • POST /users/__query/:id validates advanced select, populate, include, and tasks shapes

Invalid requests return 400 application/problem+json with structured errors entries using parameter or pointer:

{
  "title": "Bad Request",
  "detail": "Bad Request",
  "status": 400,
  "errors": [
    {
      "parameter": "include_count",
      "detail": "Invalid option: expected one of \"true\"|\"false\""
    }
  ]
}

User-Defined Request Schemas

Model and data routers can add route-specific Zod validation through the requestSchemas option.

Use this when you want stricter application-level request validation on top of the built-in router boundary validation.

Recommended shape:

  • whole-body schemas: requestSchemas.<route> or requestSchemas.<route>.default
  • nested advanced mutation payloads: requestSchemas.<route>.data

Model router examples:

  • requestSchemas.create
  • requestSchemas.update
  • requestSchemas.upsert
  • requestSchemas.count
  • requestSchemas.distinct
  • requestSchemas.advancedList
  • requestSchemas.advancedReadFilter
  • requestSchemas.advancedRead
  • requestSchemas.advancedCreate.default
  • requestSchemas.advancedCreate.data
  • requestSchemas.advancedUpdate.default
  • requestSchemas.advancedUpdate.data
  • requestSchemas.advancedUpsert.default
  • requestSchemas.advancedUpsert.data
  • requestSchemas.subList
  • requestSchemas.subRead
  • requestSchemas.subCreate
  • requestSchemas.subUpdate
  • requestSchemas.subBulkUpdate

Data router examples:

  • requestSchemas.advancedList
  • requestSchemas.advancedReadFilter
  • requestSchemas.advancedRead

Example:

import { z } from 'zod';
import acl from '@web-ts-toolkit/access-router';

const router = acl.createRouter('User', {
  basePath: '/users',
  identifier: 'name',
  requestSchemas: {
    create: z.object({
      name: z.string().min(3),
      role: z.string(),
    }),
    advancedCreate: {
      data: z.object({
        name: z.string().min(3),
        role: z.literal('user'),
      }),
    },
    advancedUpdate: {
      data: z.object({
        role: z.enum(['manager', 'staff']),
      }),
    },
  },
});

Validation order:

  • built-in route/query/body-shape validation runs first
  • user-defined requestSchemas validation runs second
  • write-operation model validate hooks still run afterward in the service layer

Custom Route Validation

The package also exports the same validation helpers used by the built-in public routers:

  • parsePathParam
  • parseQuery
  • parseBody
  • requestSchemas
  • advanced body schemas such as listBodySchema, readByIdBodySchema, advancedCreateBodySchema, and advancedUpdateBodySchema

Example:

import acl, {
  parseBody,
  parsePathParam,
  parseQuery,
  requestSchemas,
  readByIdBodySchema,
} from '@web-ts-toolkit/access-router';

const router = acl.createRouter('User', {
  basePath: '/users',
});

router.router.post('/custom/:id', async (req) => {
  const id = parsePathParam(req.params.id, 'id');
  const { include_permissions } = parseQuery(requestSchemas.readQuery, req.query);
  const body = parseBody(readByIdBodySchema, req.body);

  return {
    id,
    includePermissions: include_permissions === 'true',
    body,
  };
});

These helpers throw the same BadRequestError shape as the built-in router endpoints, so custom routes can stay consistent with the package defaults.

Hook Signatures

The most common model hooks are called with this bound to the current Express request.

baseFilter

(this: express.Request, permissions: Permissions) =>
  | Filter
  | true
  | null
  | undefined
  | Promise<Filter | true | null | undefined>
  • Return a Filter to restrict access.
  • Return true, null, or undefined for no extra base filter.
  • Return false to deny access.

decorate

(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
  unknown | Promise<unknown>;
  • Runs after a document has been loaded and trimmed.
  • Can also be an array of middleware functions.

overrideFilter

(this: express.Request, filter: Filter, permissions: Permissions) => Filter | Promise<Filter>;
  • Runs before the base filter is applied.
  • Use it to rewrite or augment the caller-provided filter.

validate

(this: express.Request, allowedData: unknown, permissions: Permissions, context: MiddlewareContext) => boolean | unknown[] | Promise<boolean | unknown[]>
  • Return true to allow the write.
  • Return false to reject it.
  • Return an array to provide validation errors.

prepare

(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
  unknown | Promise<unknown>;
  • Runs before create/update data is assigned to the document.
  • Can also be an array of middleware functions.

transform

(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
  unknown | Promise<unknown>;
  • Runs during update flows before the document is saved.
  • Can also be an array of middleware functions.

finalize

(this: express.Request, value: unknown, permissions: Permissions, context: MiddlewareContext) =>
  unknown | Promise<unknown>;
  • Runs after create/update persistence work and before response decoration.
  • Can also be an array of middleware functions.

docPermissions

(this: express.Request, doc: unknown, permissions: Permissions, context: MiddlewareContext) =>
  Record<string, unknown> | Promise<Record<string, unknown>>;
  • Returns the document-level permission object written to the configured permission field.

Example

acl.createModelRouter('Post', {
  baseFilter: {
    read(this, permissions) {
      if (permissions.has('isAdmin')) return true;
      return { published: true };
    },
  },
  validate: {
    create(this, data) {
      if (!data || typeof data !== 'object' || !('title' in data)) {
        return ['title is required'];
      }

      return true;
    },
  },
  prepare: {
    create(this, data) {
      if (typeof data === 'object' && data) {
        return { ...data, createdAt: new Date() };
      }

      return data;
    },
  },
  transform: {
    update(this, doc) {
      return doc;
    },
  },
  finalize: {
    update(this, doc) {
      return doc;
    },
  },
  decorate: {
    read(this, doc) {
      const record = typeof doc === 'object' && doc ? doc : {};
      return { ...record, summary: '...' };
    },
  },
  overrideFilter: {
    read(this, filter) {
      return filter ?? {};
    },
  },
  docPermissions: {
    read(this, doc, permissions) {
      return {
        canArchive: permissions.has('isAdmin'),
      };
    },
  },
});