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:
co-authored by
Sebastian Rindom
parent
f2ba4018fc
commit
577bcc23d4
@@ -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
|
||||
|
||||
+20
-15
@@ -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