@tinloof/medusa-sanity-sync
v0.1.1
Published
A flexible and extensible Medusa plugin for syncing data to Sanity CMS.
Readme
@tinloof/medusa-sanity-sync
A flexible and extensible Medusa plugin for syncing data to Sanity CMS.
Features
- Multi-entity support: Sync products, categories, collections, and custom entities
- Single transformer: One top-level transformer function handles all entity transformations
- Draft-aware: Updates target drafts when they exist, deletes remove both published and draft documents
- Event batching: Configurable batching with deduplication for efficient syncing
- Custom sync handlers: Override create/update/delete behavior (e.g., archive instead of delete)
- Admin UI widgets: Visual sync status for all entity types
- Non-destructive updates: Only sets fields returned by the transformer, preserving Sanity-managed fields
Installation
npm install @tinloof/medusa-sanity-sync
# or
yarn add @tinloof/medusa-sanity-syncQuick Start
1. Configure the Plugin
Add the plugin to your medusa-config.ts:
import { defineConfig } from "@medusajs/medusa";
export default defineConfig({
// ... other config
plugins: [
{
resolve: "@tinloof/medusa-sanity-sync",
options: {
api_token: process.env.SANITY_API_TOKEN,
project_id: process.env.SANITY_PROJECT_ID,
api_version: "2024-01-01",
dataset: "production",
studio_url: process.env.SANITY_STUDIO_URL,
},
},
],
});Configuration Options
| Option | Type | Required | Description |
| ------------- | --------------------- | -------- | ------------------------------------------ |
| api_token | string | Yes | Sanity API token with write permissions |
| project_id | string | Yes | Sanity project ID |
| api_version | string | Yes | Sanity API version (e.g., "2024-01-01") |
| dataset | string | Yes | Sanity dataset name |
| studio_url | string | No | URL to Sanity Studio for generating links |
| entities | EntityDefinition[] | No | Entity configurations (defaults to all) |
| transformer | TransformerFunction | No | Top-level transformer for all entity types |
| batching | BatcherConfig | No | Event batching config (interval, max size) |
| syncHandlers | SyncHandlers | No | Custom create/update/delete handlers |
Transformer
The transformer is a single function at the plugin level that handles transformations for all entity types:
import type { SanitySyncPluginOptions } from "@tinloof/medusa-sanity-sync";
const options: SanitySyncPluginOptions = {
api_token: process.env.SANITY_API_TOKEN,
project_id: process.env.SANITY_PROJECT_ID,
api_version: "2024-01-01",
dataset: "production",
// Single transformer for all entities
transformer: (entityType, data) => {
switch (entityType) {
case "product":
return {
title: data.title,
handle: data.handle,
description: data.description,
images: data.images?.map((img) => ({ url: img.url })),
};
case "product_category":
return {
name: data.name,
handle: data.handle,
};
case "product_collection":
return {
title: data.title,
handle: data.handle,
};
default:
return data;
}
},
};Important: Only fields returned by the transformer are set on the Sanity document. Fields managed in Sanity (e.g., rich text, images, sections) are preserved — the plugin never unsets fields not in the transformer output.
Default Transformer
If no transformer is provided, a default transformer is used that maps basic fields:
- Products:
title,handle - Categories:
name,handle - Collections:
title,handle - Unknown entities: All fields except internal ones (
id,created_at,updated_at,deleted_at)
You can import and extend the default transformer:
import { defaultTransformer } from "@tinloof/medusa-sanity-sync";
const options: SanitySyncPluginOptions = {
// ... connection config
transformer: (entityType, data) => {
if (entityType === "product") {
return {
...defaultTransformer(entityType, data),
description: data.description,
customField: data.metadata?.custom_value,
};
}
return defaultTransformer(entityType, data);
},
};Entity Configuration
Using Default Entities
The plugin includes default configurations for products, categories, and collections with explicit query fields:
import {
defaultProductEntity,
defaultCategoryEntity,
defaultCollectionEntity,
defaultEntities,
} from "@tinloof/medusa-sanity-sync";
// Use all defaults - simplest setup
const options = {
// ... connection config
entities: defaultEntities,
};
// Or select specific entities
const options = {
// ... connection config
entities: [defaultProductEntity, defaultCategoryEntity],
};Query Fields
Each entity definition specifies which fields to fetch from Medusa via queryFields. The default entities include sensible defaults:
- Products:
id,title,handle,subtitle,description,status,thumbnail - Categories:
id,name,handle,description,is_active,is_internal,rank,parent_category_id - Collections:
id,title,handle
To fetch additional fields or relations, extend the defaults:
import { defaultProductEntity } from "@tinloof/medusa-sanity-sync";
const options = {
// ... connection config
entities: [
{
...defaultProductEntity,
// Add relations to the existing query fields
queryFields: [
...defaultProductEntity.queryFields,
"images.*",
"variants.*",
],
},
],
};Note: If
queryFieldsis omitted, a*wildcard is used. This works for some entity types (e.g., products) but may not return all scalar fields for others (e.g., categories, collections). When in doubt, specify fields explicitly.
Custom Entity Definition
Create your own entity definitions for any Medusa data type:
import type { EntityDefinition } from "@tinloof/medusa-sanity-sync";
const myCustomEntity: EntityDefinition = {
// Medusa entity name (used in queries)
medusaEntity: "my_custom_entity",
// Sanity document type
sanityType: "customEntity",
// Events that trigger sync
events: [
"my-custom-entity.created",
"my-custom-entity.updated",
"my-custom-entity.deleted",
],
// Fields to fetch from Medusa
queryFields: ["id", "name", "description"],
};
// Handle it in your transformer
const options = {
// ... connection config
transformer: (entityType, data) => {
if (entityType === "my_custom_entity") {
return {
name: data.name,
description: data.description,
};
}
// ... handle other entities
},
entities: [myCustomEntity],
};Draft-Aware Sync
The plugin is fully draft-aware:
- Updates: If a Sanity draft exists (
drafts.<id>), the draft is updated. Otherwise the published document is updated. - Deletes: Both the published document and its draft are deleted.
- Creates: Documents are created with
createIfNotExiststo avoid duplicates.
This means editors can work on drafts in Sanity Studio without their changes being overwritten — Medusa synced fields stay current on whichever version is active.
Transformation Pipeline
The plugin uses a simple transformation pipeline:
- Transformer: The top-level transformer function transforms the data for Sanity.
- Sanity Metadata:
_typeand_idare added automatically.
Event → Batcher → Transformer → Add _type/_id → Sanity APISubscribers
Default Subscribers
The plugin includes a unified subscriber for:
- Products (
product.created,product.updated,product.deleted) - Categories (
product-category.created,product-category.updated,product-category.deleted) - Collections (
product-collection.created,product-collection.updated,product-collection.deleted)
Custom Subscribers
Create subscribers for custom entities using the factory:
// src/subscribers/my-custom-entity-sync.ts
import { createEntitySubscriber } from "@tinloof/medusa-sanity-sync/subscribers";
const { handler, config } = createEntitySubscriber({
entityType: "my_custom_entity",
events: [
"my-custom-entity.created",
"my-custom-entity.updated",
"my-custom-entity.deleted",
],
});
export default handler;
export { config };Workflows
Use syncEntitiesWorkflow for syncing any entity type:
import { syncEntitiesWorkflow } from "@tinloof/medusa-sanity-sync/workflows";
// Sync specific products
await syncEntitiesWorkflow(container).run({
input: {
entityType: "product",
ids: ["prod_123", "prod_456"],
},
});
// Sync all categories
await syncEntitiesWorkflow(container).run({
input: {
entityType: "product_category",
},
});API Routes
Sync
# Sync a specific entity
POST /admin/sanity/entities/:entityType/:id/sync
# Sync all registered entities
POST /admin/sanity/syncs
# Get sync history
GET /admin/sanity/syncsDocuments
# Get a Sanity document by ID
GET /admin/sanity/documents/:idAdmin UI
Widgets
The plugin provides sync status widgets for:
- Product details page
- Category details page
- Collection details page
Each widget shows:
- Current sync status (synced/not synced)
- Button to trigger manual sync
- View Sanity document JSON
Sync History Page
Access the sync history at /app/sanity in the admin dashboard.
Advanced Usage
Accessing the Sanity Client
import { SANITY_MODULE } from "@tinloof/medusa-sanity-sync";
const sanityModule = container.resolve(SANITY_MODULE);
// Get the raw Sanity client
const client = sanityModule.getClient();
// Query Sanity directly
const products = await client.fetch('*[_type == "product"]');Custom Widget for New Entity Types
// src/admin/widgets/my-entity-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk";
import { SyncWidget } from "@tinloof/medusa-sanity-sync/admin";
const MyEntityWidget = ({ data }) => (
<SyncWidget data={data} entityType="my_custom_entity" compareField="name" />
);
export const config = defineWidgetConfig({
zone: "my_custom_entity.details.after",
});
export default MyEntityWidget;Event Batching
Events are batched for efficient processing. Configure batching behavior:
const options: SanitySyncPluginOptions = {
// ... connection config
batching: {
flushInterval: 10000, // Flush every 10 seconds (default)
maxBatchSize: 50, // Flush after 50 events (default)
enabled: true, // Set false to sync immediately per event
},
};Deduplication
The batcher automatically deduplicates events for the same entity:
| Sequence | Result | | ---------------- | -------------------- | | create + update | create (latest data) | | create + delete | skip both | | update + update | update (latest data) | | update + delete | delete only |
Custom Sync Handlers
Override the default create/update/delete behavior:
const options: SanitySyncPluginOptions = {
// ... connection config
syncHandlers: {
onCreate: async (entityType, id, data, ctx) => {
await ctx.sanityClient.create({ _type: entityType, _id: id, ...data });
},
onUpdate: async (entityType, id, data, ctx) => {
await ctx.sanityClient.patch(id).set(data).commit();
},
onDelete: async (entityType, id, ctx) => {
// Archive instead of delete
await ctx.sanityClient.patch(id).set({ archived: true }).commit();
},
},
};TypeScript
Full TypeScript support with exported types:
import type {
EntityDefinition,
SanitySyncPluginOptions,
SyncEntitiesInput,
SyncResult,
SyncHandlers,
SyncContext,
TransformerFunction,
BatcherConfig,
} from "@tinloof/medusa-sanity-sync";
// Import utility functions
import { defaultTransformer } from "@tinloof/medusa-sanity-sync";Compatibility
- Medusa v2.12.3+
- Node.js v20+
License
MIT
