Merge pull request #281 from medusajs/feat/contentful-extension

fix(contentful): add region sync + improvements
This commit is contained in:
Sebastian Rindom
2021-06-16 07:46:07 +02:00
committed by GitHub
5 changed files with 313 additions and 95 deletions
+3 -3
View File
@@ -1,13 +1,13 @@
let ignore = [`**/dist`] let ignore = [`**/dist`];
// Jest needs to compile this code, but generally we don't want this copied // Jest needs to compile this code, but generally we don't want this copied
// to output folders // to output folders
if (process.env.NODE_ENV !== `test`) { if (process.env.NODE_ENV !== `test`) {
ignore.push(`**/__tests__`) ignore.push(`**/__tests__`);
} }
module.exports = { module.exports = {
sourceMaps: true, sourceMaps: true,
presets: ["babel-preset-medusa-package"], presets: ["babel-preset-medusa-package"],
ignore, ignore,
} };
@@ -2,9 +2,17 @@ import _ from "lodash"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { createClient } from "contentful-management" import { createClient } from "contentful-management"
const IGNORE_THRESHOLD = 2 // seconds
class ContentfulService extends BaseService { class ContentfulService extends BaseService {
constructor( constructor(
{ productService, redisClient, productVariantService, eventBusService }, {
regionService,
productService,
redisClient,
productVariantService,
eventBusService,
},
options options
) { ) {
super() super()
@@ -13,6 +21,8 @@ class ContentfulService extends BaseService {
this.productVariantService_ = productVariantService this.productVariantService_ = productVariantService
this.regionService_ = regionService
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
this.options_ = options this.options_ = options
@@ -24,16 +34,19 @@ class ContentfulService extends BaseService {
this.redis_ = redisClient this.redis_ = redisClient
} }
async getIgnoreIds_(type) { async addIgnore_(id, side) {
return new Promise((resolve, reject) => { const key = `${id}_ignore_${side}`
this.redis_.get(`${type}_ignore_ids`, (err, reply) => { return await this.redis_.set(
if (err) { key,
return reject(err) 1,
} "EX",
this.options_.ignore_threshold || IGNORE_THRESHOLD
)
}
return resolve(JSON.parse(reply)) async shouldIgnore_(id, side) {
}) const key = `${id}_ignore_${side}`
}) return await this.redis_.get(key)
} }
async getContentfulEnvironment_() { async getContentfulEnvironment_() {
@@ -46,12 +59,17 @@ class ContentfulService extends BaseService {
} }
} }
async getVariantEntries_(variants) { async getVariantEntries_(variants, config = { publish: false }) {
try { try {
const contentfulVariants = await Promise.all( const contentfulVariants = await Promise.all(
variants.map((variant) => variants.map(async (variant) => {
this.updateProductVariantInContentful(variant) let updated = await this.updateProductVariantInContentful(variant)
) if (config.publish) {
updated = updated.publish()
}
return updated
})
) )
return contentfulVariants return contentfulVariants
@@ -137,13 +155,21 @@ class ContentfulService extends BaseService {
}) })
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
const variantEntries = await this.getVariantEntries_(p.variants) const variantEntries = await this.getVariantEntries_(p.variants, {
publish: true,
})
const variantLinks = this.getVariantLinks_(variantEntries) const variantLinks = this.getVariantLinks_(variantEntries)
const fields = { const fields = {
[this.getCustomField("title", "product")]: { [this.getCustomField("title", "product")]: {
"en-US": p.title, "en-US": p.title,
}, },
[this.getCustomField("subtitle", "product")]: {
"en-US": p.subtitle,
},
[this.getCustomField("description", "product")]: {
"en-US": p.description,
},
[this.getCustomField("variants", "product")]: { [this.getCustomField("variants", "product")]: {
"en-US": variantLinks, "en-US": variantLinks,
}, },
@@ -157,7 +183,14 @@ class ContentfulService extends BaseService {
if (p.images.length > 0) { if (p.images.length > 0) {
const imageLinks = await this.createImageAssets(product) const imageLinks = await this.createImageAssets(product)
if (imageLinks) {
fields.images = {
"en-US": imageLinks,
}
}
}
if (p.thumbnail) {
const thumbnailAsset = await environment.createAsset({ const thumbnailAsset = await environment.createAsset({
fields: { fields: {
title: { title: {
@@ -176,7 +209,7 @@ class ContentfulService extends BaseService {
}, },
}) })
await thumbnailAsset.processForAllLocales() await thumbnailAsset.processForAllLocales().then((a) => a.publish())
const thumbnailLink = { const thumbnailLink = {
sys: { sys: {
@@ -189,12 +222,6 @@ class ContentfulService extends BaseService {
fields.thumbnail = { fields.thumbnail = {
"en-US": thumbnailLink, "en-US": thumbnailLink,
} }
if (imageLinks) {
fields.images = {
"en-US": imageLinks,
}
}
} }
if (p.type) { if (p.type) {
@@ -233,9 +260,6 @@ class ContentfulService extends BaseService {
fields, fields,
}) })
// const ignoreIds = (await this.getIgnoreIds_("product")) || []
// ignoreIds.push(product.id)
// this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
return result return result
} catch (error) { } catch (error) {
throw error throw error
@@ -282,12 +306,125 @@ class ContentfulService extends BaseService {
} }
} }
async createRegionInContentful(region) {
const hasType = await this.getType("region")
.then(() => true)
.catch(() => false)
if (!hasType) {
return
}
try {
const r = await this.regionService_.retrieve(region.id, {
relations: ["countries", "payment_providers", "fulfillment_providers"],
})
const environment = await this.getContentfulEnvironment_()
const fields = {
[this.getCustomField("medusaId", "product")]: {
"en-US": r.id,
},
[this.getCustomField("name", "region")]: {
"en-US": r.name,
},
[this.getCustomField("countries", "region")]: {
"en-US": r.countries,
},
[this.getCustomField("paymentProviders", "region")]: {
"en-US": r.payment_providers,
},
[this.getCustomField("fulfillmentProviders", "region")]: {
"en-US": r.fulfillment_providers,
},
}
const result = await environment.createEntryWithId("region", r.id, {
fields,
})
return result
} catch (error) {
throw error
}
}
async updateRegionInContentful(data) {
const hasType = await this.getType("region")
.then(() => true)
.catch(() => false)
if (!hasType) {
return
}
const updateFields = [
"name",
"currency_code",
"countries",
"payment_providers",
"fulfillment_providers",
]
const found = data.fields.find((f) => updateFields.includes(f))
if (!found) {
return
}
try {
const ignore = await this.shouldIgnore_(data.id, "contentful")
if (ignore) {
return
}
const r = await this.regionService_.retrieve(data.id, {
relations: ["countries", "payment_providers", "fulfillment_providers"],
})
const environment = await this.getContentfulEnvironment_()
// check if region exists
let regionEntry = undefined
try {
regionEntry = await environment.getEntry(data.id)
} catch (error) {
return this.createRegionInContentful(r)
}
const regionEntryFields = {
...regionEntry.fields,
[this.getCustomField("name", "region")]: {
"en-US": r.name,
},
[this.getCustomField("countries", "region")]: {
"en-US": r.countries,
},
[this.getCustomField("paymentProviders", "region")]: {
"en-US": r.payment_providers,
},
[this.getCustomField("fulfillmentProviders", "region")]: {
"en-US": r.fulfillment_providers,
},
}
regionEntry.fields = regionEntryFields
const updatedEntry = await regionEntry.update()
const publishedEntry = await updatedEntry.publish()
await this.addIgnore_(data.id, "medusa")
return publishedEntry
} catch (error) {
throw error
}
}
async updateProductInContentful(data) { async updateProductInContentful(data) {
const updateFields = [ const updateFields = [
"variants", "variants",
"options", "options",
"tags", "tags",
"title", "title",
"subtitle",
"tags", "tags",
"type", "type",
"type_id", "type_id",
@@ -302,16 +439,10 @@ class ContentfulService extends BaseService {
} }
try { try {
// const ignoreIds = (await this.getIgnoreIds_("product")) || [] const ignore = await this.shouldIgnore_(data.id, "contentful")
if (ignore) {
// if (ignoreIds.includes(product.id)) { return
// const newIgnoreIds = ignoreIds.filter((id) => id !== product.id) }
// this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds))
// return
// } else {
// ignoreIds.push(product.id)
// this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
// }
const p = await this.productService_.retrieve(data.id, { const p = await this.productService_.retrieve(data.id, {
relations: [ relations: [
@@ -341,6 +472,12 @@ class ContentfulService extends BaseService {
[this.getCustomField("title", "product")]: { [this.getCustomField("title", "product")]: {
"en-US": p.title, "en-US": p.title,
}, },
[this.getCustomField("subtitle", "product")]: {
"en-US": p.subtitle,
},
[this.getCustomField("description", "product")]: {
"en-US": p.description,
},
[this.getCustomField("options", "product")]: { [this.getCustomField("options", "product")]: {
"en-US": p.options, "en-US": p.options,
}, },
@@ -376,7 +513,7 @@ class ContentfulService extends BaseService {
}, },
}) })
await thumbnailAsset.processForAllLocales() await thumbnailAsset.processForAllLocales().then((a) => a.publish())
const thumbnailLink = { const thumbnailLink = {
sys: { sys: {
@@ -430,6 +567,8 @@ class ContentfulService extends BaseService {
const updatedEntry = await productEntry.update() const updatedEntry = await productEntry.update()
const publishedEntry = await updatedEntry.publish() const publishedEntry = await updatedEntry.publish()
await this.addIgnore_(data.id, "medusa")
return publishedEntry return publishedEntry
} catch (error) { } catch (error) {
throw error throw error
@@ -460,19 +599,10 @@ class ContentfulService extends BaseService {
} }
try { try {
// const ignoreIds = (await this.getIgnoreIds_("product_variant")) || [] const ignore = await this.shouldIgnore_(variant.id, "contentful")
if (ignore) {
//if (ignoreIds.includes(variant.id)) { return
// const newIgnoreIds = ignoreIds.filter((id) => id !== variant.id) }
// this.redis_.set(
// "product_variant_ignore_ids",
// JSON.stringify(newIgnoreIds)
// )
// return
//} else {
// ignoreIds.push(variant.id)
// this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
//}
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
// check if product exists // check if product exists
@@ -512,6 +642,8 @@ class ContentfulService extends BaseService {
const updatedEntry = await variantEntry.update() const updatedEntry = await variantEntry.update()
const publishedEntry = await updatedEntry.publish() const publishedEntry = await updatedEntry.publish()
await this.addIgnore_(variant.id, "medusa")
return publishedEntry return publishedEntry
} catch (error) { } catch (error) {
throw error throw error
@@ -519,29 +651,42 @@ class ContentfulService extends BaseService {
} }
async sendContentfulProductToAdmin(productId) { async sendContentfulProductToAdmin(productId) {
const ignore = await this.shouldIgnore_(productId, "medusa")
if (ignore) {
return
}
try { try {
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
const productEntry = await environment.getEntry(productId) const productEntry = await environment.getEntry(productId)
const product = await this.productService_.retrieve(productId) const product = await this.productService_.retrieve(productId)
//const ignoreIds = (await this.getIgnoreIds_("product")) || []
//if (ignoreIds.includes(productId)) {
// const newIgnoreIds = ignoreIds.filter((id) => id !== productId)
// this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds))
// return
//} else {
// ignoreIds.push(productId)
// this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
//}
let update = {} let update = {}
const title = const title =
productEntry.fields[this.getCustomField("title", "product")]["en-US"] productEntry.fields[this.getCustomField("title", "product")]["en-US"]
const subtitle =
productEntry.fields[this.getCustomField("subtitle", "product")]["en-US"]
const description =
productEntry.fields[this.getCustomField("description", "product")][
"en-US"
]
if (product.title !== title) { if (product.title !== title) {
update.title = title update.title = title
} }
if (product.subtitle !== subtitle) {
update.subtitle = subtitle
}
if (product.description !== description) {
update.description = description
}
// Get the thumbnail, if present // Get the thumbnail, if present
if (productEntry.fields.thumbnail) { if (productEntry.fields.thumbnail) {
const thumb = await environment.getAsset( const thumb = await environment.getAsset(
@@ -556,7 +701,9 @@ class ContentfulService extends BaseService {
} }
if (!_.isEmpty(update)) { if (!_.isEmpty(update)) {
await this.productService_.update(productId, update) await this.productService_.update(productId, update).then(async () => {
return await this.addIgnore_(productId, "contentful")
})
} }
} catch (error) { } catch (error) {
throw error throw error
@@ -564,32 +711,25 @@ class ContentfulService extends BaseService {
} }
async sendContentfulProductVariantToAdmin(variantId) { async sendContentfulProductVariantToAdmin(variantId) {
const ignore = this.shouldIgnore_(variantId, "medusa")
if (ignore) {
return
}
try { try {
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
const variantEntry = await environment.getEntry(variantId) const variantEntry = await environment.getEntry(variantId)
// const ignoreIds = (await this.getIgnoreIds_("product_variant")) || [] const updatedVariant = await this.productVariantService_
// if (ignoreIds.includes(variantId)) { .update(variantId, {
// const newIgnoreIds = ignoreIds.filter((id) => id !== variantId)
// this.redis_.set(
// "product_variant_ignore_ids",
// JSON.stringify(newIgnoreIds)
// )
// return
// } else {
// ignoreIds.push(variantId)
// this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
// }
const updatedVariant = await this.productVariantService_.update(
variantId,
{
title: title:
variantEntry.fields[this.getCustomField("title", "variant")][ variantEntry.fields[this.getCustomField("title", "variant")][
"en-US" "en-US"
], ],
} })
) .then(async () => {
return await this.addIgnore_(variantId, "contentful")
})
return updatedVariant return updatedVariant
} catch (error) { } catch (error) {
@@ -10,6 +10,14 @@ class ContentfulSubscriber {
this.contentfulService_ = contentfulService this.contentfulService_ = contentfulService
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
this.eventBus_.subscribe("region.created", async (data) => {
await this.contentfulService_.createRegionInContentful(data)
})
this.eventBus_.subscribe("region.updated", async (data) => {
await this.contentfulService_.updateRegionInContentful(data)
})
this.eventBus_.subscribe("product-variant.updated", async (data) => { this.eventBus_.subscribe("product-variant.updated", async (data) => {
await this.contentfulService_.updateProductVariantInContentful(data) await this.contentfulService_.updateProductVariantInContentful(data)
}) })
@@ -1,6 +1,14 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import RegionService from "../region" import RegionService from "../region"
const eventBusService = {
emit: jest.fn(),
withTransaction: function() {
return this
},
}
describe("RegionService", () => { describe("RegionService", () => {
describe("create", () => { describe("create", () => {
const regionRepository = MockRepository({}) const regionRepository = MockRepository({})
@@ -58,6 +66,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
fulfillmentProviderRepository: fpRepository, fulfillmentProviderRepository: fpRepository,
paymentProviderRepository: ppRepository, paymentProviderRepository: ppRepository,
currencyRepository, currencyRepository,
@@ -168,6 +177,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
regionRepository, regionRepository,
}) })
@@ -237,6 +247,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
fulfillmentProviderRepository: fpRepository, fulfillmentProviderRepository: fpRepository,
paymentProviderRepository: ppRepository, paymentProviderRepository: ppRepository,
regionRepository, regionRepository,
@@ -335,6 +346,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
fulfillmentProviderRepository: fpRepository, fulfillmentProviderRepository: fpRepository,
paymentProviderRepository: ppRepository, paymentProviderRepository: ppRepository,
regionRepository, regionRepository,
@@ -380,6 +392,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
regionRepository, regionRepository,
}) })
@@ -429,6 +442,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
regionRepository, regionRepository,
countryRepository, countryRepository,
}) })
@@ -473,6 +487,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
regionRepository, regionRepository,
}) })
@@ -522,6 +537,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
fulfillmentProviderRepository: fpRepository, fulfillmentProviderRepository: fpRepository,
paymentProviderRepository: ppRepository, paymentProviderRepository: ppRepository,
regionRepository, regionRepository,
@@ -582,6 +598,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
fulfillmentProviderRepository: fpRepository, fulfillmentProviderRepository: fpRepository,
regionRepository, regionRepository,
}) })
@@ -631,6 +648,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
regionRepository, regionRepository,
}) })
@@ -665,6 +683,7 @@ describe("RegionService", () => {
const regionService = new RegionService({ const regionService = new RegionService({
manager: MockManager, manager: MockManager,
eventBusService,
regionRepository, regionRepository,
}) })
+65 -14
View File
@@ -8,11 +8,17 @@ import { countries } from "../utils/countries"
* @implements BaseService * @implements BaseService
*/ */
class RegionService extends BaseService { class RegionService extends BaseService {
static Events = {
UPDATED: "region.updated",
CREATED: "region.created",
}
constructor({ constructor({
manager, manager,
regionRepository, regionRepository,
countryRepository, countryRepository,
storeService, storeService,
eventBusService,
currencyRepository, currencyRepository,
paymentProviderRepository, paymentProviderRepository,
fulfillmentProviderRepository, fulfillmentProviderRepository,
@@ -33,6 +39,9 @@ class RegionService extends BaseService {
/** @private @const {StoreService} */ /** @private @const {StoreService} */
this.storeService_ = storeService this.storeService_ = storeService
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
/** @private @const {CurrencyRepository} */ /** @private @const {CurrencyRepository} */
this.currencyRepository_ = currencyRepository this.currencyRepository_ = currencyRepository
@@ -60,6 +69,7 @@ class RegionService extends BaseService {
currencyRepository: this.currencyRepository_, currencyRepository: this.currencyRepository_,
countryRepository: this.countryRepository_, countryRepository: this.countryRepository_,
storeService: this.storeService_, storeService: this.storeService_,
eventBusService: this.eventBus_,
paymentProviderRepository: this.paymentProviderRepository_, paymentProviderRepository: this.paymentProviderRepository_,
paymentProviderService: this.paymentProviderService_, paymentProviderService: this.paymentProviderService_,
fulfillmentProviderRepository: this.fulfillmentProviderRepository_, fulfillmentProviderRepository: this.fulfillmentProviderRepository_,
@@ -117,6 +127,13 @@ class RegionService extends BaseService {
const created = regionRepository.create(regionObject) const created = regionRepository.create(regionObject)
const result = await regionRepository.save(created) const result = await regionRepository.save(created)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.CREATED, {
id: result.id,
})
return result return result
}) })
} }
@@ -168,6 +185,14 @@ class RegionService extends BaseService {
} }
const result = await regionRepository.save(region) const result = await regionRepository.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: result.id,
fields: Object.keys(update),
})
return result return result
}) })
} }
@@ -390,6 +415,14 @@ class RegionService extends BaseService {
region.countries = [...(region.countries || []), country] region.countries = [...(region.countries || []), country]
const updated = await regionRepo.save(region) const updated = await regionRepo.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: updated.id,
fields: ["countries"],
})
return updated return updated
}) })
} }
@@ -419,6 +452,12 @@ class RegionService extends BaseService {
) )
const updated = await regionRepo.save(region) const updated = await regionRepo.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: updated.id,
fields: ["countries"],
})
return updated return updated
}) })
} }
@@ -458,6 +497,14 @@ class RegionService extends BaseService {
region.payment_providers = [...region.payment_providers, pp] region.payment_providers = [...region.payment_providers, pp]
const updated = await regionRepo.save(region) const updated = await regionRepo.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: updated.id,
fields: ["payment_providers"],
})
return updated return updated
}) })
} }
@@ -497,6 +544,12 @@ class RegionService extends BaseService {
region.fulfillment_providers = [...region.fulfillment_providers, fp] region.fulfillment_providers = [...region.fulfillment_providers, fp]
const updated = await regionRepo.save(region) const updated = await regionRepo.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: updated.id,
fields: ["fulfillment_providers"],
})
return updated return updated
}) })
} }
@@ -525,6 +578,12 @@ class RegionService extends BaseService {
) )
const updated = await regionRepo.save(region) const updated = await regionRepo.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: updated.id,
fields: ["payment_providers"],
})
return updated return updated
}) })
} }
@@ -553,23 +612,15 @@ class RegionService extends BaseService {
) )
const updated = await regionRepo.save(region) const updated = await regionRepo.save(region)
await this.eventBus_
.withTransaction(manager)
.emit(RegionService.Events.UPDATED, {
id: updated.id,
fields: ["fulfillment_providers"],
})
return updated return updated
}) })
} }
/**
* Decorates a region
* @param {object} region - the region to decorate
* @param {[string]} fields - the fields to include
* @param {[string]} expandFields - the fields to expand
* @return {Region} the region
*/
async decorate(region, fields, expandFields = []) {
const requiredFields = ["id", "metadata"]
const decorated = _.pick(region, fields.concat(requiredFields))
const final = await this.runDecorators_(decorated)
return final
}
} }
export default RegionService export default RegionService