feat: medusa-source-shopify loader (#563)
* added statuses to product + unit test for updating status * add update to product model * added integration tests * added integration test to validate that updating status to null results in invalid_data error * removed comment * update GET /store/products integration test * fixed unit test with IdMap * init plugin * changed dbehaviour on invalid status input on admin list products * mprices * updated migration to add status = published on all existing products + added integration test on GET /admin/products when status null is provided * merged product status * init ShopifyService * made requested changes to migration and GET /store/products * fixed test * made requested changes to migration * push progress on source plugin * add webhook product/create handler * fixed normalization of variant weight * removed weight func * work on events * finished product hooks (error on new variant needs to be fixed) * fixed order status * create fulfillments * update fulfillment on cancel * refactored services, handle returns though medusa, helper methods * order updates * removed dist * update gitignore * emit cahnges to product * added redis ignore check to prevent update loops * fixed product-variant.deleted event * fix more events * fix test * fix: order taxes * added refund with no items * fixes to hooks * fixed handling refunds and returns issued from Shopify * added unit tests to ShopifyProductService and ShopifyCollectionService * linting fix * prepared loader PR * fix: jsDocs * fix: pager * fix: build output and babelrc * chore: linting * fix: address type * fix: migration clean up * fix: update snapshots with ext_ids Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
This commit is contained in:
committed by
GitHub
parent
f2ba4018fc
commit
577bcc23d4
@@ -27,6 +27,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -53,6 +54,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description1",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product1",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -86,6 +88,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product_filtering_1",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -112,6 +115,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product_filtering_3",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -145,6 +149,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product_filtering_2",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
|
||||
@@ -17,6 +17,7 @@ Array [
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -240,6 +241,7 @@ Array [
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description1",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product1",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -387,6 +389,7 @@ Array [
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product_filtering_3",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -419,6 +422,7 @@ Array [
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product_filtering_1",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -451,6 +455,7 @@ Array [
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product_filtering_2",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -488,6 +493,7 @@ Array [
|
||||
"deleted_at": null,
|
||||
"description": "test-giftcard-description",
|
||||
"discountable": false,
|
||||
"external_id": null,
|
||||
"handle": "test-giftcard",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
|
||||
@@ -17,6 +17,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": "test-product-description",
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": "test-product",
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
|
||||
@@ -47,6 +47,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": null,
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": null,
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -119,6 +120,7 @@ Object {
|
||||
"display_id": 1,
|
||||
"draft_order_id": null,
|
||||
"email": "test@email.com",
|
||||
"external_id": null,
|
||||
"fulfillment_status": "fulfilled",
|
||||
"id": "test-order",
|
||||
"idempotency_key": null,
|
||||
@@ -218,6 +220,7 @@ Object {
|
||||
"deleted_at": null,
|
||||
"description": null,
|
||||
"discountable": true,
|
||||
"external_id": null,
|
||||
"handle": null,
|
||||
"height": null,
|
||||
"hs_code": null,
|
||||
@@ -290,6 +293,7 @@ Object {
|
||||
"display_id": Any<Number>,
|
||||
"draft_order_id": null,
|
||||
"email": "test@email.com",
|
||||
"external_id": null,
|
||||
"fulfillment_status": "fulfilled",
|
||||
"id": "test-order",
|
||||
"idempotency_key": null,
|
||||
|
||||
@@ -9,6 +9,7 @@ Joi.address = () => {
|
||||
Joi.object().keys({
|
||||
first_name: Joi.string().required(),
|
||||
last_name: Joi.string().required(),
|
||||
company: Joi.string().optional(),
|
||||
address_1: Joi.string().required(),
|
||||
address_2: Joi.string()
|
||||
.allow(null, "")
|
||||
|
||||
14
packages/medusa-source-shopify/.babelrc
Normal file
14
packages/medusa-source-shopify/.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/medusa-source-shopify/.gitignore
vendored
Normal file
16
packages/medusa-source-shopify/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
!jest.config.js
|
||||
|
||||
/dist
|
||||
/api
|
||||
/services
|
||||
/utils
|
||||
/subscribers
|
||||
/loaders
|
||||
|
||||
|
||||
7
packages/medusa-source-shopify/.npmignore
Normal file
7
packages/medusa-source-shopify/.npmignore
Normal file
@@ -0,0 +1,7 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
0
packages/medusa-source-shopify/README.md
Normal file
0
packages/medusa-source-shopify/README.md
Normal file
50
packages/medusa-source-shopify/package.json
Normal file
50
packages/medusa-source-shopify/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "medusa-source-shopify",
|
||||
"version": "1.0.0-dev-1634210430972",
|
||||
"description": "Source plugin that allows users to import products from a Shopify store",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-source-shopify"
|
||||
},
|
||||
"author": "Kasper Fabrcius Kristensen <kasper@medusa-commerce.com>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "babel src -d . --ignore **/__tests__,**/__mocks__",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__,**/__mocks__",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.x",
|
||||
"typeorm": "0.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.15.4",
|
||||
"@shopify/shopify-api": "^1.4.1",
|
||||
"axios": "^0.21.4",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"ioredis": "^4.27.9",
|
||||
"lodash": "^4.17.21",
|
||||
"medusa-core-utils": "^1.1.22",
|
||||
"medusa-interfaces": "^1.1.24",
|
||||
"medusa-test-utils": "^1.1.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.15.4",
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/node": "^7.15.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-transform-instanceof": "^7.14.5",
|
||||
"@babel/plugin-transform-runtime": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/register": "^7.15.3",
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.32.0",
|
||||
"jest": "^27.2.0"
|
||||
}
|
||||
}
|
||||
8
packages/medusa-source-shopify/src/loaders/index.js
Normal file
8
packages/medusa-source-shopify/src/loaders/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default async (container, options) => {
|
||||
try {
|
||||
const shopifyService = container.resolve("shopifyService")
|
||||
await shopifyService.importShopify()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const ProductCollectionServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
id: `col_${data.metadata.sh_id}`,
|
||||
...data,
|
||||
})
|
||||
}),
|
||||
retrieveByHandle: jest.fn().mockImplementation((handle) => {
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { medusaProducts } from "./test-products"
|
||||
|
||||
export const ProductServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
if (data.handle === "ipod-nano") {
|
||||
return Promise.resolve(medusaProducts.ipod)
|
||||
}
|
||||
}),
|
||||
update: jest.fn().mockImplementation((_id, _update) => {
|
||||
return Promise.resolve(medusaProducts.ipod)
|
||||
}),
|
||||
retrieveByExternalId: jest.fn().mockImplementation((id) => {
|
||||
if (id === "shopify_ipod") {
|
||||
return Promise.resolve(medusaProducts.ipod)
|
||||
}
|
||||
if (id === "shopify_deleted") {
|
||||
return Promise.resolve(medusaProducts.ipod)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation((_id, _config) => {
|
||||
if (_id === "prod_ipod") {
|
||||
return Promise.resolve(medusaProducts.ipod)
|
||||
}
|
||||
}),
|
||||
addOption: jest.fn().mockImplementation((_id, _title) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const ProductVariantServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
create: jest.fn().mockImplementation((_data) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
update: jest.fn().mockImplementation((_data) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
delete: jest.fn().mockImplementation((_data) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
addOption: jest.fn().mockImplementation((id, title) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ShippingProfileServiceMock = {
|
||||
retrieveGiftCardDefault: jest.fn().mockImplementation((_data) => {
|
||||
return Promise.resolve({ id: "gift_card_profile" })
|
||||
}),
|
||||
retrieveDefault: jest.fn().mockImplementation((_data) => {
|
||||
return Promise.resolve({ id: "default_shipping_profile" })
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { shopifyProducts } from "./test-products"
|
||||
|
||||
export const ShopifyClientServiceMock = {
|
||||
get: jest.fn().mockImplementation((params) => {
|
||||
if (params.path === "products/ipod") {
|
||||
return Promise.resolve(shopifyProducts.ipod)
|
||||
}
|
||||
if (params.path === "products/new_ipod") {
|
||||
return Promise.resolve(shopifyProducts.new_ipod)
|
||||
}
|
||||
if (params.path === "products/shopify_ipod") {
|
||||
return Promise.resolve({
|
||||
body: {
|
||||
product: shopifyProducts.ipod_update,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (params.path === "products/shopify_deleted") {
|
||||
return Promise.resolve({
|
||||
body: {
|
||||
product: {
|
||||
...shopifyProducts.ipod,
|
||||
variants: shopifyProducts.ipod.variants.slice(1, -1),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}),
|
||||
list: jest.fn().mockImplementation((path, _headers, _query) => {
|
||||
if (path === "products") {
|
||||
return Promise.resolve([shopifyProducts.ipod])
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { medusaProducts } from "./test-products"
|
||||
|
||||
export const ShopifyProductServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
create: jest.fn().mockImplementation((_data) => {
|
||||
return Promise.resolve(medusaProducts.ipod)
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const ShopifyRedisServiceMock = {
|
||||
addIgnore: jest.fn().mockImplementation((_id, _event) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
shouldIgnore: jest.fn().mockImplementation((_id, _event) => {
|
||||
return false
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
export const medusaProducts = {
|
||||
ipod: {
|
||||
id: "prod_ipod",
|
||||
title: "IPod Nano - 8GB",
|
||||
subtitle: null,
|
||||
description:
|
||||
"<p>It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.</p>",
|
||||
handle: "ipod-nano",
|
||||
is_giftcard: false,
|
||||
status: "proposed",
|
||||
images: [
|
||||
{
|
||||
id: "img_01FHZ9ZFC4SP5ME8T18SW47R7S",
|
||||
url:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: "img_01FHZ9ZFC4SK54GE072FHMQ5XT",
|
||||
url:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano-2.png?v=1633120966",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: "img_01FHZ9ZFC44EKQPVWK76HBZT0S",
|
||||
url:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
thumbnail:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
options: [
|
||||
{
|
||||
id: "opt_01FHZ9ZFCPCQ7B1MPDD9X9YQX3",
|
||||
title: "Color",
|
||||
product_id: "prod_01FHZ9ZFC3KKYKA35NXNJ5A7FR",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
variants: [
|
||||
{
|
||||
id: "variant_01FHZ9ZFED5MEX3V4A7H5V2A0C",
|
||||
title: "Pink",
|
||||
product_id: "prod_01FHZ9ZFC3KKYKA35NXNJ5A7FR",
|
||||
prices: [
|
||||
{
|
||||
id: "ma_01FHZ9ZFEPN276FGACR08ZBZ2M",
|
||||
currency_code: "usd",
|
||||
amount: 19900,
|
||||
sale_amount: null,
|
||||
variant_id: "variant_01FHZ9ZFED5MEX3V4A7H5V2A0C",
|
||||
region_id: null,
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
},
|
||||
],
|
||||
sku: "IPOD2008PINK",
|
||||
barcode: "1234_pink",
|
||||
ean: null,
|
||||
upc: "1234_pink",
|
||||
inventory_quantity: 10,
|
||||
allow_backorder: true,
|
||||
manage_inventory: true,
|
||||
hs_code: null,
|
||||
origin_country: null,
|
||||
mid_code: null,
|
||||
material: null,
|
||||
weight: 567,
|
||||
length: null,
|
||||
height: null,
|
||||
width: null,
|
||||
options: [
|
||||
{
|
||||
id: "optval_01FHZ9ZFEDMCJME1BR4X8H89MG",
|
||||
value: "Pink",
|
||||
option_id: "opt_01FHZ9ZFCPCQ7B1MPDD9X9YQX3",
|
||||
variant_id: "variant_01FHZ9ZFED5MEX3V4A7H5V2A0C",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: {
|
||||
sh_id: 808950810,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "variant_01FHZ9ZFFA2KPA4JJSKF07VGWH",
|
||||
title: "Red",
|
||||
product_id: "prod_01FHZ9ZFC3KKYKA35NXNJ5A7FR",
|
||||
prices: [
|
||||
{
|
||||
id: "ma_01FHZ9ZFFFX31Z9GZ8ZHQ5KQ2P",
|
||||
currency_code: "usd",
|
||||
amount: 19900,
|
||||
sale_amount: null,
|
||||
variant_id: "variant_01FHZ9ZFFA2KPA4JJSKF07VGWH",
|
||||
region_id: null,
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
},
|
||||
],
|
||||
sku: "IPOD2008RED",
|
||||
barcode: "1234_red",
|
||||
ean: null,
|
||||
upc: "1234_red",
|
||||
inventory_quantity: 20,
|
||||
allow_backorder: true,
|
||||
manage_inventory: true,
|
||||
hs_code: null,
|
||||
origin_country: null,
|
||||
mid_code: null,
|
||||
material: null,
|
||||
weight: 567,
|
||||
length: null,
|
||||
height: null,
|
||||
width: null,
|
||||
options: [
|
||||
{
|
||||
id: "optval_01FHZ9ZFFASNAD8JSWS85Y4ZM0",
|
||||
value: "Red",
|
||||
option_id: "opt_01FHZ9ZFCPCQ7B1MPDD9X9YQX3",
|
||||
variant_id: "variant_01FHZ9ZFFA2KPA4JJSKF07VGWH",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: {
|
||||
sh_id: 49148385,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "variant_01FHZ9ZFG2F54TZMAWV0RJCQ5D",
|
||||
title: "Green",
|
||||
product_id: "prod_01FHZ9ZFC3KKYKA35NXNJ5A7FR",
|
||||
prices: [
|
||||
{
|
||||
id: "ma_01FHZ9ZFG6QPPCG4XPX8JG0HGW",
|
||||
currency_code: "usd",
|
||||
amount: 19900,
|
||||
sale_amount: null,
|
||||
variant_id: "variant_01FHZ9ZFG2F54TZMAWV0RJCQ5D",
|
||||
region_id: null,
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
},
|
||||
],
|
||||
sku: "IPOD2008GREEN",
|
||||
barcode: "1234_green",
|
||||
ean: null,
|
||||
upc: "1234_green",
|
||||
inventory_quantity: 30,
|
||||
allow_backorder: true,
|
||||
manage_inventory: true,
|
||||
hs_code: null,
|
||||
origin_country: null,
|
||||
mid_code: null,
|
||||
material: null,
|
||||
weight: 567,
|
||||
length: null,
|
||||
height: null,
|
||||
width: null,
|
||||
options: [
|
||||
{
|
||||
id: "optval_01FHZ9ZFG2K7GG7B79VVZW6AKN",
|
||||
value: "Green",
|
||||
option_id: "opt_01FHZ9ZFCPCQ7B1MPDD9X9YQX3",
|
||||
variant_id: "variant_01FHZ9ZFG2F54TZMAWV0RJCQ5D",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: {
|
||||
sh_id: 39072856,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "variant_01FHZ9ZFGN4GZN3P6JEB39C9G8",
|
||||
title: "Black",
|
||||
product_id: "prod_01FHZ9ZFC3KKYKA35NXNJ5A7FR",
|
||||
prices: [
|
||||
{
|
||||
id: "ma_01FHZ9ZFGVNSHF4YRANBJSVN11",
|
||||
currency_code: "usd",
|
||||
amount: 19900,
|
||||
sale_amount: null,
|
||||
variant_id: "variant_01FHZ9ZFGN4GZN3P6JEB39C9G8",
|
||||
region_id: null,
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
},
|
||||
],
|
||||
sku: "IPOD2008BLACK",
|
||||
barcode: "1234_black",
|
||||
ean: null,
|
||||
upc: "1234_black",
|
||||
inventory_quantity: 40,
|
||||
allow_backorder: true,
|
||||
manage_inventory: true,
|
||||
hs_code: null,
|
||||
origin_country: null,
|
||||
mid_code: null,
|
||||
material: null,
|
||||
weight: 567,
|
||||
length: null,
|
||||
height: null,
|
||||
width: null,
|
||||
options: [
|
||||
{
|
||||
id: "optval_01FHZ9ZFGNJXHVHFERQHR1AXHX",
|
||||
value: "Black",
|
||||
option_id: "opt_01FHZ9ZFCPCQ7B1MPDD9X9YQX3",
|
||||
variant_id: "variant_01FHZ9ZFGN4GZN3P6JEB39C9G8",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: {
|
||||
sh_id: 457924702,
|
||||
},
|
||||
},
|
||||
],
|
||||
profile_id: "sp_01FHZ9YRZT5DD4HTKJG2T38WXD",
|
||||
weight: null,
|
||||
length: null,
|
||||
height: null,
|
||||
width: null,
|
||||
hs_code: null,
|
||||
origin_country: null,
|
||||
mid_code: null,
|
||||
material: null,
|
||||
collection_id: null,
|
||||
collection: null,
|
||||
type_id: null,
|
||||
type: null,
|
||||
tags: [
|
||||
{
|
||||
id: "ptag_01FHZ9ZFBVN7EK9VZ2EC0T2Y1A",
|
||||
value: "Emotive",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: "ptag_01FHZ9ZFBY01226XFYXP0R3EEK",
|
||||
value: " Flash Memory",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: "ptag_01FHZ9ZFC0N8S7G85F2TZXKSAM",
|
||||
value: " MP3",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: "ptag_01FHZ9ZFC2K6V0A43S398DC2Q2",
|
||||
value: " Music",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
discountable: true,
|
||||
external_id: "shopify_ipod",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
}
|
||||
|
||||
const shopifyIpod = {
|
||||
id: "shopify_ipod",
|
||||
title: "IPod Nano - 8GB",
|
||||
body_html:
|
||||
"<p>It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.</p>",
|
||||
vendor: "Apple",
|
||||
product_type: "Cult Products",
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
handle: "ipod-nano",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
published_at: "2007-12-31T19:00:00-05:00",
|
||||
template_suffix: null,
|
||||
status: "active",
|
||||
published_scope: "web",
|
||||
tags: "Emotive, Flash Memory, MP3, Music",
|
||||
admin_graphql_api_id: "gid://shopify/Product/632910392",
|
||||
variants: [
|
||||
{
|
||||
id: 808950810,
|
||||
product_id: 632910392,
|
||||
title: "Pink",
|
||||
price: "199.00",
|
||||
sku: "IPOD2008PINK",
|
||||
position: 1,
|
||||
inventory_policy: "continue",
|
||||
compare_at_price: null,
|
||||
fulfillment_service: "manual",
|
||||
inventory_management: "shopify",
|
||||
option1: "Pink",
|
||||
option2: null,
|
||||
option3: null,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
taxable: true,
|
||||
barcode: "1234_pink",
|
||||
grams: 567,
|
||||
image_id: 562641783,
|
||||
weight: 1.25,
|
||||
weight_unit: "lb",
|
||||
inventory_item_id: 808950810,
|
||||
inventory_quantity: 10,
|
||||
old_inventory_quantity: 10,
|
||||
presentment_prices: [
|
||||
{
|
||||
price: {
|
||||
amount: "199.00",
|
||||
currency_code: "USD",
|
||||
},
|
||||
compare_at_price: null,
|
||||
},
|
||||
],
|
||||
requires_shipping: true,
|
||||
admin_graphql_api_id: "gid://shopify/ProductVariant/808950810",
|
||||
},
|
||||
{
|
||||
id: 49148385,
|
||||
product_id: 632910392,
|
||||
title: "Red",
|
||||
price: "199.00",
|
||||
sku: "IPOD2008RED",
|
||||
position: 2,
|
||||
inventory_policy: "continue",
|
||||
compare_at_price: null,
|
||||
fulfillment_service: "manual",
|
||||
inventory_management: "shopify",
|
||||
option1: "Red",
|
||||
option2: null,
|
||||
option3: null,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
taxable: true,
|
||||
barcode: "1234_red",
|
||||
grams: 567,
|
||||
image_id: null,
|
||||
weight: 1.25,
|
||||
weight_unit: "lb",
|
||||
inventory_item_id: 49148385,
|
||||
inventory_quantity: 20,
|
||||
old_inventory_quantity: 20,
|
||||
presentment_prices: [
|
||||
{
|
||||
price: {
|
||||
amount: "199.00",
|
||||
currency_code: "USD",
|
||||
},
|
||||
compare_at_price: null,
|
||||
},
|
||||
],
|
||||
requires_shipping: true,
|
||||
admin_graphql_api_id: "gid://shopify/ProductVariant/49148385",
|
||||
},
|
||||
{
|
||||
id: 39072856,
|
||||
product_id: 632910392,
|
||||
title: "Green",
|
||||
price: "199.00",
|
||||
sku: "IPOD2008GREEN",
|
||||
position: 3,
|
||||
inventory_policy: "continue",
|
||||
compare_at_price: null,
|
||||
fulfillment_service: "manual",
|
||||
inventory_management: "shopify",
|
||||
option1: "Green",
|
||||
option2: null,
|
||||
option3: null,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
taxable: true,
|
||||
barcode: "1234_green",
|
||||
grams: 567,
|
||||
image_id: null,
|
||||
weight: 1.25,
|
||||
weight_unit: "lb",
|
||||
inventory_item_id: 39072856,
|
||||
inventory_quantity: 30,
|
||||
old_inventory_quantity: 30,
|
||||
presentment_prices: [
|
||||
{
|
||||
price: {
|
||||
amount: "199.00",
|
||||
currency_code: "USD",
|
||||
},
|
||||
compare_at_price: null,
|
||||
},
|
||||
],
|
||||
requires_shipping: true,
|
||||
admin_graphql_api_id: "gid://shopify/ProductVariant/39072856",
|
||||
},
|
||||
{
|
||||
id: 457924702,
|
||||
product_id: 632910392,
|
||||
title: "Black",
|
||||
price: "199.00",
|
||||
sku: "IPOD2008BLACK",
|
||||
position: 4,
|
||||
inventory_policy: "continue",
|
||||
compare_at_price: null,
|
||||
fulfillment_service: "manual",
|
||||
inventory_management: "shopify",
|
||||
option1: "Black",
|
||||
option2: null,
|
||||
option3: null,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
taxable: true,
|
||||
barcode: "1234_black",
|
||||
grams: 567,
|
||||
image_id: null,
|
||||
weight: 1.25,
|
||||
weight_unit: "lb",
|
||||
inventory_item_id: 457924702,
|
||||
inventory_quantity: 40,
|
||||
old_inventory_quantity: 40,
|
||||
presentment_prices: [
|
||||
{
|
||||
price: {
|
||||
amount: "199.00",
|
||||
currency_code: "USD",
|
||||
},
|
||||
compare_at_price: null,
|
||||
},
|
||||
],
|
||||
requires_shipping: true,
|
||||
admin_graphql_api_id: "gid://shopify/ProductVariant/457924702",
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
id: 594680422,
|
||||
product_id: 632910392,
|
||||
name: "Color",
|
||||
position: 1,
|
||||
values: ["Pink", "Red", "Green", "Black"],
|
||||
},
|
||||
],
|
||||
images: [
|
||||
{
|
||||
id: 850703190,
|
||||
product_id: 632910392,
|
||||
position: 1,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
alt: null,
|
||||
width: 123,
|
||||
height: 456,
|
||||
src:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
variant_ids: [],
|
||||
admin_graphql_api_id: "gid://shopify/ProductImage/850703190",
|
||||
},
|
||||
{
|
||||
id: 562641783,
|
||||
product_id: 632910392,
|
||||
position: 2,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
alt: null,
|
||||
width: 123,
|
||||
height: 456,
|
||||
src:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano-2.png?v=1633120966",
|
||||
variant_ids: [808950810],
|
||||
admin_graphql_api_id: "gid://shopify/ProductImage/562641783",
|
||||
},
|
||||
{
|
||||
id: 378407906,
|
||||
product_id: 632910392,
|
||||
position: 3,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
alt: null,
|
||||
width: 123,
|
||||
height: 456,
|
||||
src:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
variant_ids: [],
|
||||
admin_graphql_api_id: "gid://shopify/ProductImage/378407906",
|
||||
},
|
||||
],
|
||||
image: {
|
||||
id: 850703190,
|
||||
product_id: 632910392,
|
||||
position: 1,
|
||||
created_at: "2021-10-01T16:42:46-04:00",
|
||||
updated_at: "2021-10-01T16:42:46-04:00",
|
||||
alt: null,
|
||||
width: 123,
|
||||
height: 456,
|
||||
src:
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
variant_ids: [],
|
||||
admin_graphql_api_id: "gid://shopify/ProductImage/850703190",
|
||||
},
|
||||
}
|
||||
|
||||
export const shopifyProducts = {
|
||||
ipod: shopifyIpod,
|
||||
new_ipod: { ...shopifyIpod, id: "shopify_new_ipod" },
|
||||
ipod_update: {
|
||||
...shopifyIpod,
|
||||
title: "IPod Nano",
|
||||
options: [
|
||||
...shopifyIpod.options,
|
||||
{
|
||||
id: 594680423,
|
||||
product_id: 632910392,
|
||||
name: "Memory",
|
||||
position: 2,
|
||||
values: ["8GB", "16GB"],
|
||||
},
|
||||
],
|
||||
variants: [
|
||||
{ ...shopifyIpod.variants[0], title: "Pink / 8GB", option2: "8GB" },
|
||||
{ ...shopifyIpod.variants[1], title: "Red / 8GB", option2: "8GB" },
|
||||
{ ...shopifyIpod.variants[2], title: "Green / 8GB", option2: "8GB" },
|
||||
{ ...shopifyIpod.variants[3], title: "Black / 8GB", option2: "8GB" },
|
||||
{ ...shopifyIpod.variants[0], title: "Pink / 16GB", option2: "16GB" },
|
||||
{ ...shopifyIpod.variants[1], title: "Red / 16GB", option2: "16GB" },
|
||||
{ ...shopifyIpod.variants[2], title: "Green / 16GB", option2: "16GB" },
|
||||
{ ...shopifyIpod.variants[3], title: "Black / 16GB", option2: "16GB" },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ShopifyCollectionService create normalizes a collection from Shopify 1`] = `
|
||||
Object {
|
||||
"handle": "spring",
|
||||
"metadata": Object {
|
||||
"sh_body": "spring collection",
|
||||
"sh_id": "spring",
|
||||
},
|
||||
"title": "Spring",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,157 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ShopifyProductService normalizeProduct_ succesfully normalizes a product from Shopify 1`] = `
|
||||
Object {
|
||||
"collection_id": null,
|
||||
"description": "<p>It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.</p>",
|
||||
"external_id": "shopify_ipod",
|
||||
"handle": "ipod-nano",
|
||||
"images": Array [
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano-2.png?v=1633120966",
|
||||
"https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
],
|
||||
"is_giftcard": false,
|
||||
"options": Array [
|
||||
Object {
|
||||
"title": "Color",
|
||||
"values": Array [
|
||||
Object {
|
||||
"value": "Pink",
|
||||
},
|
||||
Object {
|
||||
"value": "Red",
|
||||
},
|
||||
Object {
|
||||
"value": "Green",
|
||||
},
|
||||
Object {
|
||||
"value": "Black",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"product_type": Object {
|
||||
"value": "Cult Products",
|
||||
},
|
||||
"status": "proposed",
|
||||
"tags": Array [
|
||||
Object {
|
||||
"value": "Emotive",
|
||||
},
|
||||
Object {
|
||||
"value": " Flash Memory",
|
||||
},
|
||||
Object {
|
||||
"value": " MP3",
|
||||
},
|
||||
Object {
|
||||
"value": " Music",
|
||||
},
|
||||
],
|
||||
"thumbnail": "https://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1633120966",
|
||||
"title": "IPod Nano - 8GB",
|
||||
"variants": Array [
|
||||
Object {
|
||||
"allow_backorder": true,
|
||||
"barcode": "1234_pink",
|
||||
"inventory_quantity": 10,
|
||||
"manage_inventory": true,
|
||||
"metadata": Object {
|
||||
"sh_id": 808950810,
|
||||
},
|
||||
"options": Array [
|
||||
Object {
|
||||
"value": "Pink",
|
||||
},
|
||||
],
|
||||
"prices": Array [
|
||||
Object {
|
||||
"amount": 19900,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
"sku": "IPOD2008PINK",
|
||||
"title": "Pink",
|
||||
"upc": "1234_pink",
|
||||
"variant_rank": 1,
|
||||
"weight": 567,
|
||||
},
|
||||
Object {
|
||||
"allow_backorder": true,
|
||||
"barcode": "1234_red",
|
||||
"inventory_quantity": 20,
|
||||
"manage_inventory": true,
|
||||
"metadata": Object {
|
||||
"sh_id": 49148385,
|
||||
},
|
||||
"options": Array [
|
||||
Object {
|
||||
"value": "Red",
|
||||
},
|
||||
],
|
||||
"prices": Array [
|
||||
Object {
|
||||
"amount": 19900,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
"sku": "IPOD2008RED",
|
||||
"title": "Red",
|
||||
"upc": "1234_red",
|
||||
"variant_rank": 2,
|
||||
"weight": 567,
|
||||
},
|
||||
Object {
|
||||
"allow_backorder": true,
|
||||
"barcode": "1234_green",
|
||||
"inventory_quantity": 30,
|
||||
"manage_inventory": true,
|
||||
"metadata": Object {
|
||||
"sh_id": 39072856,
|
||||
},
|
||||
"options": Array [
|
||||
Object {
|
||||
"value": "Green",
|
||||
},
|
||||
],
|
||||
"prices": Array [
|
||||
Object {
|
||||
"amount": 19900,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
"sku": "IPOD2008GREEN",
|
||||
"title": "Green",
|
||||
"upc": "1234_green",
|
||||
"variant_rank": 3,
|
||||
"weight": 567,
|
||||
},
|
||||
Object {
|
||||
"allow_backorder": true,
|
||||
"barcode": "1234_black",
|
||||
"inventory_quantity": 40,
|
||||
"manage_inventory": true,
|
||||
"metadata": Object {
|
||||
"sh_id": 457924702,
|
||||
},
|
||||
"options": Array [
|
||||
Object {
|
||||
"value": "Black",
|
||||
},
|
||||
],
|
||||
"prices": Array [
|
||||
Object {
|
||||
"amount": 19900,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
"sku": "IPOD2008BLACK",
|
||||
"title": "Black",
|
||||
"upc": "1234_black",
|
||||
"variant_rank": 4,
|
||||
"weight": 567,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
import ShopifyCollectionService from "../shopify-collection"
|
||||
import { ProductCollectionServiceMock } from "../__mocks__/product-collection"
|
||||
import { ShopifyProductServiceMock } from "../__mocks__/shopify-product"
|
||||
import { shopifyProducts } from "../__mocks__/test-products"
|
||||
|
||||
describe("ShopifyCollectionService", () => {
|
||||
describe("create", () => {
|
||||
const shopifyCollectionService = new ShopifyCollectionService({
|
||||
manager: MockManager,
|
||||
shopifyProductService: ShopifyProductServiceMock,
|
||||
productCollectionService: ProductCollectionServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("creates a collection with a product", async () => {
|
||||
const collects = [
|
||||
{
|
||||
collection_id: "spring",
|
||||
created_at: "2018-04-25T13:51:12-04:00",
|
||||
id: 841564295,
|
||||
position: 2,
|
||||
product_id: "shopify_ipod",
|
||||
sort_value: "0000000002",
|
||||
updated_at: "2018-04-25T13:51:12-04:00",
|
||||
},
|
||||
]
|
||||
const collections = [
|
||||
{
|
||||
id: "spring",
|
||||
body_html: "spring collection",
|
||||
title: "Spring",
|
||||
handle: "spring",
|
||||
},
|
||||
]
|
||||
const products = [shopifyProducts.ipod]
|
||||
|
||||
const results = await shopifyCollectionService.createWithProducts(
|
||||
collects,
|
||||
collections,
|
||||
products
|
||||
)
|
||||
|
||||
expect(
|
||||
ProductCollectionServiceMock.retrieveByHandle
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(ProductCollectionServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyProductServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(results).toEqual([
|
||||
{
|
||||
id: "col_spring",
|
||||
title: "Spring",
|
||||
handle: "spring",
|
||||
metadata: {
|
||||
sh_id: "spring",
|
||||
sh_body: "spring collection",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("normalizes a collection from Shopify", () => {
|
||||
const shopifyCollection = {
|
||||
id: "spring",
|
||||
body_html: "spring collection",
|
||||
title: "Spring",
|
||||
handle: "spring",
|
||||
}
|
||||
|
||||
const normalized = shopifyCollectionService.normalizeCollection_(
|
||||
shopifyCollection
|
||||
)
|
||||
|
||||
expect(normalized).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
import ShopifyProductService from "../shopify-product"
|
||||
import { ProductServiceMock } from "../__mocks__/product-service"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile"
|
||||
import { ShopifyClientServiceMock } from "../__mocks__/shopify-client"
|
||||
import { ShopifyRedisServiceMock } from "../__mocks__/shopify-redis"
|
||||
import { medusaProducts, shopifyProducts } from "../__mocks__/test-products"
|
||||
|
||||
describe("ShopifyProductService", () => {
|
||||
describe("normalizeProduct_", () => {
|
||||
const shopifyProductService = new ShopifyProductService({
|
||||
manager: MockManager,
|
||||
shopifyClientService: ShopifyClientServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("succesfully normalizes a product from Shopify", async () => {
|
||||
const data = await ShopifyClientServiceMock.get({ path: "products/ipod" })
|
||||
|
||||
const normalized = shopifyProductService.normalizeProduct_(data)
|
||||
|
||||
expect(normalized).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
const shopifyProductService = new ShopifyProductService({
|
||||
manager: MockManager,
|
||||
shopifyClientService: ShopifyClientServiceMock,
|
||||
productService: ProductServiceMock,
|
||||
shopifyRedisService: ShopifyRedisServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("succesfully creates a product from Shopify", async () => {
|
||||
const data = shopifyProducts.new_ipod
|
||||
|
||||
const product = await shopifyProductService.create(data)
|
||||
|
||||
expect(ShopifyRedisServiceMock.shouldIgnore).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyRedisServiceMock.addIgnore).toHaveBeenCalledTimes(1)
|
||||
expect(ShippingProfileServiceMock.retrieveDefault).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(ProductServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(4)
|
||||
expect(product).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "prod_ipod",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const shopifyProductService = new ShopifyProductService({
|
||||
manager: MockManager,
|
||||
shopifyClientService: ShopifyClientServiceMock,
|
||||
productService: ProductServiceMock,
|
||||
shopifyRedisService: ShopifyRedisServiceMock,
|
||||
shippingProfileService: ShippingProfileServiceMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("updates a product and adds 4 new variants", async () => {
|
||||
const data = shopifyProducts.ipod_update
|
||||
jest
|
||||
.spyOn(shopifyProductService, "addProductOptions_")
|
||||
.mockImplementation(() => ({
|
||||
...medusaProducts.ipod,
|
||||
options: [
|
||||
...medusaProducts.ipod.options,
|
||||
{
|
||||
id: "opt_01FHZ9ZFCPCQ7B1MPDD9X9YQX4",
|
||||
title: "Memory",
|
||||
product_id: "prod_01FHZ9ZFC3KKYKA35NXNJ5A7FR",
|
||||
created_at: "2021-10-14T11:46:10.391Z",
|
||||
updated_at: "2021-10-14T11:46:10.391Z",
|
||||
deleted_at: null,
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
await shopifyProductService.update(data)
|
||||
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(8)
|
||||
expect(ProductServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyClientServiceMock.get).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("updates a product and deletes 2 existing variants", async () => {
|
||||
const data = { ...shopifyProducts.ipod, id: "shopify_deleted" }
|
||||
data.variants = data.variants.slice(1, -1)
|
||||
|
||||
await shopifyProductService.update(data)
|
||||
|
||||
expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(2)
|
||||
expect(ProductServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ShopifyClientServiceMock.get).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { pager } from "../utils/pager"
|
||||
import { createClient } from "../utils/create-client"
|
||||
import { DataType } from "@shopify/shopify-api"
|
||||
|
||||
class ShopifyClientService extends BaseService {
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
constructor({}, options) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
|
||||
/** @private @const {ShopifyRestClient} */
|
||||
this.client_ = createClient(this.options)
|
||||
}
|
||||
|
||||
get(params) {
|
||||
return this.client_.get(params)
|
||||
}
|
||||
|
||||
async list(path, extraHeaders = null, extraQuery = {}) {
|
||||
return await pager(this.client_, path, extraHeaders, extraQuery)
|
||||
}
|
||||
|
||||
delete(params) {
|
||||
return this.client_.post(params)
|
||||
}
|
||||
|
||||
post(params) {
|
||||
return this.client_.post({
|
||||
path: params.path,
|
||||
body: params.body,
|
||||
type: DataType.JSON,
|
||||
})
|
||||
}
|
||||
|
||||
put(params) {
|
||||
return this.client_.post(params)
|
||||
}
|
||||
}
|
||||
|
||||
export default ShopifyClientService
|
||||
@@ -0,0 +1,104 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { removeIndex } from "../utils/remove-index"
|
||||
|
||||
class ShopifyCollectionService extends BaseService {
|
||||
constructor(
|
||||
{ manager, shopifyProductService, productCollectionService },
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
/** @private @const {ShopifyProductService} */
|
||||
this.productService_ = shopifyProductService
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.collectionService_ = productCollectionService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ShopifyCollectionService({
|
||||
manager: transactionManager,
|
||||
options: this.options,
|
||||
shopifyProductService: this.productService_,
|
||||
productCollectionService: this.collectionService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object[]} collects
|
||||
* @param {Object[]} collections
|
||||
* @param {Object[]} products
|
||||
* @return {Promise}
|
||||
*/
|
||||
async createWithProducts(collects, collections, products) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const normalizedCollections = collections.map((c) =>
|
||||
this.normalizeCollection_(c)
|
||||
)
|
||||
|
||||
const result = []
|
||||
|
||||
for (const nc of normalizedCollections) {
|
||||
let collection = await this.collectionService_
|
||||
.retrieveByHandle(nc.handle)
|
||||
.catch((_) => undefined)
|
||||
|
||||
if (!collection) {
|
||||
collection = await this.collectionService_
|
||||
.withTransaction(manager)
|
||||
.create(nc)
|
||||
}
|
||||
|
||||
const productIds = collects.reduce((productIds, c) => {
|
||||
if (c.collection_id === collection.metadata.sh_id) {
|
||||
productIds.push(c.product_id)
|
||||
}
|
||||
return productIds
|
||||
}, [])
|
||||
|
||||
const reducedProducts = products.reduce((reducedProducts, p) => {
|
||||
if (productIds.includes(p.id)) {
|
||||
reducedProducts.push(p)
|
||||
removeIndex(products, p)
|
||||
}
|
||||
return reducedProducts
|
||||
}, [])
|
||||
|
||||
for (const product of reducedProducts) {
|
||||
await this.productService_
|
||||
.withTransaction(manager)
|
||||
.create(product, collection.id)
|
||||
}
|
||||
|
||||
result.push(collection)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
normalizeCollection_(shopifyCollection) {
|
||||
return {
|
||||
title: shopifyCollection.title,
|
||||
handle: shopifyCollection.handle,
|
||||
metadata: {
|
||||
sh_id: shopifyCollection.id,
|
||||
sh_body: shopifyCollection.body_html,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ShopifyCollectionService
|
||||
553
packages/medusa-source-shopify/src/services/shopify-product.js
Normal file
553
packages/medusa-source-shopify/src/services/shopify-product.js
Normal file
@@ -0,0 +1,553 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import _ from "lodash"
|
||||
import { parsePrice } from "../utils/parse-price"
|
||||
import { INCLUDE_PRESENTMENT_PRICES } from "../utils/const"
|
||||
import axios from "axios"
|
||||
|
||||
class ShopifyProductService extends BaseService {
|
||||
constructor(
|
||||
{
|
||||
manager,
|
||||
productService,
|
||||
productVariantService,
|
||||
shippingProfileService,
|
||||
shopifyClientService,
|
||||
shopifyRedisService,
|
||||
},
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
/** @private @const {ProductService} */
|
||||
this.productService_ = productService
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
/** @private @const {ShippingProfileService} */
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
/** @private @const {ShopifyRestClient} */
|
||||
this.shopify_ = shopifyClientService
|
||||
|
||||
this.redis_ = shopifyRedisService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ShopifyProductService({
|
||||
manager: transactionManager,
|
||||
options: this.options,
|
||||
shippingProfileService: this.shippingProfileService_,
|
||||
productVariantService: this.productVariantService_,
|
||||
productService: this.productService_,
|
||||
shopifyClientService: this.shopify_,
|
||||
shopifyRedisService: this.redis_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a product based on an event in Shopify.
|
||||
* Also adds the product to a collection if a collection id is provided
|
||||
* @param {object} data
|
||||
* @param {string} collectionId optional
|
||||
* @return {Product} the created product
|
||||
*/
|
||||
async create(data, collectionId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const ignore = await this.redis_.shouldIgnore(data.id, "product.created")
|
||||
if (ignore) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingProduct = await this.productService_
|
||||
.withTransaction(manager)
|
||||
.retrieveByExternalId(data.id)
|
||||
.catch((_) => undefined)
|
||||
|
||||
if (existingProduct) {
|
||||
return await this.update(data)
|
||||
}
|
||||
|
||||
const normalizedProduct = this.normalizeProduct_(data, collectionId)
|
||||
normalizedProduct.profile_id = await this.getShippingProfile_(
|
||||
normalizedProduct.is_giftcard
|
||||
)
|
||||
|
||||
let variants = normalizedProduct.variants
|
||||
delete normalizedProduct.variants
|
||||
|
||||
const product = await this.productService_
|
||||
.withTransaction(manager)
|
||||
.create(normalizedProduct)
|
||||
|
||||
if (variants) {
|
||||
variants = variants.map((v) =>
|
||||
this.addVariantOptions_(v, product.options)
|
||||
)
|
||||
|
||||
for (const variant of variants) {
|
||||
await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.create(product.id, variant)
|
||||
}
|
||||
}
|
||||
|
||||
await this.redis_.addIgnore(data.id, "product.created")
|
||||
|
||||
return product
|
||||
})
|
||||
}
|
||||
|
||||
async update(data) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const ignore = await this.redis_.shouldIgnore(data.id, "product.updated")
|
||||
if (ignore) {
|
||||
return
|
||||
}
|
||||
|
||||
let existing = await this.productService_
|
||||
.retrieveByExternalId(data.id, {
|
||||
relations: ["variants", "options"],
|
||||
})
|
||||
.catch((_) => undefined)
|
||||
|
||||
if (!existing) {
|
||||
return await this.create(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Variants received from webhook do not include
|
||||
* presentment prices. Therefore, we fetch them
|
||||
* separately, and add to the data object.
|
||||
*/
|
||||
const { variants } = await this.shopify_
|
||||
.get({
|
||||
path: `products/${data.id}`,
|
||||
extraHeaders: INCLUDE_PRESENTMENT_PRICES,
|
||||
})
|
||||
.then((res) => {
|
||||
return res.body.product
|
||||
})
|
||||
|
||||
data.variants = variants || []
|
||||
const normalized = this.normalizeProduct_(data)
|
||||
|
||||
existing = await this.addProductOptions_(existing, normalized.options)
|
||||
|
||||
await this.updateVariants_(existing, normalized.variants)
|
||||
await this.deleteVariants_(existing, normalized.variants)
|
||||
delete normalized.variants
|
||||
|
||||
const update = {}
|
||||
|
||||
for (const key of Object.keys(normalized)) {
|
||||
if (normalized[key] !== existing[key]) {
|
||||
update[key] = normalized[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(update)) {
|
||||
await this.redis_.addIgnore(data.id, "product.updated")
|
||||
return await this.productService_
|
||||
.withTransaction(manager)
|
||||
.update(existing.id, update)
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a product based on an event in Shopify
|
||||
* @param {string} id
|
||||
* @return {Promise}
|
||||
*/
|
||||
async delete(id) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const product = await this.productService_.retrieveByExternalId(id)
|
||||
|
||||
return await this.productService_
|
||||
.withTransaction(manager)
|
||||
.delete(product.id)
|
||||
})
|
||||
}
|
||||
|
||||
async shopifyProductUpdate(id, fields) {
|
||||
const product = await this.productService_.retrieve(id, {
|
||||
relations: ["tags", "type"],
|
||||
})
|
||||
|
||||
// Event was not emitted by update
|
||||
if (!fields) {
|
||||
return
|
||||
}
|
||||
|
||||
const update = {
|
||||
product: {
|
||||
id: product.external_id,
|
||||
},
|
||||
}
|
||||
|
||||
if (fields.includes("title")) {
|
||||
update.product.title = product.title
|
||||
}
|
||||
|
||||
if (fields.includes("tags")) {
|
||||
const values = product.tags.map((t) => t.value)
|
||||
update.product.tags = values.join(",")
|
||||
}
|
||||
|
||||
if (fields.includes("description")) {
|
||||
update.product.body_html = product.description
|
||||
}
|
||||
|
||||
if (fields.includes("handle")) {
|
||||
update.product.handle = product.handle
|
||||
}
|
||||
|
||||
if (fields.includes("type")) {
|
||||
update.product.type = product.type?.value
|
||||
}
|
||||
|
||||
await axios
|
||||
.put(
|
||||
`https://${this.options.domain}.myshopify.com/admin/api/2021-10/products/${product.external_id}.json`,
|
||||
update,
|
||||
{
|
||||
headers: {
|
||||
"X-Shopify-Access-Token": this.options.password,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`An error occured while attempting to issue a product update to Shopify: ${err.message}`
|
||||
)
|
||||
})
|
||||
|
||||
await this.redis_.addIgnore(product.external_id, "product.updated")
|
||||
}
|
||||
|
||||
async shopifyVariantUpdate(id, fields) {
|
||||
const variant = await this.productVariantService_.retrieve(id, {
|
||||
relations: ["prices", "product"],
|
||||
})
|
||||
|
||||
// Event was not emitted by update
|
||||
if (!fields) {
|
||||
return
|
||||
}
|
||||
|
||||
const update = {
|
||||
variant: {
|
||||
id: variant.metadata.sh_id,
|
||||
},
|
||||
}
|
||||
|
||||
if (fields.includes("title")) {
|
||||
update.variant.title = variant.title
|
||||
}
|
||||
|
||||
if (fields.includes("allow_backorder")) {
|
||||
update.variant.inventory_police = variant.allow_backorder
|
||||
? "continue"
|
||||
: "deny"
|
||||
}
|
||||
|
||||
if (fields.includes("prices")) {
|
||||
update.variant.price = variant.prices[0].amount / 100
|
||||
}
|
||||
|
||||
if (fields.includes("weight")) {
|
||||
update.variant.grams = variant.weight
|
||||
}
|
||||
|
||||
await axios
|
||||
.put(
|
||||
`https://${this.options.domain}.myshopify.com/admin/api/2021-10/variants/${variant.metadata.sh_id}.json`,
|
||||
update,
|
||||
{
|
||||
headers: {
|
||||
"X-Shopify-Access-Token": this.options.password,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`An error occured while attempting to issue a product update to Shopify: ${err.message}`
|
||||
)
|
||||
})
|
||||
|
||||
await this.redis_.addIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.updated"
|
||||
)
|
||||
}
|
||||
|
||||
async shopifyVariantDelete(productId, metadata) {
|
||||
const product = await this.productService_.retrieve(productId)
|
||||
|
||||
await axios
|
||||
.delete(
|
||||
`https://${this.options.domain}.myshopify.com/admin/api/2021-10/products/${product.external_id}/variants/${metadata.sh_id}.json`,
|
||||
{
|
||||
headers: {
|
||||
"X-Shopify-Access-Token": this.options.password,
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`An error occured while attempting to issue a product variant delete to Shopify: ${err.message}`
|
||||
)
|
||||
})
|
||||
|
||||
await this.redis_.addIgnore(metadata.sh_id, "product-variant.deleted")
|
||||
}
|
||||
|
||||
async updateCollectionId(productId, collectionId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
return await this.productService_
|
||||
.withTransaction(manager)
|
||||
.update(productId, { collection_id: collectionId })
|
||||
})
|
||||
}
|
||||
|
||||
async updateVariants_(product, updateVariants) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const { id, variants, options } = product
|
||||
for (let variant of updateVariants) {
|
||||
const ignore =
|
||||
(await this.redis_.shouldIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.updated"
|
||||
)) ||
|
||||
(await this.redis_.shouldIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.created"
|
||||
))
|
||||
if (ignore) {
|
||||
continue
|
||||
}
|
||||
|
||||
variant = this.addVariantOptions_(variant, options)
|
||||
const match = variants.find((v) => v.sku === variant.sku)
|
||||
if (match) {
|
||||
await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.update(match.id, variant)
|
||||
} else {
|
||||
await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.create(id, variant)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async deleteVariants_(product, updateVariants) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const { variants } = product
|
||||
for (const variant of variants) {
|
||||
const ignore = await this.redis_.shouldIgnore(
|
||||
variant.metadata.sh_id,
|
||||
"product-variant.deleted"
|
||||
)
|
||||
if (ignore) {
|
||||
continue
|
||||
}
|
||||
|
||||
const match = updateVariants.find((v) => v.sku === variant.sku)
|
||||
if (!match) {
|
||||
await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.delete(variant.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
addVariantOptions_(variant, productOptions) {
|
||||
const options = productOptions.map((o, i) => ({
|
||||
option_id: o.id,
|
||||
...variant.options[i],
|
||||
}))
|
||||
variant.options = options
|
||||
|
||||
return variant
|
||||
}
|
||||
|
||||
async addProductOptions_(product, updateOptions) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const { id, options } = product
|
||||
|
||||
for (const option of updateOptions) {
|
||||
const match = options.find((o) => o.title === option.title)
|
||||
if (!match) {
|
||||
await this.productService_
|
||||
.withTransaction(manager)
|
||||
.addOption(id, option.title)
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.productService_.retrieve(id, {
|
||||
relations: ["variants", "options"],
|
||||
})
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async getShippingProfile_(isGiftCard) {
|
||||
let shippingProfile
|
||||
if (isGiftCard) {
|
||||
shippingProfile =
|
||||
await this.shippingProfileService_.retrieveGiftCardDefault()
|
||||
} else {
|
||||
shippingProfile = await this.shippingProfileService_.retrieveDefault()
|
||||
}
|
||||
|
||||
return shippingProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a product, with a possible optional collection id
|
||||
* @param {object} product
|
||||
* @param {string} collectionId optional
|
||||
* @return {object} normalized object
|
||||
*/
|
||||
normalizeProduct_(product, collectionId) {
|
||||
return {
|
||||
title: product.title,
|
||||
handle: product.handle,
|
||||
description: product.body_html,
|
||||
product_type: {
|
||||
value: product.product_type,
|
||||
},
|
||||
is_giftcard: product.product_type === "Gift Cards",
|
||||
options:
|
||||
product.options.map((option) => this.normalizeProductOption_(option)) ||
|
||||
[],
|
||||
variants:
|
||||
product.variants.map((variant) => this.normalizeVariant_(variant)) ||
|
||||
[],
|
||||
tags: product.tags.split(",").map((tag) => this.normalizeTag_(tag)) || [],
|
||||
images: product.images.map((img) => img.src) || [],
|
||||
thumbnail: product.image?.src || null,
|
||||
collection_id: collectionId || null,
|
||||
external_id: product.id,
|
||||
status: "proposed",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a product option
|
||||
* @param {object} option
|
||||
* @return {object} normalized ProductOption
|
||||
*/
|
||||
normalizeProductOption_(option) {
|
||||
return {
|
||||
title: option.name,
|
||||
values: option.values.map((v) => {
|
||||
return { value: v }
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a product variant
|
||||
* @param {object} variant
|
||||
* @return {object} normalized variant
|
||||
*/
|
||||
normalizeVariant_(variant) {
|
||||
return {
|
||||
title: variant.title,
|
||||
prices: this.normalizePrices_(variant.presentment_prices),
|
||||
sku: variant.sku || null,
|
||||
barcode: variant.barcode || null,
|
||||
upc: variant.barcode || null,
|
||||
inventory_quantity: variant.inventory_quantity,
|
||||
variant_rank: variant.position,
|
||||
allow_backorder: variant.inventory_policy === "continue",
|
||||
manage_inventory: variant.inventory_management === "shopify",
|
||||
weight: variant.grams,
|
||||
options: this.normalizeVariantOptions_(
|
||||
variant.option1,
|
||||
variant.option2,
|
||||
variant.option3
|
||||
),
|
||||
metadata: {
|
||||
sh_id: variant.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an array of presentment prices
|
||||
* @param {array} presentmentPrices
|
||||
* @return {Object[]} array of normalized prices
|
||||
*/
|
||||
normalizePrices_(presentmentPrices) {
|
||||
return presentmentPrices.map((p) => {
|
||||
return {
|
||||
amount: parsePrice(p.price.amount),
|
||||
currency_code: p.price.currency_code.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the three possble variant options
|
||||
* @param {string} option1
|
||||
* @param {string} option2
|
||||
* @param {string} option3
|
||||
* @return {Object[]} normalized variant options
|
||||
*/
|
||||
normalizeVariantOptions_(option1, option2, option3) {
|
||||
const opts = []
|
||||
if (option1) {
|
||||
opts.push({
|
||||
value: option1,
|
||||
})
|
||||
}
|
||||
|
||||
if (option2) {
|
||||
opts.push({
|
||||
value: option2,
|
||||
})
|
||||
}
|
||||
|
||||
if (option3) {
|
||||
opts.push({
|
||||
value: option3,
|
||||
})
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a tag
|
||||
* @param {string} tag
|
||||
* @return {Object} normalized Tag
|
||||
*/
|
||||
normalizeTag_(tag) {
|
||||
return {
|
||||
value: tag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ShopifyProductService
|
||||
30
packages/medusa-source-shopify/src/services/shopify-redis.js
Normal file
30
packages/medusa-source-shopify/src/services/shopify-redis.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { IGNORE_THRESHOLD } from "../utils/const"
|
||||
|
||||
class shopifyRedisService extends BaseService {
|
||||
constructor({ redisClient }, options) {
|
||||
super()
|
||||
|
||||
this.options_ = options
|
||||
|
||||
/** @private @const {RedisClient} */
|
||||
this.redis_ = redisClient
|
||||
}
|
||||
|
||||
async addIgnore(id, side) {
|
||||
const key = `${id}_ignore_${side}`
|
||||
return await this.redis_.set(
|
||||
key,
|
||||
1,
|
||||
"EX",
|
||||
this.options_.ignore_threshold || IGNORE_THRESHOLD
|
||||
)
|
||||
}
|
||||
|
||||
async shouldIgnore(id, action) {
|
||||
const key = `${id}_ignore_${action}`
|
||||
return await this.redis_.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
export default shopifyRedisService
|
||||
78
packages/medusa-source-shopify/src/services/shopify.js
Normal file
78
packages/medusa-source-shopify/src/services/shopify.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { INCLUDE_PRESENTMENT_PRICES } from "../utils/const"
|
||||
|
||||
class ShopifyService extends BaseService {
|
||||
constructor(
|
||||
{
|
||||
manager,
|
||||
shippingProfileService,
|
||||
shopifyProductService,
|
||||
shopifyCollectionService,
|
||||
shopifyClientService,
|
||||
},
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
/** @private @const {ShippingProfileService} */
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
/** @private @const {ShopifyProductService} */
|
||||
this.productService_ = shopifyProductService
|
||||
/** @private @const {ShopifyCollectionService} */
|
||||
this.collectionService_ = shopifyCollectionService
|
||||
/** @private @const {ShopifyRestClient} */
|
||||
this.client_ = shopifyClientService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ShopifyService({
|
||||
manager: transactionManager,
|
||||
options: this.options,
|
||||
shippingProfileService: this.shippingProfileService_,
|
||||
shopifyClientService: this.client_,
|
||||
shopifyProductService: this.productService_,
|
||||
shopifyCollectionService: this.collectionService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
async importShopify() {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
await this.shippingProfileService_.createDefault()
|
||||
await this.shippingProfileService_.createGiftCardDefault()
|
||||
|
||||
const products = await this.client_.list(
|
||||
"products",
|
||||
INCLUDE_PRESENTMENT_PRICES
|
||||
)
|
||||
const customCollections = await this.client_.list("custom_collections")
|
||||
const smartCollections = await this.client_.list("smart_collections")
|
||||
const collects = await this.client_.list("collects")
|
||||
|
||||
await this.collectionService_
|
||||
.withTransaction(manager)
|
||||
.createWithProducts(
|
||||
collects,
|
||||
[...customCollections, ...smartCollections],
|
||||
products
|
||||
)
|
||||
|
||||
for (const product of products) {
|
||||
await this.productService_.withTransaction(manager).create(product)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ShopifyService
|
||||
13
packages/medusa-source-shopify/src/utils/build-query.js
Normal file
13
packages/medusa-source-shopify/src/utils/build-query.js
Normal file
@@ -0,0 +1,13 @@
|
||||
export function buildQuery(query) {
|
||||
let path = ""
|
||||
|
||||
if (query) {
|
||||
const queryString = Object.entries(query).map(([key, value]) => {
|
||||
return `${key}=${value}`
|
||||
})
|
||||
|
||||
path = `?${queryString.join("&")}`
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
5
packages/medusa-source-shopify/src/utils/const.js
Normal file
5
packages/medusa-source-shopify/src/utils/const.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const INCLUDE_PRESENTMENT_PRICES = {
|
||||
"X-Shopify-Api-Features": "include-presentment-prices",
|
||||
}
|
||||
|
||||
export const IGNORE_THRESHOLD = 2
|
||||
@@ -0,0 +1,7 @@
|
||||
import Shopify from "@shopify/shopify-api"
|
||||
|
||||
export const createClient = (options) => {
|
||||
const { domain, password } = options
|
||||
|
||||
return new Shopify.Clients.Rest(`${domain}.myshopify.com`, password)
|
||||
}
|
||||
46
packages/medusa-source-shopify/src/utils/pager.js
Normal file
46
packages/medusa-source-shopify/src/utils/pager.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export async function pager(
|
||||
client,
|
||||
path,
|
||||
extraHeaders = null,
|
||||
extraQuery = {}
|
||||
) {
|
||||
let objects = []
|
||||
let nextPage = null
|
||||
let hasNext = true
|
||||
|
||||
while (hasNext) {
|
||||
const params = {
|
||||
path,
|
||||
query: { page_info: nextPage },
|
||||
}
|
||||
|
||||
if (extraHeaders) {
|
||||
Object.assign(params, { extraHeaders: extraHeaders })
|
||||
}
|
||||
|
||||
if (extraQuery) {
|
||||
Object.assign(params.query, extraQuery)
|
||||
}
|
||||
|
||||
if (!params.query.page_info) {
|
||||
delete params.query.page_info
|
||||
}
|
||||
|
||||
const response = await client.get(params)
|
||||
|
||||
objects = [...objects, ...response.body[path]]
|
||||
|
||||
const link = response.headers.get("link")
|
||||
const match =
|
||||
/(?:page_info=)(?<page_info>[a-zA-Z0-9]+)(?:>; rel="next")/.exec(link)
|
||||
|
||||
if (match?.groups) {
|
||||
nextPage = match.groups["page_info"]
|
||||
hasNext = true
|
||||
} else {
|
||||
hasNext = false
|
||||
}
|
||||
}
|
||||
|
||||
return objects
|
||||
}
|
||||
3
packages/medusa-source-shopify/src/utils/parse-price.js
Normal file
3
packages/medusa-source-shopify/src/utils/parse-price.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function parsePrice(price) {
|
||||
return parseInt(Number(price).toFixed(2) * 100)
|
||||
}
|
||||
4
packages/medusa-source-shopify/src/utils/remove-index.js
Normal file
4
packages/medusa-source-shopify/src/utils/remove-index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export function removeIndex(arr, obj) {
|
||||
const index = arr.indexOf(obj)
|
||||
arr.splice(index, 1)
|
||||
}
|
||||
5411
packages/medusa-source-shopify/yarn.lock
Normal file
5411
packages/medusa-source-shopify/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -60,12 +60,21 @@
|
||||
jsesc "^2.5.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61"
|
||||
integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==
|
||||
"@babel/generator@^7.15.4":
|
||||
version "7.15.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.8.tgz#fa56be6b596952ceb231048cf84ee499a19c0cd1"
|
||||
integrity sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
"@babel/types" "^7.15.6"
|
||||
jsesc "^2.5.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz#3d0e43b00c5e49fdb6c57e421601a7a658d5f835"
|
||||
integrity sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.14.5":
|
||||
version "7.14.5"
|
||||
@@ -77,17 +86,17 @@
|
||||
browserslist "^4.16.6"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.14.6":
|
||||
version "7.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.8.tgz#a6f8c3de208b1e5629424a9a63567f56501955fc"
|
||||
integrity sha512-bpYvH8zJBWzeqi1o+co8qOrw+EXzQ/0c74gVmY205AWXy9nifHrOg77y+1zwxX5lXE7Icq4sPlSQ4O2kWBrteQ==
|
||||
"@babel/helper-create-class-features-plugin@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz#7f977c17bd12a5fba363cb19bea090394bf37d2e"
|
||||
integrity sha512-7ZmzFi+DwJx6A7mHRwbuucEYpyBwmh2Ca0RvI6z2+WLZYCqV0JOaLb+u0zbtmDicebgKBZgqbYfLaKNqSgv5Pw==
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure" "^7.14.5"
|
||||
"@babel/helper-function-name" "^7.14.5"
|
||||
"@babel/helper-member-expression-to-functions" "^7.14.7"
|
||||
"@babel/helper-optimise-call-expression" "^7.14.5"
|
||||
"@babel/helper-replace-supers" "^7.14.5"
|
||||
"@babel/helper-split-export-declaration" "^7.14.5"
|
||||
"@babel/helper-annotate-as-pure" "^7.15.4"
|
||||
"@babel/helper-function-name" "^7.15.4"
|
||||
"@babel/helper-member-expression-to-functions" "^7.15.4"
|
||||
"@babel/helper-optimise-call-expression" "^7.15.4"
|
||||
"@babel/helper-replace-supers" "^7.15.4"
|
||||
"@babel/helper-split-export-declaration" "^7.15.4"
|
||||
|
||||
"@babel/helper-function-name@^7.14.5":
|
||||
version "7.14.5"
|
||||
@@ -98,6 +107,15 @@
|
||||
"@babel/template" "^7.14.5"
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-function-name@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc"
|
||||
integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==
|
||||
dependencies:
|
||||
"@babel/helper-get-function-arity" "^7.15.4"
|
||||
"@babel/template" "^7.15.4"
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-get-function-arity@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815"
|
||||
@@ -105,6 +123,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-get-function-arity@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b"
|
||||
integrity sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-hoist-variables@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d"
|
||||
@@ -112,13 +137,27 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-member-expression-to-functions@^7.14.5", "@babel/helper-member-expression-to-functions@^7.14.7":
|
||||
"@babel/helper-hoist-variables@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df"
|
||||
integrity sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-member-expression-to-functions@^7.14.5":
|
||||
version "7.14.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz#97e56244beb94211fe277bd818e3a329c66f7970"
|
||||
integrity sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-member-expression-to-functions@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz#bfd34dc9bba9824a4658b0317ec2fd571a51e6ef"
|
||||
integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-module-imports@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3"
|
||||
@@ -147,6 +186,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-optimise-call-expression@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171"
|
||||
integrity sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==
|
||||
dependencies:
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
|
||||
@@ -162,6 +208,16 @@
|
||||
"@babel/traverse" "^7.14.5"
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-replace-supers@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz#52a8ab26ba918c7f6dee28628b07071ac7b7347a"
|
||||
integrity sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==
|
||||
dependencies:
|
||||
"@babel/helper-member-expression-to-functions" "^7.15.4"
|
||||
"@babel/helper-optimise-call-expression" "^7.15.4"
|
||||
"@babel/traverse" "^7.15.4"
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-simple-access@^7.14.8":
|
||||
version "7.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924"
|
||||
@@ -176,11 +232,23 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-split-export-declaration@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz#aecab92dcdbef6a10aa3b62ab204b085f776e257"
|
||||
integrity sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==
|
||||
dependencies:
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.8":
|
||||
version "7.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz#32be33a756f29e278a0d644fa08a2c9e0f88a34c"
|
||||
integrity sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.14.9":
|
||||
version "7.15.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
|
||||
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
|
||||
|
||||
"@babel/helper-validator-option@^7.14.5":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
|
||||
@@ -209,6 +277,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.8.tgz#66fd41666b2d7b840bd5ace7f7416d5ac60208d4"
|
||||
integrity sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==
|
||||
|
||||
"@babel/parser@^7.15.4":
|
||||
version "7.15.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.8.tgz#7bacdcbe71bdc3ff936d510c15dcea7cf0b99016"
|
||||
integrity sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==
|
||||
|
||||
"@babel/plugin-syntax-async-generators@^7.8.4":
|
||||
version "7.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
|
||||
@@ -293,23 +366,23 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
|
||||
"@babel/plugin-transform-typescript@^7.14.5":
|
||||
version "7.14.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.6.tgz#6e9c2d98da2507ebe0a883b100cde3c7279df36c"
|
||||
integrity sha512-XlTdBq7Awr4FYIzqhmYY80WN0V0azF74DMPyFqVHBvf81ZUgc4X7ZOpx6O8eLDK6iM5cCQzeyJw0ynTaefixRA==
|
||||
"@babel/plugin-transform-typescript@^7.15.0":
|
||||
version "7.15.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.8.tgz#ff0e6a47de9b2d58652123ab5a879b2ff20665d8"
|
||||
integrity sha512-ZXIkJpbaf6/EsmjeTbiJN/yMxWPFWvlr7sEG1P95Xb4S4IBcrf2n7s/fItIhsAmOf8oSh3VJPDppO6ExfAfKRQ==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.14.6"
|
||||
"@babel/helper-create-class-features-plugin" "^7.15.4"
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
"@babel/plugin-syntax-typescript" "^7.14.5"
|
||||
|
||||
"@babel/preset-typescript@^7.13.0":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.14.5.tgz#aa98de119cf9852b79511f19e7f44a2d379bcce0"
|
||||
integrity sha512-u4zO6CdbRKbS9TypMqrlGH7sd2TAJppZwn3c/ZRLeO/wGsbddxgbPDUZVNrie3JWYLQ9vpineKlsrWFvO6Pwkw==
|
||||
"@babel/preset-typescript@^7.15.0":
|
||||
version "7.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.15.0.tgz#e8fca638a1a0f64f14e1119f7fe4500277840945"
|
||||
integrity sha512-lt0Y/8V3y06Wq/8H/u0WakrqciZ7Fz7mwPDHWUJAXlABL5hiUG42BNlRXiELNjeWjO5rWmnNKlx+yzJvxezHow==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.14.5"
|
||||
"@babel/helper-validator-option" "^7.14.5"
|
||||
"@babel/plugin-transform-typescript" "^7.14.5"
|
||||
"@babel/plugin-transform-typescript" "^7.15.0"
|
||||
|
||||
"@babel/template@^7.14.5", "@babel/template@^7.3.3":
|
||||
version "7.14.5"
|
||||
@@ -320,6 +393,15 @@
|
||||
"@babel/parser" "^7.14.5"
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/template@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194"
|
||||
integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.5"
|
||||
"@babel/parser" "^7.15.4"
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/traverse@^7.1.0", "@babel/traverse@^7.14.5", "@babel/traverse@^7.14.8":
|
||||
version "7.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.8.tgz#c0253f02677c5de1a8ff9df6b0aacbec7da1a8ce"
|
||||
@@ -335,6 +417,21 @@
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.15.4":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d"
|
||||
integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.14.5"
|
||||
"@babel/generator" "^7.15.4"
|
||||
"@babel/helper-function-name" "^7.15.4"
|
||||
"@babel/helper-hoist-variables" "^7.15.4"
|
||||
"@babel/helper-split-export-declaration" "^7.15.4"
|
||||
"@babel/parser" "^7.15.4"
|
||||
"@babel/types" "^7.15.4"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
|
||||
version "7.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.8.tgz#38109de8fcadc06415fbd9b74df0065d4d41c728"
|
||||
@@ -343,6 +440,14 @@
|
||||
"@babel/helper-validator-identifier" "^7.14.8"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.15.4", "@babel/types@^7.15.6":
|
||||
version "7.15.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f"
|
||||
integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.14.9"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
@@ -1474,6 +1579,11 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-walk@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
|
||||
integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
|
||||
|
||||
domexception@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||
@@ -1983,6 +2093,14 @@ global-dirs@^2.0.1:
|
||||
dependencies:
|
||||
ini "1.3.7"
|
||||
|
||||
global@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
|
||||
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
|
||||
dependencies:
|
||||
min-document "^2.19.0"
|
||||
process "^0.11.10"
|
||||
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
@@ -3138,6 +3256,13 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
|
||||
|
||||
min-document@^2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
|
||||
integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
|
||||
dependencies:
|
||||
dom-walk "^0.1.0"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
@@ -3536,6 +3661,11 @@ process-nextick-args@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
process@^0.11.10:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class externalIdOrder1638952072999 implements MigrationInterface {
|
||||
name = "externalIdOrder1638952072999"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "order" ADD "external_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" ADD "external_id" character varying`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "external_id"`)
|
||||
await queryRunner.query(`ALTER TABLE "order" DROP COLUMN "external_id"`)
|
||||
}
|
||||
}
|
||||
@@ -50,45 +50,45 @@ export class Address {
|
||||
id: string
|
||||
|
||||
@Index()
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
customer_id: string | null
|
||||
|
||||
@ManyToOne(() => Customer)
|
||||
@JoinColumn({ name: "customer_id" })
|
||||
customer: Customer | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
company: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
first_name: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
last_name: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
address_1: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
address_2: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
city: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
country_code: string | null
|
||||
|
||||
@ManyToOne(() => Country)
|
||||
@JoinColumn({ name: "country_code", referencedColumnName: "iso_2" })
|
||||
country: Country | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
province: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
postal_code: string | null
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
phone: string | null
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
|
||||
@@ -175,7 +175,7 @@ export class Order {
|
||||
|
||||
@OneToMany(
|
||||
() => ShippingMethod,
|
||||
method => method.order,
|
||||
(method) => method.order,
|
||||
{
|
||||
cascade: ["insert"],
|
||||
}
|
||||
@@ -184,14 +184,14 @@ export class Order {
|
||||
|
||||
@OneToMany(
|
||||
() => Payment,
|
||||
payment => payment.order,
|
||||
(payment) => payment.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
payments: Payment[]
|
||||
|
||||
@OneToMany(
|
||||
() => Fulfillment,
|
||||
fulfillment => fulfillment.order,
|
||||
(fulfillment) => fulfillment.order,
|
||||
{
|
||||
cascade: ["insert"],
|
||||
}
|
||||
@@ -200,28 +200,28 @@ export class Order {
|
||||
|
||||
@OneToMany(
|
||||
() => Return,
|
||||
ret => ret.order,
|
||||
(ret) => ret.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
returns: Return[]
|
||||
|
||||
@OneToMany(
|
||||
() => ClaimOrder,
|
||||
co => co.order,
|
||||
(co) => co.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
claims: ClaimOrder[]
|
||||
|
||||
@OneToMany(
|
||||
() => Refund,
|
||||
ref => ref.order,
|
||||
(ref) => ref.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
refunds: Refund[]
|
||||
|
||||
@OneToMany(
|
||||
() => Swap,
|
||||
swap => swap.order,
|
||||
(swap) => swap.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
swaps: Swap[]
|
||||
@@ -235,7 +235,7 @@ export class Order {
|
||||
|
||||
@OneToMany(
|
||||
() => LineItem,
|
||||
lineItem => lineItem.order,
|
||||
(lineItem) => lineItem.order,
|
||||
{
|
||||
cascade: ["insert"],
|
||||
}
|
||||
@@ -244,7 +244,7 @@ export class Order {
|
||||
|
||||
@OneToMany(
|
||||
() => GiftCardTransaction,
|
||||
gc => gc.order
|
||||
(gc) => gc.order
|
||||
)
|
||||
gift_card_transactions: GiftCardTransaction[]
|
||||
|
||||
@@ -266,6 +266,9 @@ export class Order {
|
||||
@Column({ nullable: true })
|
||||
idempotency_key: string
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
external_id: string | null
|
||||
|
||||
// Total fields
|
||||
shipping_total: number
|
||||
discount_total: number
|
||||
|
||||
@@ -150,6 +150,9 @@ export class Product {
|
||||
@Column({ default: true })
|
||||
discountable: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
external_id: string
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
|
||||
@@ -393,6 +393,44 @@ class OrderService extends BaseService {
|
||||
return order
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an order by id.
|
||||
* @param {string} externalId - id of order to retrieve
|
||||
* @param {object} config - query config to get order by
|
||||
* @return {Promise<Order>} the order document
|
||||
*/
|
||||
async retrieveByExternalId(externalId, config = {}) {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(config)
|
||||
|
||||
const query = {
|
||||
where: { external_id: externalId },
|
||||
}
|
||||
|
||||
if (relations && relations.length > 0) {
|
||||
query.relations = relations
|
||||
}
|
||||
|
||||
if (select && select.length > 0) {
|
||||
query.select = select
|
||||
}
|
||||
|
||||
const rels = query.relations
|
||||
delete query.relations
|
||||
const raw = await orderRepo.findOneWithRelations(rels, query)
|
||||
if (!raw) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Order with external id ${externalId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
const order = this.decorateTotals_(raw, totalsToSelect)
|
||||
return order
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the existence of an order by cart id.
|
||||
* @param {string} cartId - cart id to find order
|
||||
|
||||
@@ -70,6 +70,30 @@ class ProductCollectionService extends BaseService {
|
||||
return collection
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a product collection by id.
|
||||
* @param {string} collectionHandle - the handle of the collection to retrieve.
|
||||
* @param {object} config - query config for request
|
||||
* @return {Promise<ProductCollection>} the collection.
|
||||
*/
|
||||
async retrieveByHandle(collectionHandle, config = {}) {
|
||||
const collectionRepo = this.manager_.getCustomRepository(
|
||||
this.productCollectionRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_({ handle: collectionHandle }, config)
|
||||
const collection = await collectionRepo.findOne(query)
|
||||
|
||||
if (!collection) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product collection with handle: ${collectionHandle} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a product collection
|
||||
* @param {object} collection - the collection to create
|
||||
|
||||
@@ -614,6 +614,7 @@ class ProductVariantService extends BaseService {
|
||||
.emit(ProductVariantService.Events.DELETED, {
|
||||
id: variant.id,
|
||||
product_id: variant.product_id,
|
||||
metadata: variant.metadata,
|
||||
})
|
||||
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -201,6 +201,78 @@ class ProductService extends BaseService {
|
||||
return product
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a product by handle.
|
||||
* Throws in case of DB Error and if product was not found.
|
||||
* @param {string} productHandle - handle of the product to get.
|
||||
* @param {object} config - details about what to get from the product
|
||||
* @return {Promise<Product>} the result of the find one operation.
|
||||
*/
|
||||
async retrieveByHandle(productHandle, config = {}) {
|
||||
const productRepo = this.manager_.getCustomRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
|
||||
const query = { where: { handle: productHandle } }
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
query.relations = config.relations
|
||||
}
|
||||
|
||||
if (config.select && config.select.length > 0) {
|
||||
query.select = config.select
|
||||
}
|
||||
|
||||
const rels = query.relations
|
||||
delete query.relations
|
||||
const product = await productRepo.findOneWithRelations(rels, query)
|
||||
|
||||
if (!product) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with handle: ${productHandle} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a product by external id.
|
||||
* Throws in case of DB Error and if product was not found.
|
||||
* @param {string} externalId - handle of the product to get.
|
||||
* @param {object} config - details about what to get from the product
|
||||
* @return {Promise<Product>} the result of the find one operation.
|
||||
*/
|
||||
async retrieveByExternalId(externalId, config = {}) {
|
||||
const productRepo = this.manager_.getCustomRepository(
|
||||
this.productRepository_
|
||||
)
|
||||
|
||||
const query = { where: { external_id: externalId } }
|
||||
|
||||
if (config.relations && config.relations.length > 0) {
|
||||
query.relations = config.relations
|
||||
}
|
||||
|
||||
if (config.select && config.select.length > 0) {
|
||||
query.select = config.select
|
||||
}
|
||||
|
||||
const rels = query.relations
|
||||
delete query.relations
|
||||
const product = await productRepo.findOneWithRelations(rels, query)
|
||||
|
||||
if (!product) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with exteral_id: ${externalId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return product
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all variants belonging to a product.
|
||||
* @param {string} productId - the id of the product to get variants from.
|
||||
|
||||
@@ -336,6 +336,34 @@ class RegionService extends BaseService {
|
||||
return country
|
||||
}
|
||||
|
||||
async retrieveByCountryCode(code, config = {}) {
|
||||
const countryRepository = this.manager_.getCustomRepository(
|
||||
this.countryRepository_
|
||||
)
|
||||
|
||||
const country = await countryRepository.findOne({
|
||||
where: {
|
||||
iso_2: code.toLowerCase(),
|
||||
},
|
||||
})
|
||||
|
||||
if (!country) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Country with code ${code} not found`
|
||||
)
|
||||
}
|
||||
|
||||
if (!country.region_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Country does not belong to a region`
|
||||
)
|
||||
}
|
||||
|
||||
return await this.retrieve(country.region_id, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a region by its id.
|
||||
* @param {string} regionId - the id of the region to retrieve
|
||||
|
||||
@@ -5387,20 +5387,20 @@ medusa-core-utils@^0.1.27:
|
||||
"@hapi/joi" "^16.1.8"
|
||||
joi-objectid "^3.0.1"
|
||||
|
||||
medusa-core-utils@^1.1.29:
|
||||
version "1.1.29"
|
||||
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.29.tgz#3ad1c21aae18d627063a6548504e24ce70031015"
|
||||
integrity sha512-knlHwetXkyYjkGNU/a6JVT9IkKtYa5UD45I5C0jAs9BqzacKN/WCxeKzOwCJPBMP1LrSBajwm45kgvICUerWzA==
|
||||
medusa-core-utils@^1.1.30:
|
||||
version "1.1.30"
|
||||
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.30.tgz#6fe670a9df1ddbbd786f7a3a89c167eb3596aea2"
|
||||
integrity sha512-vWtSr2uZzRRv3HfUc9rclLxmMx+7lL+5qFZrgSZK5Wf19jO4Jp15wWgNsf3bp7Bf5ICaPfT96G1jbyqiMvI6HA==
|
||||
dependencies:
|
||||
joi "^17.3.0"
|
||||
joi-objectid "^3.0.1"
|
||||
|
||||
medusa-interfaces@^1.1.30:
|
||||
version "1.1.30"
|
||||
resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.30.tgz#9a6fa19a88854db67b0c82cef9061afda560bd3a"
|
||||
integrity sha512-OQVPO/6Gr5OJOWjSCaDQcTZDMCo7cJoQP4w5P/+XoTM6YvD7PpJhVJR17W7du5XIXoiM5i/eFDi3/Gb7nsZdJg==
|
||||
medusa-interfaces@^1.1.31:
|
||||
version "1.1.31"
|
||||
resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.31.tgz#50180132fa6b785c595ebba0e9c4c80cb7eba948"
|
||||
integrity sha512-85IEL2K7E1hBB9L2VrLok1KiYOdnoQxY3cGm5VcjYzkxJSxb3GjDcbptRIaq199w/j4ZfAyDXusXzLrfaZHcLw==
|
||||
dependencies:
|
||||
medusa-core-utils "^1.1.29"
|
||||
medusa-core-utils "^1.1.30"
|
||||
|
||||
medusa-telemetry@^0.0.10:
|
||||
version "0.0.10"
|
||||
@@ -5417,13 +5417,13 @@ medusa-telemetry@^0.0.10:
|
||||
remove-trailing-slash "^0.1.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
medusa-test-utils@^1.1.32:
|
||||
version "1.1.32"
|
||||
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.32.tgz#c0797b517d9d4475483147514a383b2c4ed88b8c"
|
||||
integrity sha512-RBme+gBI7pmRiFcrxvU4dO4a509zLINrRZwW4p1UiQwg2JCfzTt9iMwhV9Mn6Z+SDAP2n1M2TFBYmNYuXwoJGQ==
|
||||
medusa-test-utils@^1.1.33:
|
||||
version "1.1.33"
|
||||
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.33.tgz#0c2f159f0cd5f3d3e79f5aabb30286a9bf839ef9"
|
||||
integrity sha512-VM29+5h4NM3isIKh1AqssCbvj+aC/Meqqf+c3PNdhtK93+jNmahadpYtGPhT5i/8lMOqnleatAgfnhjq8a7X9A==
|
||||
dependencies:
|
||||
"@babel/plugin-transform-classes" "^7.9.5"
|
||||
medusa-core-utils "^1.1.29"
|
||||
medusa-core-utils "^1.1.30"
|
||||
randomatic "^3.1.1"
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
@@ -7062,11 +7062,16 @@ side-channel@^1.0.4:
|
||||
get-intrinsic "^1.0.2"
|
||||
object-inspect "^1.9.0"
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af"
|
||||
integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==
|
||||
|
||||
signal-exit@^3.0.3:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
|
||||
integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
|
||||
Reference in New Issue
Block a user