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-dynamic-metadata

v0.0.11

Published

A starter for Medusa plugins.

Readme

medusa-dynamic-metadata

Configuration-driven Medusa v2 plugin for rendering and persisting dynamic metadata fields across admin entity detail pages.

Plugin Overview

medusa-dynamic-metadata lets you define metadata descriptors per entity in plugin options, then automatically renders a typed metadata editor in Medusa Admin widgets for those entity detail pages.

What It Does

  • Resolves metadata configuration from medusa-config.ts (entities map).
  • Detects entity type in admin widgets (zone first, data-shape fallback).
  • Renders a universal metadata form supporting 27 field types (text, number, select, json, relation, media URL/file upload, etc.).
  • Validates descriptor-driven constraints (required, validation.min/max/regex, options validation).
  • Saves metadata to entity-specific admin APIs (or custom endpoint override).
  • Provides admin API for retrieving resolved descriptors by entity.

Problem It Solves

It removes the need to hand-build custom metadata UI and persistence logic per entity. Teams can add/modify metadata fields through plugin configuration instead of repeated frontend/backend boilerplate.

Medusa Version

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


Installation & Setup

Install

npm install medusa-dynamic-metadata
# or
yarn add medusa-dynamic-metadata

Register in medusa-config.ts

import { defineConfig } from "@medusajs/framework"

export default defineConfig({
  plugins: [
    {
      resolve: "medusa-dynamic-metadata",
      options: {
        entities: {
          products: {
            descriptors: [
              { key: "brand", label: "Brand", type: "text", filterable: true },
              { key: "warranty_years", label: "Warranty (Years)", type: "number" },
            ],
          },
        },
      },
    },
  ],
})

Database Migrations

No custom module/entity migrations are present in this plugin source. It uses existing Medusa entity metadata columns and admin endpoints.


Configuration (config.ts / plugin options)

Defined in src/config/metadata-options.ts.

Root Options

| Option | Type | Required | Default | Description | |---|---|---|---|---| | entities | Record<string, EntityMetadataConfig> | Optional | {} | Per-entity metadata configuration keyed by entity type (for example products, orders). |

Entity Metadata Config

| Option | Type | Required | Default | Description | |---|---|---|---|---| | descriptors | MetadataDescriptor[] | Optional | [] | Field definitions to render and persist. | | expose_client_helpers | boolean | Optional | false | Flag stored in normalized config (helper exposure toggle). | | filterable | boolean | Optional | false | Entity-level filterability flag in config. | | widget_zone | string | Optional | Auto-resolved from entity mapping or {singular}.details.after | Admin widget zone override. | | api_endpoint | string | Optional | Derived from entity registry | Admin update endpoint override for saving metadata. |

Metadata Descriptor Fields

| Field | Type | Required | Default | Description | |---|---|---|---|---| | key | string | Yes | - | Metadata key on entity. | | type | MetadataFieldType | Yes | - | Input type and coercion/validation behavior. | | label | string | Optional | key | Display label. | | filterable | boolean | Optional | false | Per-field filterable marker. | | required | boolean | Optional | false | Required validation. | | default_value | unknown | Optional | undefined | Default form value when metadata is missing. | | validation | { min?: number; max?: number; regex?: string } | Optional | undefined | Numeric/regex constraints. | | options | { value: string; label?: string }[] | Optional | undefined | Used for select, multiselect, radio, checkbox. |

Supported MetadataFieldType Values

text, textarea, richtext, markdown, number, integer, float, bool, date, time, datetime, select, multiselect, radio, checkbox, image, video, audio, file, url, email, phone, color, json, object, array, relation

Complete Example Config

{
  resolve: "medusa-dynamic-metadata",
  options: {
    entities: {
      products: {
        descriptors: [
          { key: "brand", label: "Brand", type: "text", filterable: true, required: true },
          { key: "warranty_years", label: "Warranty (Years)", type: "integer", validation: { min: 0, max: 20 } },
          {
            key: "status",
            label: "Status",
            type: "select",
            options: [
              { value: "draft", label: "Draft" },
              { value: "active", label: "Active" },
            ],
          },
          { key: "care_guide", label: "Care Guide", type: "file" },
          { key: "specs", label: "Specs", type: "json" },
        ],
        expose_client_helpers: false,
        filterable: true,
        widget_zone: "product.details.after",
        api_endpoint: "/admin/products/{id}",
      },
      orders: {
        descriptors: [
          { key: "source", label: "Source", type: "text" },
          { key: "priority", label: "Priority", type: "radio", options: [{ value: "normal" }, { value: "high" }] },
        ],
      },
    },
  },
}

Environment Variables

Environment variable usage found in plugin source:

| Variable | Where | Purpose | Required | Example | |---|---|---|---|---| | NODE_ENV | src/admin/components/universal-metadata-widget.tsx | Enables/disables debug/warn logs in non-production mode. | Optional | production |

⚠️ Note: src/modules/README.md contains example text referencing process.env.API_KEY, but it is documentation content, not runtime plugin code.


REST APIs / Routes

1) GET /admin/metadata-config

  • Auth: Admin (admin route namespace)
  • Query params:
    • entity (string, optional, default: "products")
  • Response:
    {
      "metadataDescriptors": [
        {
          "key": "brand",
          "type": "text",
          "label": "Brand"
        }
      ]
    }
  • Behavior:
    • Reads plugin config with resolveDynamicMetadataOptions.
    • Returns empty descriptor array when entity is not configured (no 404).

2) GET /store/metadata-config

  • Auth: Storefront (store route namespace)
  • Query params:
    • entity (string, optional)
  • Response (when entity is provided and exposed):
    {
      "metadataDescriptors": [
        {
          "key": "brand",
          "type": "text",
          "label": "Brand"
        }
      ]
    }
  • Response (when entity is omitted):
    {
      "metadataDescriptorsByEntity": {
        "products": [
          {
            "key": "brand",
            "type": "text",
            "label": "Brand"
          }
        ]
      }
    }
  • Behavior:
    • Returns only entities with expose_client_helpers: true.
    • Returns empty descriptor array when the requested entity is hidden/not configured.

3) POST /admin/product-variants/:id

  • Auth: Admin (admin route namespace)
  • Path params:
    • id (string, required): product variant ID.
  • Body: passthrough payload with optional metadata object plus other variant update fields.
  • Response:
    {
      "product_variant": {
        "id": "variant_...",
        "metadata": {}
      }
    }
  • Behavior:
    • Executes updateProductVariantsWorkflow with selector by ID and update payload.
    • Returns first updated variant as product_variant.

Important Endpoint Examples

# Get metadata descriptors for products
curl -X GET "http://localhost:9000/admin/metadata-config?entity=products" \
  -H "Authorization: Bearer <admin_token>"
# Update metadata on a product variant
curl -X POST "http://localhost:9000/admin/product-variants/variant_123" \
  -H "Authorization: Bearer <admin_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "fabric": "cotton",
      "is_limited": true
    }
  }'
// fetch example for metadata config
const res = await fetch("/admin/metadata-config?entity=orders", {
  credentials: "include",
})
const data = await res.json()

Services

No custom backend service classes are defined in this plugin source.

⚠️ Note: Most logic is implemented as configuration utilities (src/config/*) and shared metadata helpers (src/shared/metadata/utils.ts) consumed by admin UI.


Workflows & Steps (Medusa v2)

No custom workflows or steps are defined in this plugin source.

The plugin calls Medusa core workflow updateProductVariantsWorkflow in POST /admin/product-variants/:id.


Subscribers / Event Hooks

No runtime subscribers/event handlers are defined in this plugin source (only template/readme scaffolding under src/subscribers).


Admin UI / Widgets

Universal Components

  • UniversalMetadataWidget (src/admin/components/universal-metadata-widget.tsx)

    • Detects entity type from widget zone; falls back to data-shape signals.
    • Loads descriptors via useMetadataConfig.
    • Renders MetadataTableWidget only when descriptors exist.
  • MetadataTableWidget (src/admin/components/metadata-table.tsx)

    • Renders typed controls per descriptor.
    • Performs descriptor-based validation.
    • Uploads media/file fields through POST /admin/uploads and stores URL in metadata value.
    • Saves metadata by resolving entity endpoint (resolveApiEndpoint) and calling POST.
    • Invalidates/refetches React Query cache using resolved query keys.

Registered Metadata Widgets (wrappers)

Each file is a minimal wrapper around UniversalMetadataWidget with a fixed zone:

| Entity | Zone | |---|---| | products | product.details.after | | orders | order.details.after | | categories | product_category.details.after | | collections | product_collection.details.after | | customers | customer.details.after | | regions | region.details.after | | sales_channels | sales_channel.details.after | | stores | store.details.after | | promotions | promotion.details.after | | campaigns | campaign.details.after | | price_lists | price_list.details.after | | shipping_profiles | shipping_profile.details.after | | inventory_items | inventory_item.details.after | | product_variants | product_variant.details.after | | product_tags | product_tag.details.after | | product_types | product_type.details.after |

Additional Widget

  • hide-default-metadata (src/admin/widgets/hide-default-metadata.tsx)
    • Zone: product.details.side.before
    • Purpose: DOM-level hiding of default Medusa metadata panel to avoid UI duplication.
    • Interaction: none (automatic MutationObserver behavior).

⚠️ Note: This widget hides DOM elements by structure/class heuristics and may need adjustment if Admin DOM structure changes in future Medusa releases.


Models & Entities

No custom database models/entities are defined in this plugin source.

The plugin operates on existing Medusa entities by reading/writing their metadata field via admin APIs.


Use Cases & Examples

  1. Product enrichment without code changes

    • Add new product metadata fields (brand, care instructions, support URLs) via plugin options.
    • Use: entities.products.descriptors + product.details.after widget.
  2. Operational order annotations

    • Add structured metadata to orders (source channel, routing note, internal tags).
    • Use: entities.orders.descriptors + metadata table UI.
  3. Per-entity metadata governance

    • Make fields required and validated (min/max/regex) to standardize admin data entry.
    • Use: descriptor required and validation.
  4. Variant-level custom attributes

    • Persist metadata on product variants and update through variant route workflow.
    • Use: POST /admin/product-variants/:id.
  5. File/media metadata links

    • Upload file/image/video/audio in metadata form and store uploaded URL in metadata.
    • Use: file/media descriptor types in metadata table.

Troubleshooting

Metadata widget doesn’t appear

  • Cause: no descriptors configured for that entity.
  • Fix: add entities.<entityType>.descriptors in plugin options; rebuild/restart app.

Product type / tag / settings pages: config exists but widget never shows

  • Cause: the admin UI only renders the dynamic table after /admin/metadata-config?entity=… returns descriptors. If the server cannot associate your plugins[] entry with this package (for example resolve is a filesystem path, file:…, or require.resolve("medusa-dynamic-metadata")), options were previously ignored and the widget hid itself (built-in Metadata can still show 0 keys).
  • Fix: use resolve: "medusa-dynamic-metadata" when possible, or any path whose segments include the folder medusa-dynamic-metadata (path-based resolve values are supported). Restart the server after changing config.
  • Verify: curl -sS "http://localhost:9000/admin/metadata-config?entity=product_types" -H "Authorization: Bearer <token>" should return a non-empty metadataDescriptors array when product_types is configured.
  • Admin bundle: after upgrading or developing the plugin locally, run medusa plugin:build (or npm run build in the package) and restart so Admin loads the latest widgets (for example product-types-metadata-table).

“Unable to load metadata configuration”

  • Cause: admin can’t reach /admin/metadata-config or plugin not loaded.
  • Fix: verify plugin registration in medusa-config.ts, rebuild plugin, restart Medusa server.

“No API endpoint configured for

  • Cause: entity type lacks mapping in registry and no api_endpoint override configured.
  • Fix: set api_endpoint (and optionally widget_zone) for that entity in plugin options.

Save fails with backend error

  • Cause: invalid payload, endpoint mismatch, or authorization issue.
  • Fix: verify resolved endpoint, admin auth/session, and descriptor/value shape.

File upload fails in metadata field

  • Cause: /admin/uploads request fails or no URL returned.
  • Fix: confirm uploads endpoint availability and permissions; inspect server response message shown in toast.

Default and custom metadata sections both visible

  • Cause: hide widget not active for current zone/page or DOM structure mismatch.
  • Fix: verify hide-default-metadata widget registration and adjust selector logic if Admin markup changed.