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-review

v1.0.5

Published

Reviews with ratings on any resource — star ratings, helpfulness voting, and aggregate statistics

Downloads

852

Readme

@molecule/api-resource-review

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.

Reviews with ratings resource for molecule.dev.

Polymorphic reviews that attach to any resource type. Supports star ratings (1–5), title/body text, helpfulness voting, and aggregate rating statistics.

Quick Start

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

// Wire routes into your Express app via mlcl inject
// POST   /:resourceType/:resourceId/reviews
// GET    /:resourceType/:resourceId/reviews
// GET    /:resourceType/:resourceId/reviews/rating
// GET    /reviews/:reviewId
// PUT    /reviews/:reviewId
// DELETE /reviews/:reviewId
// POST   /reviews/:reviewId/helpful

Type

resource

Installation

npm install @molecule/api-resource-review @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource zod

API

Interfaces

Review

A review attached to a resource, with a numeric rating and optional body.

interface Review {
  /** Unique review identifier. */
  id: string
  /** The type of resource this review is attached to (e.g. 'product', 'course'). */
  resourceType: string
  /** The ID of the resource this review is attached to. */
  resourceId: string
  /** The ID of the user who created this review. */
  userId: string
  /** Numeric rating (1–5). */
  rating: number
  /** Short review title/summary. */
  title: string
  /** Detailed review body text. */
  body: string
  /** Number of users who marked this review as helpful. */
  helpful: number
  /** When the review was created (ISO 8601). */
  createdAt: string
  /** When the review was last updated (ISO 8601). */
  updatedAt: string
}

CreateReviewInput

Input for creating a new review.

interface CreateReviewInput {
  /** Numeric rating (1–5). */
  rating: number
  /** Short review title/summary. */
  title: string
  /** Detailed review body text. */
  body: string
}

UpdateReviewInput

Input for updating an existing review.

interface UpdateReviewInput {
  /** Updated numeric rating (1–5). */
  rating?: number
  /** Updated review title. */
  title?: string
  /** Updated review body text. */
  body?: string
}

RatingStats

Aggregate rating statistics for a resource.

interface RatingStats {
  /** Average rating across all reviews. */
  average: number
  /** Total number of reviews. */
  count: number
  /** Breakdown of reviews by star rating (1–5). */
  distribution: Record<number, number>
}

PaginationOptions

Options for paginated queries.

interface PaginationOptions {
  /** Maximum number of results to return. */
  limit?: number
  /** Number of results to skip. */
  offset?: number
}

ReviewQuery

Query options for listing reviews with optional sorting.

interface ReviewQuery extends PaginationOptions {
  /** Sort field. Defaults to 'createdAt'. */
  sortBy?: 'createdAt' | 'rating' | 'helpful'
  /** Sort direction. Defaults to 'desc'. */
  sortDirection?: 'asc' | 'desc'
}

PaginatedResult

A paginated result set.

interface PaginatedResult<T> {
  /** The result items for the current page. */
  data: T[]
  /** Total number of matching items across all pages. */
  total: number
  /** Maximum number of results per page. */
  limit: number
  /** Number of results skipped. */
  offset: number
}

ReviewHelpful

A record of a user marking a review as helpful.

interface ReviewHelpful {
  /** The review that was marked helpful. */
  reviewId: string
  /** The user who marked it helpful. */
  userId: string
  /** When it was marked helpful (ISO 8601). */
  createdAt: string
}

Functions

create(req, res)

Creates a new review on a resource.

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

del(req, res)

Deletes a review. Only the review owner can delete.

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

helpful(req, res)

Marks a review as helpful. Idempotent — duplicate votes are silently ignored.

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

list(req, res)

Lists paginated reviews for a resource with optional sorting.

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

averageRating(req, res)

Returns aggregate rating statistics for a resource.

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

read(req, res)

Retrieves a single review by ID.

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

update(req, res)

Updates an existing review. Only the review owner can update.

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

createReview(resourceType, resourceId, userId, data)

Creates a new review on a resource. One review per user per resource.

function createReview(
  resourceType: string,
  resourceId: string,
  userId: string,
  data: CreateReviewInput,
): Promise<Review>
  • resourceType — The type of resource being reviewed.
  • resourceId — The ID of the resource being reviewed.
  • userId — The ID of the reviewing user.
  • data — The review creation input.

Returns: The created review.

getReviewsByResource(resourceType, resourceId, options)

Retrieves paginated reviews for a resource with optional sorting.

function getReviewsByResource(
  resourceType: string,
  resourceId: string,
  options?: ReviewQuery,
): Promise<PaginatedResult<Review>>
  • resourceType — The type of resource.
  • resourceId — The ID of the resource.
  • options — Query options (pagination and sorting).

Returns: A paginated result of reviews.

getReviewById(reviewId)

Retrieves a single review by ID.

function getReviewById(reviewId: string): Promise<Review | null>
  • reviewId — The review ID to look up.

Returns: The review or null if not found.

updateReview(reviewId, userId, data)

Updates a review. Only the review owner can update.

function updateReview(
  reviewId: string,
  userId: string,
  data: UpdateReviewInput,
): Promise<Review | null>
  • reviewId — The review ID to update.
  • userId — The requesting user's ID (must match review owner).
  • data — The update input.

Returns: The updated review or null if not found or unauthorized.

deleteReview(reviewId, userId)

Deletes a review. Only the review owner can delete.

function deleteReview(reviewId: string, userId: string): Promise<boolean>
  • reviewId — The review ID to delete.
  • userId — The requesting user's ID (must match review owner).

Returns: true if deleted, false if not found or unauthorized.

getAverageRating(resourceType, resourceId)

Computes aggregate rating statistics for a resource.

function getAverageRating(resourceType: string, resourceId: string): Promise<RatingStats>
  • resourceType — The type of resource.
  • resourceId — The ID of the resource.

Returns: Rating statistics including average, count, and distribution.

markHelpful(reviewId, userId)

Marks a review as helpful by a user. Idempotent — duplicate votes are ignored.

function markHelpful(reviewId: string, userId: string): Promise<void>
  • reviewId — The review to mark as helpful.
  • userId — The user marking it helpful.

Constants

requestHandlerMap

Handler map for review routes.

const requestHandlerMap: {
  readonly create: typeof create
  readonly list: typeof list
  readonly averageRating: typeof averageRating
  readonly read: typeof read
  readonly update: typeof update
  readonly del: typeof del
  readonly helpful: typeof helpful
}

routes

Routes for review CRUD, rating statistics, and helpfulness voting.

const routes: readonly [
  {
    readonly method: 'post'
    readonly path: '/:resourceType/:resourceId/reviews'
    readonly handler: 'create'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'get'
    readonly path: '/:resourceType/:resourceId/reviews'
    readonly handler: 'list'
  },
  {
    readonly method: 'get'
    readonly path: '/:resourceType/:resourceId/reviews/rating'
    readonly handler: 'averageRating'
  },
  { readonly method: 'get'; readonly path: '/reviews/:reviewId'; readonly handler: 'read' },
  {
    readonly method: 'put'
    readonly path: '/reviews/:reviewId'
    readonly handler: 'update'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'delete'
    readonly path: '/reviews/:reviewId'
    readonly handler: 'del'
    readonly middlewares: readonly ['authenticate']
  },
  {
    readonly method: 'post'
    readonly path: '/reviews/:reviewId/helpful'
    readonly handler: 'helpful'
    readonly middlewares: readonly ['authenticate']
  },
]

createReviewSchema

Schema for validating review creation input.

const createReviewSchema: z.ZodObject<
  { rating: z.ZodNumber; title: z.ZodString; body: z.ZodString },
  z.core.$strip
>

updateReviewSchema

Schema for validating review update input. All fields are optional.

const updateReviewSchema: z.ZodObject<
  {
    rating: z.ZodOptional<z.ZodNumber>
    title: z.ZodOptional<z.ZodString>
    body: z.ZodOptional<z.ZodString>
  },
  z.core.$strip
>

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-database ^1.0.1
  • @molecule/api-i18n ^1.0.1
  • @molecule/api-logger ^1.0.1
  • @molecule/api-resource ^1.0.1
  • zod ^4.0.0

Runtime Dependencies

  • @molecule/api-database

  • @molecule/api-i18n

  • @molecule/api-logger

  • @molecule/api-resource

  • zod

  • List endpoints return a PAGINATED envelope { data, total, limit, offset }, not a bare array — read the rows off result.data (server). On the client, unwrapList(res) from @molecule/app-http normalizes this envelope (pass it the whole HttpResponse), so the rows come back; reading the response as a bare array — or res.data alone (which is the envelope) — yields an EMPTY list. Session-auth prerequisite: the mutating handlers (create, update, del, helpful) read the caller from res.locals.session.userId and fail closed with 401 — mount the routes behind your global auth middleware (the declared authenticate middleware string). Ownership is enforced in the service layer: update/del act only when the review's userId matches the AUTHENTICATED caller (a non-owner gets 404, not 403 — existence is not leaked). Never pass a client-supplied user id.

One review per user per resource: reviews is UNIQUE on (resourceType, resourceId, userId) — a second create for the same resource fails at the database; use update to change an existing review. Helpful votes are idempotent (review_helpful is keyed by (reviewId, userId); duplicates are silently ignored).

The read routes (list, read, averageRating) are PUBLIC by default so anonymous visitors can browse ratings. This package does NOT validate that the reviewed resource exists or is visible to the caller — if reviews attach to private resources in your app, gate these routes behind the parent resource's own access check.

Tables: src/__setup__/reviews.sql creates reviews and review_helpful. An mlcl-scaffolded API replays __setup__/*.sql on migrate (a CREATE TABLE IF NOT EXISTS, so a table the app's own migrations already create keeps only its own columns: ALTER in the ones it lacks); 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 review 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. Verify BEHAVIOR and the business rules, not just that CRUD compiles:

  • [ ] Submitting a review for a resource (its resourceType/resourceId — a product, course, place, whatever this app reviews) persists the rating (1-5) + title + body, and it then appears in that resource's review list and when fetched by its own review id.
  • [ ] The aggregate recomputes correctly (the rating-stats endpoint returns average + count + a per-star distribution): on a resource with no reviews, add a 5-star and a 1-star review -> average is 3, count is 2, distribution shows one 5 and one 1; then edit one rating or delete a review and confirm average/count move to match (the math is live, never stale).
  • [ ] A rating outside 1-5 or non-integer (0, 6, 3.5) is REJECTED with a visible validation error and NO row is written — the average and count do not move; an empty title or empty body is rejected the same way.
  • [ ] One review per user per resource is enforced (the table is UNIQUE on resourceType+resourceId+userId): the same user's SECOND review of the same resource is rejected — no duplicate row inflates the count/average; the way to change their mind is EDITING the existing review, which updates it in place.
  • [ ] Reviews are public and appear immediately — this package has NO moderation or verified-purchase gate and does NOT check the resource exists, was purchased, or isn't the reviewer's own listing; any signed-in user can review any resourceType/resourceId. If the app needs those gates, confirm they are layered on top, and confirm the anonymous read routes only expose reviews for resources the caller is allowed to see.
  • [ ] AUTHORIZATION — a signed-in user can edit/delete ONLY their own review; editing or deleting someone else's returns not-found (404 — existence is not leaked) and leaves that review unchanged. Mutations require a session (signed-out is rejected 401) and the author is taken from the session, so a caller cannot post or edit as another user by supplying a userId.
  • [ ] Helpful voting is idempotent: marking a review helpful bumps its helpful count by exactly one; the same user marking it again does not increment it again (no vote stuffing).