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

medusa-manual-product-suggestion

v0.0.3

Published

A starter for Medusa plugins.

Downloads

25

Readme

medusa-manual-product-suggestion

Medusa v2 plugin for manually managing per-product “similar” and “also like” product recommendations through admin APIs and admin UI widgets.

Plugin Overview

medusa-manual-product-suggestion adds a dedicated module for product-level manual recommendation mapping.
For each product, it stores:

  • similar: array of related product IDs
  • also_like: array of “you may also like” product IDs

It includes:

  • database model + migration
  • admin CRUD APIs (/admin/product-suggestions)
  • admin widget on product details for selecting suggestion products with a picker modal

Problem It Solves

It gives merchandisers/support teams a simple way to curate product recommendations manually (without algorithmic engines), and persist those associations in a normalized plugin-managed table.

Medusa Version

Built for Medusa v2 (uses @medusajs/framework / @medusajs/medusa 2.12.4).

Installation & Setup

Install

npm install medusa-manual-product-suggestion

or

yarn add medusa-manual-product-suggestion

Register in medusa-config.ts

import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  plugins: [
    {
      resolve: "medusa-manual-product-suggestion",
      options: {},
    },
  ],
})

Run Migrations

npx medusa db:migrate

This creates the product_suggestion table and indexes.

Configuration (config.ts / plugin options)

No plugin-specific runtime configuration file or options are consumed by this plugin’s source code.

| Option | Type | Required | Default | Description | |---|---|---|---|---| | None | - | - | - | The plugin currently has no custom options in code. |

Example Plugin Config Block

plugins: [
  {
    resolve: "medusa-manual-product-suggestion",
    options: {},
  },
]

Environment Variables

No runtime process.env.* usage exists in this plugin source code.

| Variable | Required | Purpose | Example | |---|---|---|---| | None | No | Plugin logic does not read environment variables directly. | - |

⚠️ Note: src/modules/README.md contains an example reference to process.env.API_KEY, but it is documentation/example content and not used by runtime plugin code.

REST APIs / Routes

Custom functional routes are admin-scoped under /admin/product-suggestions.
Two additional plugin health endpoints are also present.

Authentication

  • /admin/product-suggestions/*: Admin JWT/session required (admin API scope)
  • /admin/plugin and /store/plugin: health endpoints returning HTTP 200

GET /admin/product-suggestions

Lists product suggestions with optional filtering and pagination.

Query Params

| Name | Type | Required | Default | Description | |---|---|---|---|---| | product_id | string | No | - | Filter by product ID. | | limit | number | No | 20 | Max results (<=100). | | offset | number | No | 0 | Pagination offset. | | order | "created_at" \| "updated_at" \| "product_id" | No | created_at | Sort field. | | order_direction | "ASC" \| "DESC" | No | DESC | Sort direction. |

Response Schema

{
  "suggestions": [
    {
      "id": "psug_...",
      "product_id": "prod_...",
      "similar": ["prod_..."],
      "also_like": ["prod_..."]
    }
  ],
  "count": 1,
  "offset": 0,
  "limit": 20
}

POST /admin/product-suggestions

Creates a suggestion record for a product.

Request Body

| Field | Type | Required | Description | |---|---|---|---| | product_id | string | Yes | Product ID owning these suggestions. | | similar | string[] | No | Similar product IDs (defaults to empty array). | | also_like | string[] | No | Also-like product IDs (defaults to empty array). |

Response Schema

{
  "suggestion": {
    "id": "psug_...",
    "product_id": "prod_...",
    "similar": [],
    "also_like": []
  }
}

GET /admin/product-suggestions/:id

Returns a single suggestion by record ID.

Response Schema

{
  "suggestion": {
    "id": "psug_...",
    "product_id": "prod_...",
    "similar": ["prod_..."],
    "also_like": ["prod_..."]
  }
}

PUT /admin/product-suggestions/:id

Partially updates arrays on an existing suggestion.

Request Body

| Field | Type | Required | Description | |---|---|---|---| | similar | string[] | No | Replaces the similar list when provided. | | also_like | string[] | No | Replaces the also_like list when provided. |

Response Schema

{
  "suggestion": {
    "id": "psug_...",
    "product_id": "prod_...",
    "similar": ["prod_..."],
    "also_like": ["prod_..."]
  }
}

DELETE /admin/product-suggestions/:id

Deletes a suggestion record.

Response Schema

{
  "id": "psug_...",
  "object": "product_suggestion",
  "deleted": true
}

Health Endpoints

  • GET /admin/plugin -> 200
  • GET /store/plugin -> 200

Important Endpoint Examples

curl -X POST "http://localhost:9000/admin/product-suggestions" \
  -H "Authorization: Bearer <admin_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "prod_01ABC",
    "similar": ["prod_01DEF", "prod_01GHI"],
    "also_like": ["prod_01JKL"]
  }'
curl -X GET "http://localhost:9000/admin/product-suggestions?product_id=prod_01ABC&limit=20&offset=0&order=updated_at&order_direction=DESC" \
  -H "Authorization: Bearer <admin_jwt>"
curl -X PUT "http://localhost:9000/admin/product-suggestions/psug_01XYZ" \
  -H "Authorization: Bearer <admin_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "similar": ["prod_01DEF"],
    "also_like": ["prod_01JKL", "prod_01MNO"]
  }'
await fetch("/admin/product-suggestions/psug_01XYZ", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${adminToken}`,
  },
})

Services

ProductSuggestionModuleService

Defined in src/modules/product-suggestion/service.ts.

Manages persistence and retrieval of suggestion records, with duplicate protection on product_id.

| Method | Signature | Description | |---|---|---| | getByProductId | (product_id: string) => Promise<ProductSuggestionDTO \| null> | Fetches suggestion row by product ID. | | getSuggestion | (id: string) => Promise<ProductSuggestionDTO> | Retrieves by record ID; throws not found if missing. | | createSuggestion | (input: CreateProductSuggestionInput) => Promise<ProductSuggestionDTO> | Creates a suggestion; rejects duplicate product_id. | | updateSuggestion | (id: string, input: UpdateProductSuggestionInput) => Promise<ProductSuggestionDTO> | Partial update for similar / also_like. | | listSuggestions | (selector?, config?) => Promise<{ suggestions: ProductSuggestionDTO[]; count: number }> | List + count with pagination/order. | | deleteSuggestion | (id: string) => Promise<void> | Deletes a suggestion after existence check. |

Key DTOs:

  • ProductSuggestionDTO: { id, product_id, similar, also_like }
  • CreateProductSuggestionInput
  • UpdateProductSuggestionInput

Workflows & Steps (Medusa v2)

No Medusa workflows (createWorkflow) or custom steps (createStep) are defined in this plugin.

Subscribers / Event Hooks

No event subscribers are defined in this plugin.

Admin UI / Widgets

Product Suggestion Widget

  • File: src/admin/widgets/product-suggestion-widget.tsx
  • Placement: zone: ["product.details.after", "product.details.side.after"]
  • Renders:
    • “Product Suggestion” container
    • Similar section with count + “Add Similar” button
    • Also-like section with count + “Add Also like” button
    • Modal picker for selecting product IDs
  • User Interactions:
    • load current suggestion for the product
    • open modal in either mode (similar / also_like)
    • save selected IDs (create new suggestion or update existing one)
  • Data Consumed:
    • widget data.id as productId
    • admin API endpoints:
      • GET /admin/product-suggestions?product_id=...&limit=1
      • POST /admin/product-suggestions
      • PUT /admin/product-suggestions/:id

Product Picker Modal

  • File: src/admin/components/ProductPickerModal.tsx
  • Renders:
    • searchable product list from /admin/products
    • checkbox list + select-all support
    • Save/Cancel footer actions
  • Behavior:
    • excludes current product from selectable list
    • preselects existing chosen IDs
    • returns selected product IDs to widget onSave

Models & Entities

product_suggestion

Defined in src/modules/product-suggestion/models/product-suggestion.ts and created via migration Migration20260220000000.

| Field | Type | Nullable | Description | |---|---|---|---| | id | VARCHAR | No | Primary key | | product_id | VARCHAR | No | Referenced Medusa product ID (stored as text, searchable) | | similar | jsonb | Yes | Array of suggested similar product IDs | | also_like | jsonb | Yes | Array of suggested “also like” product IDs | | created_at | TIMESTAMP WITH TIME ZONE | No | Creation timestamp | | updated_at | TIMESTAMP WITH TIME ZONE | No | Update timestamp | | deleted_at | TIMESTAMP WITH TIME ZONE | Yes | Soft-delete timestamp |

Indexes / constraints:

  • Unique partial index on product_id where deleted_at IS NULL
  • Index on deleted_at

Relationships:

  • No explicit ORM relation is declared to Medusa product model; linkage is by stored product_id string.

Use Cases & Examples

  1. Manual cross-sell curation by merchandisers

    • Scenario: team wants handpicked “similar products” on PDP.
    • Use: admin widget on product details or POST/PUT /admin/product-suggestions.
  2. Curated “You may also like” rails

    • Scenario: marketing sets a different list than strict similarity.
    • Use: also_like field managed from widget modal.
  3. Internal admin tooling integration

    • Scenario: build custom admin pages around suggestions.
    • Use: GET /admin/product-suggestions with filters + pagination.
  4. Storefront recommendation rendering

    • Scenario: frontend loads suggestion IDs and resolves product cards.
    • Use: service getByProductId(productId) in custom route/workflow.
  5. Operational cleanup

    • Scenario: remove stale suggestion mappings.
    • Use: DELETE /admin/product-suggestions/:id.

Troubleshooting

Duplicate suggestion creation fails

  • Symptom: conflict error when creating a suggestion for an already-mapped product.
  • Cause: one active suggestion row per product_id is enforced.
  • Fix: use PUT /admin/product-suggestions/:id to update existing row, or delete old row first.

Validation errors on create/update/list

  • Symptom: 400 with Zod validation details.
  • Cause: invalid body/query types (e.g., empty product_id, invalid arrays, bad pagination/order values).
  • Fix: match validator contracts in validators.ts.

Widget not showing in admin product page

  • Cause: plugin not registered, admin bundle stale, or server not restarted.
  • Fix:
    • ensure plugin is in plugins array
    • rebuild/restart Medusa
    • hard refresh admin app

Suggestion data not loaded in widget

  • Cause: admin API request failure or no existing row for product.
  • Fix: check /admin/product-suggestions?product_id=<id>&limit=1 response and auth/session.

Missing database table errors

  • Cause: migration not executed.
  • Fix: run npx medusa db:migrate.