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

@molecule/api-resource-tag

v1.0.2

Published

Tag/category resource handlers with polymorphic resource tagging

Readme

@molecule/api-resource-tag

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Tag resource for molecule.dev.

Provides CRUD for tags (name, slug, color, description) and a join-table system for tagging any entity. Includes popular-tag and slug-based lookups.

Quick Start

import { routes, requestHandlerMap } from '@molecule/api-resource-tag'

// Wire routes into your Express app via mlcl inject
// Routes: POST/GET/PATCH/DELETE /tags, GET /tags/popular,
//         GET /tags/:slug/resources, POST/DELETE /:resourceType/:resourceId/tags

Type

resource

Installation

npm install @molecule/api-resource-tag @molecule/api-database @molecule/api-i18n @molecule/api-locales-tag @molecule/api-logger @molecule/api-permissions @molecule/api-resource

API

Interfaces

ResourceTag

A join record linking a tag to any resource.

interface ResourceTag {
  /** Unique identifier. */
  id: string
  /** Foreign key to the tag. */
  tagId: string
  /** The type of resource (e.g. 'project', 'product'). */
  resourceType: string
  /** The ID of the tagged resource. */
  resourceId: string
  /** ISO 8601 creation timestamp. */
  createdAt: string
}

Tag

A tag record with name, slug, optional color, and optional description.

interface Tag {
  /** Unique identifier. */
  id: string
  /** Display name. */
  name: string
  /** URL-friendly slug derived from name. */
  slug: string
  /** Optional hex color for visual display (e.g. '#ff5733'). */
  color: string | null
  /** Optional description of the tag. */
  description: string | null
  /** ISO 8601 creation timestamp. */
  createdAt: string
  /** ISO 8601 last-updated timestamp. */
  updatedAt: string
}

TagOwnershipContext

Inputs a {@link TagOwnershipResolver} receives to authorize a tag write.

interface TagOwnershipContext {
  /** The parent resource type from the URL (e.g. `'posts'`, `'products'`). */
  resourceType: string
  /** The parent resource id from the URL. */
  resourceId: string
  /** The authenticated caller's user id. */
  userId: string
}

Types

CreateTagInput

Input for creating a new tag.

type CreateTagInput = Pick<Tag, 'name'> & {
  /** Optional hex color. */
  color?: string | null
  /** Optional description. */
  description?: string | null
}

TagOwnershipResolver

Answers "may this caller write tags on (resourceType, resourceId)?". Return true to allow; anything else (or a throw) denies. Registered per resourceType.

type TagOwnershipResolver = (context: TagOwnershipContext) => boolean | Promise<boolean>

UpdateTagInput

Input for updating an existing tag.

type UpdateTagInput = Partial<Pick<Tag, 'name' | 'color' | 'description'>>

Functions

addTag(req, res)

Adds a tag to a resource. Expects tagId in request body.

Requires an authenticated session and rejects an unauthenticated caller (401) before reading or mutating resource_tags — fail-closed defense-in-depth that does not depend on the authenticate route middleware being wired. Attaching a tag is governed by the owner of the target resource, but that ownership lives in the resource's own package (project/product/…) and is not visible here — this package has no generic cross-resource ownership check — so the gate enforced in this handler is "must be authenticated"; per-resource owner authorization is the responsibility of the resource that mounts this route.

function addTag(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with resourceType and resourceId params, tagId in body.
  • res — The response object.

clearTagOwnershipResolvers()

Clear all registered resolvers (test isolation).

function clearTagOwnershipResolvers(): void

create(req, res)

Creates a new tag with a unique slug derived from the name.

Admin-only and enforced here (not merely via route middleware): tags are a shared global taxonomy with no per-row owner, so creating a new taxonomy entry is the same class of mutation as update/del. A non-admin caller is rejected (401 when unauthenticated, 403 otherwise) before any tag row is inserted — defense-in-depth that does not depend on the requireAdmin route middleware being wired. (Attaching an existing tag to a resource is a different, owner- governed operation — see addTag/removeTag.)

function create(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The incoming request with CreateTagInput body.
  • res — The response object.

del(req, res)

Deletes a tag by ID. Cascade-deletes associated resource_tags via DB constraint.

Admin-only and enforced here (not merely via route middleware): tags are a shared global taxonomy with no per-row owner, so a non-admin caller is rejected (401 when unauthenticated, 403 otherwise) before anything is read or deleted — defense-in-depth that does not depend on the requireAdmin route middleware being wired.

function del(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request object with id param.
  • res — The response object.

getBySlug(req, res)

Gets all resources associated with a tag by slug. Optionally filters by resourceType query parameter.

function getBySlug(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with slug param and optional resourceType query.
  • res — The response object.

getTagOwnershipResolver(resourceType)

Get the registered resolver for a resourceType, or undefined.

function getTagOwnershipResolver(resourceType: string): TagOwnershipResolver | undefined
  • resourceType — The parent resource type.

Returns: The resolver, or undefined when none is registered.

isTagAdmin(res)

Resolves whether the current request's session belongs to an actor authorized to administer tags (update/delete the shared taxonomy). Fail-closed: returns false when there is no authenticated session, and otherwise only true when the session carries an admin claim or a bonded permissions provider grants the manage tag permission.

Use this for in-handler defense-in-depth (it does not depend on the route middleware being preserved by the injector).

function isTagAdmin(res: MoleculeResponse): Promise<boolean>
  • res — The response whose locals.session is inspected.

Returns: true when the session is an authorized tag admin.

isTagWriteAuthorized(context)

Fail-closed authorization for a cross-resource tag write. Denies (false) when no resolver is registered for the resourceType or the resolver throws.

function isTagWriteAuthorized(context: TagOwnershipContext): Promise<boolean>
  • context — The resource + caller to authorize.

Returns: Whether the caller may write tags on the resource.

list(_req, res)

Lists all tags ordered by name.

function list(_req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • _req — The request object.
  • res — The response object.

popular(req, res)

Returns the most popular tags ordered by usage count.

function popular(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request object. Optional query param limit (default 20, max 100).
  • res — The response object.

read(req, res)

Reads a single tag by ID.

function read(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request object with id param.
  • res — The response object.

registerTagOwnershipResolver(resourceType, resolver)

Register the ownership resolver for a parent resourceType. Call once at startup for every resource type whose rows may be tagged.

function registerTagOwnershipResolver(resourceType: string, resolver: TagOwnershipResolver): void
  • resourceType — The parent resource type (matches the route param).
  • resolver — Authorizes a tag write for that resource type.

removeTag(req, res)

Removes a tag from a resource.

Requires an authenticated session and rejects an unauthenticated caller (401) before mutating resource_tags — fail-closed defense-in-depth that does not depend on the authenticate route middleware being wired. Detaching a tag is governed by the owner of the target resource, but that ownership lives in the resource's own package (project/product/…) and is not visible here — this package has no generic cross-resource ownership check — so the gate enforced in this handler is "must be authenticated"; per-resource owner authorization is the responsibility of the resource that mounts this route.

function removeTag(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request with resourceType, resourceId, and tagId params.
  • res — The response object.

requireAdmin()

Route middleware that gates the admin-only tag mutation routes (update, del). Calls next() only for an authenticated admin; otherwise forwards an error to the framework error handler — Unauthorized when no session is present, Forbidden when the session is authenticated but not an admin.

Exposed as a requestHandlerMap key so the injector's route scanner keeps it (unlike the inert global 'authenticate' string, which is dropped).

function requireAdmin(): MoleculeRequestHandler

Returns: An Express-compatible middleware function.

unregisterTagOwnershipResolver(resourceType)

Remove the resolver for a resourceType (returns whether one existed).

function unregisterTagOwnershipResolver(resourceType: string): boolean
  • resourceType — The parent resource type.

Returns: true if a resolver was registered and removed.

update(req, res)

Updates a tag by ID.

Admin-only and enforced here (not merely via route middleware): tags are a shared global taxonomy with no per-row owner, so a non-admin caller is rejected (401 when unauthenticated, 403 otherwise) before anything is read or written — defense-in-depth that does not depend on the requireAdmin route middleware being wired.

function update(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
  • req — The request object with id param and UpdateTagInput body.
  • res — The response object.

Constants

i18nRegistered

Whether i18n registration has completed.

const i18nRegistered: true

requestHandlerMap

Handler map for tag resource routes.

requireAdmin is the admin authorizer middleware referenced by the update/del routes. It must live here (as a real handler-map key) so the mlcl injector's route scanner preserves it — a bare middleware string that isn't a handler-map key is silently dropped.

const requestHandlerMap: {
  readonly create: typeof create
  readonly list: typeof list
  readonly read: typeof read
  readonly update: typeof update
  readonly del: typeof del
  readonly popular: typeof popular
  readonly addTag: typeof addTag
  readonly removeTag: typeof removeTag
  readonly getBySlug: typeof getBySlug
  readonly requireAdmin: MoleculeRequestHandler
}

routes

Route array for tag CRUD plus resource-tagging and popular/slug lookups.

const routes: readonly [
  {
    readonly method: 'post'
    readonly path: '/tags'
    readonly handler: 'create'
    readonly middlewares: readonly ['requireAdmin']
  },
  { readonly method: 'get'; readonly path: '/tags'; readonly handler: 'list' },
  { readonly method: 'get'; readonly path: '/tags/popular'; readonly handler: 'popular' },
  { readonly method: 'get'; readonly path: '/tags/:id'; readonly handler: 'read' },
  {
    readonly method: 'patch'
    readonly path: '/tags/:id'
    readonly handler: 'update'
    readonly middlewares: readonly ['requireAdmin']
  },
  {
    readonly method: 'delete'
    readonly path: '/tags/:id'
    readonly handler: 'del'
    readonly middlewares: readonly ['requireAdmin']
  },
  { readonly method: 'get'; readonly path: '/tags/:slug/resources'; readonly handler: 'getBySlug' },
  {
    readonly method: 'post'
    readonly path: '/:resourceType/:resourceId/tags'
    readonly handler: 'addTag'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'delete'
    readonly path: '/:resourceType/:resourceId/tags/:tagId'
    readonly handler: 'removeTag'
    readonly middlewares: readonly ['authenticate']
  },
]

TAG_ADMIN_PERMISSION

Session-claim permission string ('tag:manage') that, when present in a session's permissions array, grants tag administration without a bonded permissions provider.

const TAG_ADMIN_PERMISSION: 'tag:manage'

TAG_PERMISSION_ACTION

Permission action checked against @molecule/api-permissions for tag administration.

const TAG_PERMISSION_ACTION: 'manage'

TAG_PERMISSION_RESOURCE

Permission resource checked against @molecule/api-permissions for tag administration.

const TAG_PERMISSION_RESOURCE: 'tag'

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-database ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-locales-tag ^1.0.1
  • @molecule/api-logger ^1.0.1
  • @molecule/api-permissions ^1.0.1
  • @molecule/api-resource ^1.0.1

Runtime Dependencies

  • @molecule/api-database
  • @molecule/api-i18n
  • @molecule/api-locales-tag
  • @molecule/api-logger
  • @molecule/api-permissions
  • @molecule/api-resource

The cross-resource tag routes (POST /:resourceType/:resourceId/tags, DELETE /:resourceType/:resourceId/tags/:tagId) are fail-closed: they return 404 until you register an ownership resolver for each taggable resource type. Skipping this leaves the routes denying ALL tag writes (it never opens a cross-tenant hole, but real tagging won't work). Wire it at startup:

import { registerTagOwnershipResolver } from '@molecule/api-resource-tag'
import { findById } from '@molecule/api-database'

// Allow tag writes only by the owner of the parent resource.
registerTagOwnershipResolver('posts', async ({ resourceId, userId }) => {
  const post = await findById('posts', resourceId)
  return post?.userId === userId
})

Taxonomy mutations are ADMIN-ONLY and DENY by default: POST/PATCH/DELETE /tags are gated by the requireAdmin middleware AND re-checked in-handler (fail-closed). "Admin" = an admin session claim (isAdmin, role: 'admin', a 'tag:manage'/'admin' permission string) OR a bonded @molecule/api-permissions grant of manage on tag — grant one of those at startup or every taxonomy write is denied; do NOT strip the gate. The tag read routes (list, popular, read, getBySlug) are PUBLIC by design.

Tables: src/__setup__/tags.sql creates tags and resource_tags. An mlcl-scaffolded API replays __setup__/*.sql automatically on migrate; anywhere else run it once — nothing at runtime creates them.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • [ ] Creating a tag (name, color) through the UI adds it to the tag list and it persists across a reload.
  • [ ] Assigning a tag to a resource the user owns succeeds and the tag chip renders on that resource. If EVERY tag write 404s, the ownership resolver was never registered (the routes are fail-closed) — that is an integration bug to fix, not a pass.
  • [ ] Filtering/browsing by a tag shows exactly the resources carrying it.
  • [ ] Removing a tag from a resource updates the UI immediately and stays removed after a reload.
  • [ ] A user cannot tag or untag another user's resource (denied, no change).
  • [ ] If a popular-tags surface exists, it reflects actual tag usage.