medusa-dynamic-metadata
v0.0.11
Published
A starter for Medusa plugins.
Maintainers
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(entitiesmap). - 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-metadataRegister 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.mdcontains example text referencingprocess.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).
- Reads plugin config with
2) GET /store/metadata-config
- Auth: Storefront (store route namespace)
- Query params:
entity(string, optional)
- Response (when
entityis provided and exposed):{ "metadataDescriptors": [ { "key": "brand", "type": "text", "label": "Brand" } ] } - Response (when
entityis 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.
- Returns only entities with
3) POST /admin/product-variants/:id
- Auth: Admin (admin route namespace)
- Path params:
id(string, required): product variant ID.
- Body: passthrough payload with optional
metadataobject plus other variant update fields. - Response:
{ "product_variant": { "id": "variant_...", "metadata": {} } } - Behavior:
- Executes
updateProductVariantsWorkflowwith selector by ID and update payload. - Returns first updated variant as
product_variant.
- Executes
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
MetadataTableWidgetonly 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/uploadsand stores URL in metadata value. - Saves metadata by resolving entity endpoint (
resolveApiEndpoint) and callingPOST. - 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).
- Zone:
⚠️ 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
Product enrichment without code changes
- Add new product metadata fields (brand, care instructions, support URLs) via plugin options.
- Use:
entities.products.descriptors+product.details.afterwidget.
Operational order annotations
- Add structured metadata to orders (source channel, routing note, internal tags).
- Use:
entities.orders.descriptors+ metadata table UI.
Per-entity metadata governance
- Make fields required and validated (
min/max/regex) to standardize admin data entry. - Use: descriptor
requiredandvalidation.
- Make fields required and validated (
Variant-level custom attributes
- Persist metadata on product variants and update through variant route workflow.
- Use:
POST /admin/product-variants/:id.
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>.descriptorsin 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 yourplugins[]entry with this package (for exampleresolveis a filesystem path,file:…, orrequire.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 foldermedusa-dynamic-metadata(path-basedresolvevalues 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-emptymetadataDescriptorsarray whenproduct_typesis configured. - Admin bundle: after upgrading or developing the plugin locally, run
medusa plugin:build(ornpm run buildin the package) and restart so Admin loads the latest widgets (for exampleproduct-types-metadata-table).
“Unable to load metadata configuration”
- Cause: admin can’t reach
/admin/metadata-configor 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_endpointoverride configured. - Fix: set
api_endpoint(and optionallywidget_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/uploadsrequest 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-metadatawidget registration and adjust selector logic if Admin markup changed.
