Adds plugin for Contentful synchronization (#75)
Adds plugin for Contentful Will create product and product variants in Contentful, when created in Medusa Closes #47
This commit is contained in:
1 parent
a7951ca29c
commit
d56ccba2b1
20 files changed
+7662
-109
No files matched your search
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// noop
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "medusa-plugin-contentful",
|
||||
"version": "1.0.0",
|
||||
"description": "Contentful plugin for Medusa Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-plugin-contentful"
|
||||
},
|
||||
"author": "Oliver Juhl",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d dist",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"body-parser": "^1.19.0",
|
||||
"contentful-management": "^5.27.1",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.3.0",
|
||||
"medusa-interfaces": "^0.3.0",
|
||||
"medusa-test-utils": "^0.3.0",
|
||||
"redis": "^3.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router } from "express"
|
||||
import hooks from "./routes/hooks"
|
||||
|
||||
export default (container) => {
|
||||
const app = Router()
|
||||
|
||||
hooks(app)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||
@@ -0,0 +1,5 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const contentfulService = req.scope.resolve("contentfulService")
|
||||
|
||||
const contentfulType = req.body.contentType.sys.id
|
||||
|
||||
let updated = {}
|
||||
switch (contentfulType) {
|
||||
case "product":
|
||||
updated = await contentfulService.sendContentfulProductToAdmin(req.body)
|
||||
break
|
||||
case "productVariant":
|
||||
updated = await contentfulService.sendContentfulProductVariantToAdmin(
|
||||
req.body
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
res.status(200).send(updated)
|
||||
} catch (error) {
|
||||
res.status(400).send(`Webhook error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from "express"
|
||||
import bodyParser from "body-parser"
|
||||
import middlewares from "../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/hooks", route)
|
||||
|
||||
route.post("/contentful", middlewares.wrap(require("./contentful").default))
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { createClient } from "contentful-management"
|
||||
import redis from "redis"
|
||||
|
||||
class ContentfulService extends BaseService {
|
||||
constructor(
|
||||
{ productService, productVariantService, eventBusService },
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.productService_ = productService
|
||||
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
this.options_ = options
|
||||
|
||||
this.contentful_ = createClient({
|
||||
accessToken: options.access_token,
|
||||
})
|
||||
|
||||
this.redis_ = redis.createClient()
|
||||
}
|
||||
|
||||
async getIgnoreIds_(type) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.redis_.get(`${type}_ignore_ids`, (err, reply) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
return reject("Missing key")
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(reply))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getContentfulEnvironment_() {
|
||||
try {
|
||||
const space = await this.contentful_.getSpace(this.options_.space_id)
|
||||
const environment = await space.getEnvironment(this.options_.environment)
|
||||
return environment
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getVariantEntries_(variantEntryIds) {
|
||||
if (!variantEntryIds) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
return Promise.all(variantEntryIds.map((v) => environment.getEntry(v)))
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getVariantLinks_(variantEntries) {
|
||||
if (!variantEntries) {
|
||||
return []
|
||||
}
|
||||
|
||||
return variantEntries.map((v) => ({
|
||||
sys: {
|
||||
type: "Link",
|
||||
linkType: "Entry",
|
||||
id: v.sys.id,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
async createProductInContentful(product) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const variantEntries = await this.getVariantEntries_(product.variants)
|
||||
return environment.createEntryWithId("product", product._id, {
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": product.title,
|
||||
},
|
||||
variants: {
|
||||
"en-US": this.getVariantLinks_(variantEntries),
|
||||
},
|
||||
objectId: {
|
||||
"en-US": product._id,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async createProductVariantInContentful(variant) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
return environment.createEntryWithId("productVariant", variant._id, {
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": variant.title,
|
||||
},
|
||||
sku: {
|
||||
"en-US": variant.sku,
|
||||
},
|
||||
prices: {
|
||||
"en-US": variant.prices,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": variant._id,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateProductInContentful(product) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
// check if product exists
|
||||
let productEntry = undefined
|
||||
try {
|
||||
productEntry = await environment.getEntry(product._id)
|
||||
} catch (error) {
|
||||
return this.createProductInContentful(product)
|
||||
}
|
||||
|
||||
const variantEntries = await this.getVariantEntries_(product.variants)
|
||||
productEntry.fields = _.assignIn(productEntry.fields, {
|
||||
title: {
|
||||
"en-US": product.title,
|
||||
},
|
||||
variants: {
|
||||
"en-US": this.getVariantLinks_(variantEntries),
|
||||
},
|
||||
})
|
||||
|
||||
await productEntry.update()
|
||||
const publishedEntry = await productEntry.publish()
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product")
|
||||
if (ignoreIds.includes(publishedEntry.sys.id)) {
|
||||
ignoreIds.filter((id) => id !== publishedEntry.sys.id)
|
||||
} else {
|
||||
this.eventBus_.emit("product.updated", publishedEntry)
|
||||
}
|
||||
|
||||
return publishedEntry
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateProductVariantInContentful(variant) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
// check if product exists
|
||||
let variantEntry = undefined
|
||||
variantEntry = await environment.getEntry(variant._id)
|
||||
// if not, we create a new one
|
||||
if (!variantEntry) {
|
||||
return this.createProductVariantInContentful(variant)
|
||||
}
|
||||
|
||||
variantEntry.fields = _.assignIn(variantEntry.fields, {
|
||||
title: {
|
||||
"en-US": variant.title,
|
||||
},
|
||||
sku: {
|
||||
"en-US": variant.sku,
|
||||
},
|
||||
prices: {
|
||||
"en-US": variant.prices,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": variant._id,
|
||||
},
|
||||
})
|
||||
|
||||
await variantEntry.update()
|
||||
const publishedEntry = await variantEntry.publish()
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product_variant")
|
||||
if (ignoreIds.includes(publishedEntry.sys.id)) {
|
||||
ignoreIds.filter((id) => id !== publishedEntry.sys.id)
|
||||
} else {
|
||||
this.eventBus_.emit("product-variant.updated", publishedEntry)
|
||||
}
|
||||
|
||||
return publishedEntry
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentfulProductToAdmin(product) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const productEntry = await environment.getEntry(product.sys.id)
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product")
|
||||
ignoreIds.push(product.sys.id)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
|
||||
|
||||
const updatedProduct = await this.productService_.update(
|
||||
productEntry.objectId,
|
||||
{
|
||||
title: productEntry.fields.title["en-US"],
|
||||
variants: productEntry.fields.variants["en-US"],
|
||||
}
|
||||
)
|
||||
|
||||
return updatedProduct
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentfulProductVariantToAdmin(variant) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const variantEntry = await environment.getEntry(variant.sys.id)
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product_variant")
|
||||
ignoreIds.push(variant.sys.id)
|
||||
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
|
||||
|
||||
const updatedVariant = await this.variantService_.update(
|
||||
variantEntry.objectId,
|
||||
{
|
||||
title: variantEntry.fields.title["en-US"],
|
||||
sku: variantEntry.fields.sku["en-US"],
|
||||
prices: variantEntry.fields.prices["en-US"],
|
||||
}
|
||||
)
|
||||
|
||||
return updatedVariant
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentfulService
|
||||
@@ -0,0 +1,20 @@
|
||||
class ContentfulSubscriber {
|
||||
constructor({ contentfulService, eventBusService }) {
|
||||
this.contentfulService_ = contentfulService
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
this.eventBus_.subscribe("product-variant.updated", async (data) => {
|
||||
await this.contentfulService_.updateProductVariantInContentful(data)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("product.updated", async (data) => {
|
||||
await this.contentfulService_.updateProductInContentful(data)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("product.created", async (data) => {
|
||||
await this.contentfulService_.createProductVariantInContentful(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentfulSubscriber
|
||||
Reference in new issue
Block a user