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

@zaamx/netme-bundle

v0.0.6

Published

A starter for Medusa plugins.

Downloads

486

Readme

@zaamx/netme-bundle

A Medusa v2 plugin for creating and managing product bundles — linking a parent product to one or more child products with configurable quantities, pricing rules, and metadata.

Version: 0.0.4 | License: MIT | Requires: Medusa >= 2.4.0 | Node: >= 20


Table of Contents


Overview

@zaamx/netme-bundle adds full product bundle management to a Medusa v2 backend. It exposes:

  • A bundle module with its own database table and service
  • A module link connecting Medusa's ProductModule to the bundle module
  • Workflows for creating, updating, and deleting bundles
  • Admin REST API routes for CRUD operations
  • Store REST API routes for storefront consumption and cart integration with dynamic pricing
  • An Admin UI page with full CRUD, infinite scroll product selection, and metadata management

Installation

npm install @zaamx/netme-bundle
# or
yarn add @zaamx/netme-bundle

Registration

Add the plugin to your medusa-config.ts:

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

export default defineConfig({
  plugins: [
    {
      resolve: "@zaamx/netme-bundle",
      options: {},
    },
  ],
  // ...
})

After registering, run database migrations:

npx medusa db:migrate

Architecture

@zaamx/netme-bundle
├── modules/bundles          — Bundle data model, service, migrations
├── links/product-bundle     — Module link: Product ↔ Bundle
├── workflows/               — update-product-bundle, delete-product-bundle
├── api/admin/               — Admin REST routes
├── api/store/               — Store REST routes
└── admin/                   — Admin dashboard UI (routes, components)

The plugin follows Medusa's modular architecture: the bundle module is isolated from the core ProductModule and connected via a module link. All write operations go through workflows that invoke the bundle service.


Data Model

File: src/modules/bundles/models/bundle.ts

| Field | Type | Default | Description | |---|---|---|---| | id | TEXT | — | Primary key, matches the associated product ID | | child_products | JSONB | {"data":[]} | Array of child product configurations | | bundle_meta | JSONB | {"data":[]} | Array of arbitrary key-value metadata pairs | | is_bundle | BOOLEAN | false | Whether this record represents an active bundle | | created_at | TIMESTAMPTZ | now() | Creation timestamp | | updated_at | TIMESTAMPTZ | now() | Last update timestamp | | deleted_at | TIMESTAMPTZ | NULL | Soft delete timestamp |

An index IDX_bundle_deleted_at is created on deleted_at for efficient active-bundle queries.


Module

File: src/modules/bundles/index.ts

export const BUNDLE_MODULE = "bundle"
export default Module(BUNDLE_MODULE, { service: BundleService })

BundleService

Extends MedusaService with the Bundle model. Inherits CRUD methods:

| Method | Description | |---|---| | listBundles(filters?, config?) | List bundles with optional filters | | createBundles(data) | Create one or more bundles | | updateBundles(data) | Update one or more bundles | | deleteBundles(ids) | Soft-delete bundles by ID | | attachBundleToProduct(productId) | Retrieve bundle data for a product with child_products relation |


Module Link

File: src/links/product-bundle.ts

export default defineLink(
  ProductModule.linkable.product,
  BundleModule.linkable.bundle
)

This link associates each bundle record with a Medusa product while keeping both modules independent. It enables querying products with their bundle data through Medusa's query layer.


Workflows

updateProductBundle

File: src/workflows/update-product-bundle.ts

Creates or updates a bundle for a given product.

Input (UpdateProductBundleInput):

{
  product_id: string
  is_bundle?: boolean
  child_product_ids?: Array<{
    id: string
    min_quantity?: number
    max_quantity?: number
    default_quantity?: number
    optional?: boolean
    separate_shipping?: boolean
    individual_price?: boolean
  }>
  bundle_meta?: Array<{
    key: string
    value: string
  }>
}

Steps:

  1. Resolve bundle and product services via MedusaModules
  2. Check if a bundle already exists for product_id
  3. If none exists and is_bundle=true: create a new bundle record
  4. If one exists: update with the provided fields
  5. Return the product with enriched bundle data

deleteProductBundle

File: src/workflows/delete-product-bundle.ts

Removes the bundle association from a product.

Input (DeleteProductBundleInput):

{
  product_id: string
}

Steps:

  1. Find the bundle record linked to product_id
  2. Delete it if found
  3. Return the updated product

API Reference

Admin Routes

All admin routes are authenticated and accessible under /admin/.


GET /admin/bundled-products

Returns all bundled products with enriched child product data.

Query params: limit, offset

Response:

{
  "bundled_products": [
    {
      "id": "bundle_01J...",
      "title": "Product Title",
      "product": { "id": "prod_01J..." },
      "items": [
        {
          "id": "prod_child_01J...",
          "product": { "id": "prod_01J...", "title": "Child Title" },
          "quantity": 1,
          "min_quantity": 1,
          "max_quantity": 5,
          "optional": false,
          "separate_shipping": false,
          "individual_price": false
        }
      ],
      "bundle_meta": [{ "key": "promo", "value": "summer" }],
      "is_bundle": true,
      "created_at": "2025-01-01T00:00:00Z",
      "updated_at": "2025-01-01T00:00:00Z"
    }
  ],
  "count": 10,
  "limit": 15,
  "offset": 0
}

POST /admin/bundled-products

Creates a new bundled product.

Body: UpdateProductBundleInput (see Workflows)


GET /admin/products/:id/bundle

Retrieves bundle data for a specific product.

Response: Bundle object for the product, or empty if none.


POST /admin/products/:id/bundle

Creates or replaces the bundle for a product. Always deletes the existing bundle first, then applies the new configuration.

Body: UpdateProductBundleInput


DELETE /admin/products/:id/bundle

Removes the bundle from a product.


GET /admin/plugin

Health check. Returns HTTP 200.


Store Routes

Unauthenticated routes for the storefront.


GET /store/products/:id/bundle

Returns the bundle configuration for a product, with enriched child product data.

Response: Same structure as the admin single-product bundle endpoint.


POST /store/carts/:id/bundle-line-items

Adds items to a cart with bundle-aware dynamic pricing.

Body:

{
  items: Array<{
    variant_id: string
    quantity: number
    unit_price?: number
    metadata?: {
      bundle_id?: string
      bundled_by?: string           // if set → price = 0.001 (free)
      bundle_sticks_price?: {
        nuevoPrecioPaquete: number  // if set → use this price
      }
      price_rules?: {
        nuevoPrecio: number         // if set → use this price
      }
    }
  }>
}

Pricing logic (applied per item):

| Condition | Price Applied | |---|---| | metadata.bundled_by is set | 0.001 (effectively free child item) | | metadata.bundle_sticks_price is set | bundle_sticks_price.nuevoPrecioPaquete | | metadata.price_rules is set | price_rules.nuevoPrecio | | None of the above | Default Medusa pricing |


GET /store/plugin

Health check. Returns HTTP 200.


Admin UI

The plugin registers a custom page in the Medusa Admin dashboard.

Bundled Products Page

Route: /bundled-products (sidebar entry with a cube icon, label "Bundled Products")

Features:

| Feature | Details | |---|---| | Bundle list | Table with columns: ID, Title, Items count, Product link, Status | | Pagination | 15 items per page | | Create bundle | Modal with product selector and item configuration | | Edit bundle | Same modal, product selector disabled during edit | | Delete bundle | Confirmation + API call | | Infinite scroll | Product selector loads products incrementally via Intersection Observer | | Bundle metadata | Key-value pairs, add/remove dynamically |

Per-Item Configuration (in Create/Edit modal)

Each child product entry in a bundle supports:

  • Product selector (dropdown with search, infinite scroll)
  • min_quantity, max_quantity, default_quantity number inputs
  • Toggle: optional
  • Toggle: separate_shipping
  • Toggle: individual_price
  • Remove button

Types

// Child product configuration stored in bundle.child_products
type ChildProductConfig = {
  id: string
  min_quantity?: number
  max_quantity?: number
  default_quantity?: number
  optional?: boolean
  separate_shipping?: boolean
  individual_price?: boolean
}

// Key-value metadata stored in bundle.bundle_meta
type BundleMeta = {
  key: string
  value: string
}

// Input for create/update workflow
type UpdateProductBundleInput = {
  product_id: string
  is_bundle?: boolean
  child_product_ids?: ChildProductConfig[]
  bundle_meta?: BundleMeta[]
}

// Input for delete workflow
type DeleteProductBundleInput = {
  product_id: string
}

Database Schema

Migration: src/modules/bundles/migrations/Migration20241229065751.ts

CREATE TABLE IF NOT EXISTS "bundle" (
  "id"              TEXT        NOT NULL,
  "child_products"  JSONB       NOT NULL DEFAULT '{"data":[]}',
  "bundle_meta"     JSONB       NOT NULL DEFAULT '{"data":[]}',
  "is_bundle"       BOOLEAN     NOT NULL DEFAULT false,
  "created_at"      TIMESTAMPTZ NOT NULL DEFAULT now(),
  "updated_at"      TIMESTAMPTZ NOT NULL DEFAULT now(),
  "deleted_at"      TIMESTAMPTZ          DEFAULT NULL,
  CONSTRAINT "bundle_pkey" PRIMARY KEY ("id")
);

CREATE INDEX IF NOT EXISTS "IDX_bundle_deleted_at"
  ON "bundle" ("deleted_at");

Build & Development

Scripts:

| Command | Description | |---|---| | yarn build | Build plugin for production (medusa plugin:build) | | yarn dev | Start plugin in development mode (medusa plugin:develop) |

Output directory: .medusa/server/

Package exports (from built output):

| Export | Path | |---|---| | @zaamx/netme-bundle/workflows | .medusa/server/src/workflows/index.js | | @zaamx/netme-bundle/modules/* | .medusa/server/src/modules/*/index.js | | @zaamx/netme-bundle/admin | .medusa/server/src/admin/index.mjs | | @zaamx/netme-bundle/* | .medusa/server/src/*.js |