- All schemas have been rewritten to a relational model - All services have been rewritten to accommodate the new data model - Adds idempotency keys to core endpoints allowing you to retry requests with no additional side effects - Adds staged jobs to avoid putting jobs in the queue when transactions abort - Adds atomic transactions to all methods with access to the data layer Co-authored-by: Oliver Windall Juhl <oliver@mrbltech.com>
378 lines
9.6 KiB
JavaScript
378 lines
9.6 KiB
JavaScript
import _ from "lodash"
|
|
import { BaseService } from "medusa-interfaces"
|
|
import { createClient } from "contentful-management"
|
|
|
|
class ContentfulService extends BaseService {
|
|
constructor(
|
|
{ productService, redisClient, 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_ = redisClient
|
|
}
|
|
|
|
async getIgnoreIds_(type) {
|
|
return new Promise((resolve, reject) => {
|
|
this.redis_.get(`${type}_ignore_ids`, (err, reply) => {
|
|
if (err) {
|
|
return reject(err)
|
|
}
|
|
|
|
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_(variants) {
|
|
try {
|
|
const contentfulVariants = await Promise.all(
|
|
variants.map((variant) =>
|
|
this.updateProductVariantInContentful(variant)
|
|
)
|
|
)
|
|
|
|
return contentfulVariants
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
getVariantLinks_(variantEntries) {
|
|
return variantEntries.map((v) => ({
|
|
sys: {
|
|
type: "Link",
|
|
linkType: "Entry",
|
|
id: v.sys.id,
|
|
},
|
|
}))
|
|
}
|
|
|
|
async createImageAssets(product) {
|
|
const environment = await this.getContentfulEnvironment_()
|
|
|
|
let assets = []
|
|
await Promise.all(
|
|
product.images
|
|
.filter((image) => image !== product.thumbnail)
|
|
.map(async (image, i) => {
|
|
const asset = await environment.createAsset({
|
|
fields: {
|
|
title: {
|
|
"en-US": `${product.title} - ${i}`,
|
|
},
|
|
description: {
|
|
"en-US": "",
|
|
},
|
|
file: {
|
|
"en-US": {
|
|
contentType: "image/xyz",
|
|
fileName: image,
|
|
upload: image,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await asset.processForAllLocales()
|
|
|
|
assets.push({
|
|
sys: {
|
|
type: "Link",
|
|
linkType: "Asset",
|
|
id: asset.sys.id,
|
|
},
|
|
})
|
|
})
|
|
)
|
|
|
|
return assets
|
|
}
|
|
|
|
async createProductInContentful(product) {
|
|
try {
|
|
const p = await this.productService_.retrieve(product.id, {
|
|
relations: ["variants", "options"],
|
|
})
|
|
|
|
const environment = await this.getContentfulEnvironment_()
|
|
const variantEntries = await this.getVariantEntries_(p.variants)
|
|
const variantLinks = this.getVariantLinks_(variantEntries)
|
|
|
|
const result = await environment.createEntryWithId("product", p.id, {
|
|
fields: {
|
|
title: {
|
|
"en-US": p.title,
|
|
},
|
|
variants: {
|
|
"en-US": variantLinks,
|
|
},
|
|
options: {
|
|
"en-US": p.options,
|
|
},
|
|
objectId: {
|
|
"en-US": p.id,
|
|
},
|
|
},
|
|
})
|
|
|
|
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
|
ignoreIds.push(product.id)
|
|
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
|
|
return result
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async createProductVariantInContentful(variant) {
|
|
try {
|
|
const v = await this.productVariantService_.retrieve(variant.id, {
|
|
relations: ["prices", "options"],
|
|
})
|
|
|
|
const environment = await this.getContentfulEnvironment_()
|
|
const result = await environment.createEntryWithId(
|
|
"productVariant",
|
|
v.id,
|
|
{
|
|
fields: {
|
|
title: {
|
|
"en-US": v.title,
|
|
},
|
|
sku: {
|
|
"en-US": v.sku,
|
|
},
|
|
prices: {
|
|
"en-US": v.prices,
|
|
},
|
|
options: {
|
|
"en-US": v.options,
|
|
},
|
|
objectId: {
|
|
"en-US": v.id,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
|
|
ignoreIds.push(v.id)
|
|
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
|
|
return result
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async updateProductInContentful(product) {
|
|
try {
|
|
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
|
|
|
if (ignoreIds.includes(product.id)) {
|
|
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 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 p = await this.productService_.retrieve(product.id, {
|
|
relations: ["options", "variants"],
|
|
})
|
|
|
|
const variantEntries = await this.getVariantEntries_(p.variants)
|
|
const variantLinks = this.getVariantLinks_(variantEntries)
|
|
|
|
const productEntryFields = {
|
|
...productEntry.fields,
|
|
title: {
|
|
"en-US": p.title,
|
|
},
|
|
options: {
|
|
"en-US": p.options,
|
|
},
|
|
variants: {
|
|
"en-US": variantLinks,
|
|
},
|
|
objectId: {
|
|
"en-US": p.id,
|
|
},
|
|
}
|
|
|
|
productEntry.fields = productEntryFields
|
|
|
|
const updatedEntry = await productEntry.update()
|
|
const publishedEntry = await updatedEntry.publish()
|
|
|
|
return publishedEntry
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async updateProductVariantInContentful(variant) {
|
|
try {
|
|
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
|
|
|
|
if (ignoreIds.includes(variant.id)) {
|
|
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_()
|
|
// check if product exists
|
|
let variantEntry = undefined
|
|
// if not, we create a new one
|
|
try {
|
|
variantEntry = await environment.getEntry(variant.id)
|
|
} catch (error) {
|
|
return this.createProductVariantInContentful(variant)
|
|
}
|
|
|
|
const v = await this.productVariantService_.retrieve(variant.id, {
|
|
relations: ["prices", "options"],
|
|
})
|
|
|
|
const variantEntryFields = {
|
|
...variantEntry.fields,
|
|
title: {
|
|
"en-US": v.title,
|
|
},
|
|
sku: {
|
|
"en-US": v.sku,
|
|
},
|
|
options: {
|
|
"en-US": v.options,
|
|
},
|
|
prices: {
|
|
"en-US": v.prices,
|
|
},
|
|
objectId: {
|
|
"en-US": v.id,
|
|
},
|
|
}
|
|
|
|
variantEntry.fields = variantEntryFields
|
|
|
|
const updatedEntry = await variantEntry.update()
|
|
const publishedEntry = await updatedEntry.publish()
|
|
|
|
return publishedEntry
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async sendContentfulProductToAdmin(productId) {
|
|
try {
|
|
const environment = await this.getContentfulEnvironment_()
|
|
const productEntry = await environment.getEntry(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 = {
|
|
title: productEntry.fields.title["en-US"],
|
|
}
|
|
|
|
// Get the thumbnail, if present
|
|
if (productEntry.fields.thumbnail) {
|
|
const thumb = await environment.getAsset(
|
|
productEntry.fields.thumbnail["en-US"].sys.id
|
|
)
|
|
|
|
update.thumbnail = thumb.fields.file["en-US"].url
|
|
}
|
|
|
|
const updatedProduct = await this.productService_.update(
|
|
productId,
|
|
update
|
|
)
|
|
|
|
return updatedProduct
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async sendContentfulProductVariantToAdmin(variantId) {
|
|
try {
|
|
const environment = await this.getContentfulEnvironment_()
|
|
const variantEntry = await environment.getEntry(variantId)
|
|
|
|
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
|
|
if (ignoreIds.includes(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: variantEntry.fields.title["en-US"],
|
|
}
|
|
)
|
|
|
|
return updatedVariant
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
export default ContentfulService
|