diff --git a/.changeset/bright-moose-draw.md b/.changeset/bright-moose-draw.md new file mode 100644 index 0000000000..2f83a50a00 --- /dev/null +++ b/.changeset/bright-moose-draw.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Order products on retrieval \ No newline at end of file diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap index 4f26a78ef8..44b21dd816 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap @@ -1,598 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`/admin/products GET /admin/products returns a list of products with child entities 1`] = ` -Array [ - Object { - "collection": Object { - "created_at": Any, - "deleted_at": null, - "handle": "test-collection", - "id": StringMatching /\\^test-\\*/, - "metadata": null, - "title": "Test collection", - "updated_at": Any, - }, - "collection_id": "test-collection", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product", - "height": null, - "hs_code": null, - "id": "test-product", - "images": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-\\*/, - "metadata": null, - "updated_at": Any, - "url": "test-image.png", - }, - ], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-\\*/, - "metadata": null, - "product_id": StringMatching /\\^test-\\*/, - "title": "test-option", - "updated_at": Any, - }, - ], - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "draft", - "subtitle": null, - "tags": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^tag\\*/, - "metadata": null, - "updated_at": Any, - "value": "123", - }, - ], - "thumbnail": null, - "title": "Test product", - "type": Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-\\*/, - "metadata": null, - "updated_at": Any, - "value": "test-type", - }, - "type_id": "test-type", - "updated_at": Any, - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": "test-barcode", - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean", - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-variant-option\\*/, - "metadata": null, - "option_id": StringMatching /\\^test-opt\\*/, - "updated_at": Any, - "value": "Default variant", - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price", - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "product_id": StringMatching /\\^test-\\*/, - "sku": "test-sku", - "tax_rates": null, - "title": "Test variant", - "upc": "test-upc", - "updated_at": Any, - "weight": null, - "width": null, - }, - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean2", - "height": null, - "hs_code": null, - "id": "test-variant_2", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-variant-option\\*/, - "metadata": null, - "option_id": StringMatching /\\^test-opt\\*/, - "updated_at": Any, - "value": "Default variant 2", - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": StringMatching /\\^test-price\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant_2", - }, - ], - "product_id": StringMatching /\\^test-\\*/, - "sku": "test-sku2", - "tax_rates": null, - "title": "Test variant rank (2)", - "upc": "test-upc2", - "updated_at": Any, - "weight": null, - "width": null, - }, - Object { - "allow_backorder": false, - "barcode": "test-barcode 1", - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean1", - "height": null, - "hs_code": null, - "id": "test-variant_1", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-variant-option\\*/, - "metadata": null, - "option_id": StringMatching /\\^test-opt\\*/, - "updated_at": Any, - "value": "Default variant 1", - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": StringMatching /\\^test-price\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "product_id": StringMatching /\\^test-\\*/, - "sku": "test-sku1", - "tax_rates": null, - "title": "Test variant rank (1)", - "upc": "test-upc1", - "updated_at": Any, - "weight": null, - "width": null, - }, - Object { - "allow_backorder": false, - "barcode": "test-barcode-sale", - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean-sale", - "height": null, - "hs_code": null, - "id": "test-variant-sale", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-variant-option\\*/, - "metadata": null, - "option_id": StringMatching /\\^test-opt\\*/, - "updated_at": Any, - "value": "Default variant", - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 1000, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price-sale", - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "product_id": StringMatching /\\^test-\\*/, - "sku": "test-sku-sale", - "tax_rates": null, - "title": "Test variant", - "upc": "test-upc-sale", - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - "weight": null, - "width": null, - }, - Object { - "collection": Object { - "created_at": Any, - "deleted_at": null, - "handle": "test-collection", - "id": StringMatching /\\^test-\\*/, - "metadata": null, - "title": "Test collection", - "updated_at": Any, - }, - "collection_id": "test-collection", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description1", - "discountable": true, - "external_id": null, - "handle": "test-product1", - "height": null, - "hs_code": null, - "id": "test-product1", - "images": Array [], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [], - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "draft", - "subtitle": null, - "tags": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^tag\\*/, - "metadata": null, - "updated_at": Any, - "value": "123", - }, - ], - "thumbnail": null, - "title": "Test product1", - "type": Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-\\*/, - "metadata": null, - "updated_at": Any, - "value": "test-type", - }, - "type_id": "test-type", - "updated_at": Any, - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean4", - "height": null, - "hs_code": null, - "id": "test-variant_4", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-variant-option\\*/, - "metadata": null, - "option_id": StringMatching /\\^test-opt\\*/, - "updated_at": Any, - "value": "Default variant 4", - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": StringMatching /\\^test-price\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "product_id": StringMatching /\\^test-\\*/, - "sku": "test-sku4", - "tax_rates": null, - "title": "Test variant rank (2)", - "upc": "test-upc4", - "updated_at": Any, - "weight": null, - "width": null, - }, - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean3", - "height": null, - "hs_code": null, - "id": "test-variant_3", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^test-variant-option\\*/, - "metadata": null, - "option_id": StringMatching /\\^test-opt\\*/, - "updated_at": Any, - "value": "Default variant 3", - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": StringMatching /\\^test-price\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": "test-region", - "updated_at": Any, - "variant_id": StringMatching /\\^test-variant\\*/, - }, - ], - "product_id": StringMatching /\\^test-\\*/, - "sku": "test-sku3", - "tax_rates": null, - "title": "Test variant rank (2)", - "upc": "test-upc3", - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - "weight": null, - "width": null, - }, - Object { - "collection": Any, - "collection_id": "test-collection1", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product_filtering_1", - "height": null, - "hs_code": null, - "id": "test-product_filtering_1", - "images": Array [], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Any, - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "proposed", - "subtitle": null, - "tags": Any, - "thumbnail": null, - "title": "Test product filtering 1", - "type": Any, - "type_id": "test-type", - "updated_at": Any, - "variants": Any, - "weight": null, - "width": null, - }, - Object { - "collection": Any, - "collection_id": "test-collection2", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product_filtering_2", - "height": null, - "hs_code": null, - "id": "test-product_filtering_2", - "images": Array [], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Any, - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "published", - "subtitle": null, - "tags": Any, - "thumbnail": null, - "title": "Test product filtering 2", - "type": Any, - "type_id": "test-type", - "updated_at": Any, - "variants": Any, - "weight": null, - "width": null, - }, - Object { - "collection": Any, - "collection_id": "test-collection1", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product_filtering_3", - "height": null, - "hs_code": null, - "id": "test-product_filtering_3", - "images": Array [], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Any, - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "draft", - "subtitle": null, - "tags": Any, - "thumbnail": null, - "title": "Test product filtering 3", - "type": Any, - "type_id": "test-type", - "updated_at": Any, - "variants": Any, - "weight": null, - "width": null, - }, -] -`; - exports[`/admin/products GET /admin/products returns a list of products with only giftcard in list 1`] = ` Array [ Object { diff --git a/integration-tests/api/__tests__/admin/price-list.js b/integration-tests/api/__tests__/admin/price-list.js index b4647575bd..d0bdc1aa1c 100644 --- a/integration-tests/api/__tests__/admin/price-list.js +++ b/integration-tests/api/__tests__/admin/price-list.js @@ -88,11 +88,7 @@ describe("/admin/price-lists", () => { } const response = await api - .post("/admin/price-lists", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post("/admin/price-lists", payload, adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -147,11 +143,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists/pl_no_customer_groups", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists/pl_no_customer_groups", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -209,11 +201,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -232,11 +220,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists?q=winter", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists?q=winter", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -256,11 +240,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists?q=25%", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists?q=25%", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -282,11 +262,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists?q=blablabla", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists?q=blablabla", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -300,11 +276,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists?q=vip&status[]=draft", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists?q=vip&status[]=draft", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -318,11 +290,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get("/admin/price-lists?q=vip&status[]=active", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists?q=vip&status[]=active", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -363,11 +331,7 @@ describe("/admin/price-lists", () => { const response = await api .get( `/admin/price-lists?customer_groups[]=customer-group-1,customer-group-2`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { console.warn(err.response.data) @@ -406,11 +370,10 @@ describe("/admin/price-lists", () => { }) const api = useApi() - const getResult = await api.get(`/admin/price-lists/${priceList.id}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + const getResult = await api.get( + `/admin/price-lists/${priceList.id}`, + adminReqConfig + ) expect(getResult.status).toEqual(200) expect(getResult.data.price_list.starts_at).toBeTruthy() @@ -420,11 +383,7 @@ describe("/admin/price-lists", () => { const updateResult = await api.post( `/admin/price-lists/${priceList.id}`, { ends_at: null, starts_at: null, customer_groups: [] }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(updateResult.status).toEqual(200) @@ -463,11 +422,11 @@ describe("/admin/price-lists", () => { } const response = await api - .post("/admin/price-lists/pl_no_customer_groups", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post( + "/admin/price-lists/pl_no_customer_groups", + payload, + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) @@ -575,11 +534,11 @@ describe("/admin/price-lists", () => { } const response = await api - .post("/admin/price-lists/pl_no_customer_groups", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post( + "/admin/price-lists/pl_no_customer_groups", + payload, + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) @@ -626,11 +585,7 @@ describe("/admin/price-lists", () => { } const response = await api - .post("/admin/price-lists/pl_with_some_ma", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post("/admin/price-lists/pl_with_some_ma", payload, adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -714,11 +669,7 @@ describe("/admin/price-lists", () => { .post( "/admin/price-lists/pl_no_customer_groups/prices/batch", payload, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { console.warn(err.response.data) @@ -832,11 +783,7 @@ describe("/admin/price-lists", () => { .post( "/admin/price-lists/pl_no_customer_groups/prices/batch", payload, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { console.warn(err.response.data) @@ -900,11 +847,11 @@ describe("/admin/price-lists", () => { } const response = await api - .post("/admin/price-lists/pl_with_some_ma/prices/batch", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post( + "/admin/price-lists/pl_with_some_ma/prices/batch", + payload, + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) @@ -970,11 +917,7 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .delete("/admin/price-lists/pl_no_customer_groups", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .delete("/admin/price-lists/pl_no_customer_groups", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -987,11 +930,10 @@ describe("/admin/price-lists", () => { }) try { - await api.get("/admin/price-lists/pl_no_customer_groups", { - headers: { - Authorization: "Bearer test_token", - }, - }) + await api.get( + "/admin/price-lists/pl_no_customer_groups", + adminReqConfig + ) } catch (error) { expect(error.response.status).toBe(404) expect(error.response.data.message).toEqual( @@ -1017,22 +959,17 @@ describe("/admin/price-lists", () => { const api = useApi() await api - .delete("/admin/products/test-product/variants/test-variant", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .delete( + "/admin/products/test-product/variants/test-variant", + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) const response = await api.get( "/admin/price-lists/pl_no_customer_groups", - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(response.status).toEqual(200) @@ -1057,9 +994,7 @@ describe("/admin/price-lists", () => { const response = await api .delete("/admin/price-lists/pl_no_customer_groups/prices/batch", { - headers: { - Authorization: "Bearer test_token", - }, + ...adminReqConfig, data: { price_ids: ["ma_test_1", "ma_test_2"], }, @@ -1069,11 +1004,7 @@ describe("/admin/price-lists", () => { }) const getPriceListResponse = await api - .get("/admin/price-lists/pl_no_customer_groups", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/price-lists/pl_no_customer_groups", adminReqConfig) .catch((err) => { console.warn(err.response.data) }) @@ -1166,11 +1097,10 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get(`/admin/price-lists/test-list/products?order=-created_at`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get( + `/admin/price-lists/test-list/products?order=-created_at`, + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) @@ -1179,28 +1109,6 @@ describe("/admin/price-lists", () => { expect(response.data.count).toEqual(2) expect(response.data.products).toHaveLength(2) expect(response.data.products).toEqual([ - expect.objectContaining({ - id: "test-prod-1", - variants: expect.arrayContaining([ - expect.objectContaining({ - id: "test-variant-1", - prices: expect.arrayContaining([ - expect.objectContaining({ currency_code: "usd", amount: 100 }), - expect.objectContaining({ - currency_code: "usd", - amount: 150, - price_list_id: "test-list", - }), - ]), - }), - expect.objectContaining({ - id: "test-variant-2", - prices: expect.arrayContaining([ - expect.objectContaining({ currency_code: "usd", amount: 100 }), - ]), - }), - ]), - }), expect.objectContaining({ id: "test-prod-2", variants: expect.arrayContaining([ @@ -1223,6 +1131,28 @@ describe("/admin/price-lists", () => { }), ]), }), + expect.objectContaining({ + id: "test-prod-1", + variants: expect.arrayContaining([ + expect.objectContaining({ + id: "test-variant-1", + prices: expect.arrayContaining([ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + expect.objectContaining({ + currency_code: "usd", + amount: 150, + price_list_id: "test-list", + }), + ]), + }), + expect.objectContaining({ + id: "test-variant-2", + prices: expect.arrayContaining([ + expect.objectContaining({ currency_code: "usd", amount: 100 }), + ]), + }), + ]), + }), ]) }) @@ -1230,11 +1160,10 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get(`/admin/price-lists/test-list/products?tags[]=${tag}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get( + `/admin/price-lists/test-list/products?tags[]=${tag}`, + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) @@ -1251,11 +1180,10 @@ describe("/admin/price-lists", () => { const api = useApi() const response = await api - .get(`/admin/price-lists/test-list/products?q=Headphones`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get( + `/admin/price-lists/test-list/products?q=Headphones`, + adminReqConfig + ) .catch((err) => { console.warn(err.response.data) }) @@ -1343,22 +1271,14 @@ describe("/admin/price-lists", () => { it("should delete all the prices that are part of the price list for the specified product", async () => { const api = useApi() - response = await api.get("/admin/price-lists/test-list", { - headers: { - Authorization: "Bearer test_token", - }, - }) + response = await api.get("/admin/price-lists/test-list", adminReqConfig) expect(response.status).toBe(200) expect(response.data.price_list.prices.length).toBe(3) let response = await api.delete( `/admin/price-lists/test-list/products/${product1.id}/prices`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(response.status).toBe(200) @@ -1370,11 +1290,7 @@ describe("/admin/price-lists", () => { deleted: true, }) - response = await api.get("/admin/price-lists/test-list", { - headers: { - Authorization: "Bearer test_token", - }, - }) + response = await api.get("/admin/price-lists/test-list", adminReqConfig) expect(response.status).toBe(200) expect(response.data.price_list.prices.length).toBe(1) @@ -1383,11 +1299,7 @@ describe("/admin/price-lists", () => { it("should delete all the prices that are part of the price list for the specified variant", async () => { const api = useApi() - response = await api.get("/admin/price-lists/test-list", { - headers: { - Authorization: "Bearer test_token", - }, - }) + response = await api.get("/admin/price-lists/test-list", adminReqConfig) expect(response.status).toBe(200) expect(response.data.price_list.prices.length).toBe(3) @@ -1395,11 +1307,7 @@ describe("/admin/price-lists", () => { const variant = product2.variants[0] let response = await api.delete( `/admin/price-lists/test-list/variants/${variant.id}/prices`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(response.status).toBe(200) @@ -1409,11 +1317,7 @@ describe("/admin/price-lists", () => { deleted: true, }) - response = await api.get("/admin/price-lists/test-list", { - headers: { - Authorization: "Bearer test_token", - }, - }) + response = await api.get("/admin/price-lists/test-list", adminReqConfig) expect(response.status).toBe(200) expect(response.data.price_list.prices.length).toBe(2) diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 173035994f..cf19a30054 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -647,254 +647,253 @@ describe("/admin/products", () => { const api = useApi() const response = await api - .get("/admin/products", adminHeaders) + .get("/admin/products?order=created_at", adminHeaders) .catch((err) => { console.log(err) }) - response.data.products.sort((a, b) => - a.created_at > b.created_at ? 1 : -1 + expect(response.data.products).toHaveLength(5) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "test-product", + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-*/), + product_id: expect.stringMatching(/^test-*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + images: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: "test-variant", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: "test-price", + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }), + expect.objectContaining({ + id: "test-variant_2", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: "test-variant_2", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }), + expect.objectContaining({ + id: "test-variant_1", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }), + expect.objectContaining({ + id: "test-variant-sale", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: "test-price-sale", + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }), + ]), + tags: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^tag*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + type: expect.objectContaining({ + id: expect.stringMatching(/^test-*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + collection: expect.objectContaining({ + id: expect.stringMatching(/^test-*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-product1", + created_at: expect.any(String), + options: [], + variants: expect.arrayContaining([ + expect.objectContaining({ + id: "test-variant_4", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }), + expect.objectContaining({ + id: "test-variant_3", + created_at: expect.any(String), + updated_at: expect.any(String), + product_id: expect.stringMatching(/^test-*/), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-price*/), + variant_id: expect.stringMatching(/^test-variant*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^test-variant-option*/), + variant_id: expect.stringMatching(/^test-variant*/), + option_id: expect.stringMatching(/^test-opt*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }), + ]), + tags: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^tag*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + type: expect.objectContaining({ + id: expect.stringMatching(/^test-*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + collection: expect.objectContaining({ + id: expect.stringMatching(/^test-*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + profile_id: expect.stringMatching(/^sp_*/), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-product_filtering_1", + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + type: expect.any(Object), + collection: expect.any(Object), + options: expect.any(Array), + tags: expect.any(Array), + variants: expect.any(Array), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-product_filtering_2", + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + type: expect.any(Object), + collection: expect.any(Object), + options: expect.any(Array), + tags: expect.any(Array), + variants: expect.any(Array), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-product_filtering_3", + profile_id: expect.stringMatching(/^sp_*/), + created_at: expect.any(String), + type: expect.any(Object), + collection: expect.any(Object), + options: expect.any(Array), + tags: expect.any(Array), + variants: expect.any(Array), + updated_at: expect.any(String), + }), + ]) ) - - expect(response.data.products).toMatchSnapshot([ - { - id: "test-product", - options: [ - { - id: expect.stringMatching(/^test-*/), - product_id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - images: [ - { - id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - variants: [ - { - id: "test-variant", - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: "test-price", - variant_id: expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - { - id: "test-variant_2", - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: expect.stringMatching(/^test-price*/), - variant_id: "test-variant_2", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - { - id: "test-variant_1", - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: expect.stringMatching(/^test-price*/), - variant_id: expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - { - id: "test-variant-sale", - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: "test-price-sale", - variant_id: expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - ], - tags: [ - { - id: expect.stringMatching(/^tag*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - type: { - id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - collection: { - id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - profile_id: expect.stringMatching(/^sp_*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: "test-product1", - created_at: expect.any(String), - options: [], - variants: [ - { - id: "test-variant_4", - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: expect.stringMatching(/^test-price*/), - variant_id: expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - { - id: "test-variant_3", - created_at: expect.any(String), - updated_at: expect.any(String), - product_id: expect.stringMatching(/^test-*/), - prices: [ - { - id: expect.stringMatching(/^test-price*/), - variant_id: expect.stringMatching(/^test-variant*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^test-variant-option*/), - variant_id: expect.stringMatching(/^test-variant*/), - option_id: expect.stringMatching(/^test-opt*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - }, - ], - tags: [ - { - id: expect.stringMatching(/^tag*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - type: { - id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - collection: { - id: expect.stringMatching(/^test-*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - profile_id: expect.stringMatching(/^sp_*/), - updated_at: expect.any(String), - }, - { - id: "test-product_filtering_1", - profile_id: expect.stringMatching(/^sp_*/), - created_at: expect.any(String), - type: expect.any(Object), - collection: expect.any(Object), - options: expect.any(Array), - tags: expect.any(Array), - variants: expect.any(Array), - updated_at: expect.any(String), - }, - { - id: "test-product_filtering_2", - profile_id: expect.stringMatching(/^sp_*/), - created_at: expect.any(String), - type: expect.any(Object), - collection: expect.any(Object), - options: expect.any(Array), - tags: expect.any(Array), - variants: expect.any(Array), - updated_at: expect.any(String), - }, - { - id: "test-product_filtering_3", - profile_id: expect.stringMatching(/^sp_*/), - created_at: expect.any(String), - type: expect.any(Object), - collection: expect.any(Object), - options: expect.any(Array), - tags: expect.any(Array), - variants: expect.any(Array), - updated_at: expect.any(String), - }, - ]) }) }) @@ -1800,11 +1799,11 @@ describe("/admin/products", () => { expect(response.status).toEqual(200) - expect(response.data.product.variants[1].prices.length).toEqual( + expect(response.data.product.variants[0].prices.length).toEqual( data.prices.length ) - expect(response.data.product.variants[1].prices).toEqual( + expect(response.data.product.variants[0].prices).toEqual( expect.arrayContaining([ expect.objectContaining({ amount: 8000, diff --git a/integration-tests/api/__tests__/admin/publishable-api-key.js b/integration-tests/api/__tests__/admin/publishable-api-key.js index 6ebf0eed62..73c4ee360a 100644 --- a/integration-tests/api/__tests__/admin/publishable-api-key.js +++ b/integration-tests/api/__tests__/admin/publishable-api-key.js @@ -647,10 +647,10 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: product1.id, + id: product2.id, }), expect.objectContaining({ - id: product2.id, + id: product1.id, }), ]) ) diff --git a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap b/integration-tests/api/__tests__/store/__snapshots__/products.js.snap deleted file mode 100644 index d5177b53c5..0000000000 --- a/integration-tests/api/__tests__/store/__snapshots__/products.js.snap +++ /dev/null @@ -1,450 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`/store/products /store/products/:id includes default relations 1`] = ` -Object { - "product": Object { - "collection": Object { - "created_at": Any, - "deleted_at": null, - "handle": "test-collection", - "id": "test-collection", - "metadata": null, - "title": "Test collection", - "updated_at": Any, - }, - "collection_id": "test-collection", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product", - "height": null, - "hs_code": null, - "id": "test-product", - "images": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-image", - "metadata": null, - "updated_at": Any, - "url": "test-image.png", - }, - ], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-option", - "metadata": null, - "product_id": "test-product", - "title": "test-option", - "updated_at": Any, - "values": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant", - "variant_id": "test-variant", - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option-1", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant 1", - "variant_id": "test-variant_1", - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option-2", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant 2", - "variant_id": "test-variant_2", - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option-3", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant 3", - "variant_id": "test-variant_3", - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option-4", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant 4", - "variant_id": "test-variant_4", - }, - ], - }, - ], - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "status": "published", - "subtitle": null, - "tags": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "tag1", - "metadata": null, - "updated_at": Any, - "value": "123", - }, - ], - "thumbnail": null, - "title": "Test product", - "type": Object { - "created_at": Any, - "deleted_at": null, - "id": "test-type", - "metadata": null, - "updated_at": Any, - "value": "test-type", - }, - "type_id": "test-type", - "updated_at": Any, - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": "test-barcode", - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean", - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant", - "variant_id": "test-variant", - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price", - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant", - }, - Object { - "amount": 80, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price-discount", - "max_quantity": null, - "min_quantity": null, - "price_list": Object { - "created_at": Any, - "deleted_at": null, - "description": "Winter sale for VIP customers.", - "ends_at": null, - "id": "pl", - "name": "VIP winter sale", - "starts_at": null, - "status": "active", - "type": "sale", - "updated_at": Any, - }, - "price_list_id": "pl", - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant", - }, - ], - "product_id": "test-product", - "sku": "test-sku", - "tax_rates": null, - "title": "Test variant", - "upc": "test-upc", - "updated_at": Any, - "weight": null, - "width": null, - }, - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean2", - "height": null, - "hs_code": null, - "id": "test-variant_2", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option-2", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant 2", - "variant_id": "test-variant_2", - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price2", - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant_2", - }, - Object { - "amount": 80, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price2-discount", - "max_quantity": null, - "min_quantity": null, - "price_list": Object { - "created_at": Any, - "deleted_at": null, - "description": "Winter sale for VIP customers.", - "ends_at": null, - "id": "pl", - "name": "VIP winter sale", - "starts_at": null, - "status": "active", - "type": "sale", - "updated_at": Any, - }, - "price_list_id": "pl", - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant_2", - }, - ], - "product_id": "test-product", - "sku": "test-sku2", - "tax_rates": null, - "title": "Test variant rank (2)", - "upc": "test-upc2", - "updated_at": Any, - "weight": null, - "width": null, - }, - Object { - "allow_backorder": false, - "barcode": "test-barcode 1", - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean1", - "height": null, - "hs_code": null, - "id": "test-variant_1", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option-1", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant 1", - "variant_id": "test-variant_1", - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price1", - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant_1", - }, - Object { - "amount": 80, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price1-discount", - "max_quantity": null, - "min_quantity": null, - "price_list": Object { - "created_at": Any, - "deleted_at": null, - "description": "Winter sale for VIP customers.", - "ends_at": null, - "id": "pl", - "name": "VIP winter sale", - "starts_at": null, - "status": "active", - "type": "sale", - "updated_at": Any, - }, - "price_list_id": "pl", - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant_1", - }, - ], - "product_id": "test-product", - "sku": "test-sku1", - "tax_rates": null, - "title": "Test variant rank (1)", - "upc": "test-upc1", - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - "weight": null, - "width": null, - }, -} -`; - -exports[`/store/products list params works with expand and fields 1`] = ` -Object { - "count": 2, - "limit": 1, - "offset": 0, - "products": Array [ - Object { - "id": Any, - "title": "testprod", - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": Any, - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": Any, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": Any, - }, - ], - "product_id": Any, - "sku": null, - "tax_rates": null, - "title": "test-variant", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - }, - ], -} -`; diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index f7b60a4eb8..e07083c20f 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -6,12 +6,19 @@ const { initDb, useDb } = require("../../../helpers/use-db") const { simpleProductFactory } = require("../../factories") const productSeeder = require("../../helpers/store-product-seeder") const adminSeeder = require("../../helpers/admin-seeder") + jest.setTimeout(30000) describe("/store/products", () => { let medusaProcess let dbConnection + const giftCardId = "giftcard" + const testProductId = "test-product" + const testProductId1 = "test-product1" + const testProductFilteringId1 = "test-product_filtering_1" + const testProductFilteringId2 = "test-product_filtering_2" + beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) @@ -35,6 +42,190 @@ describe("/store/products", () => { await db.teardown() }) + it("returns a list of ordered products by id ASC", async () => { + const api = useApi() + + const response = await api.get("/store/products?order=id") + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(5) + expect(response.data.products[0].id).toEqual(giftCardId) + expect(response.data.products[1].id).toEqual(testProductId) + expect(response.data.products[2].id).toEqual(testProductId1) + expect(response.data.products[3].id).toEqual(testProductFilteringId1) + expect(response.data.products[4].id).toEqual(testProductFilteringId2) + }) + + it("returns a list of ordered products by id DESC", async () => { + const api = useApi() + + const response = await api.get("/store/products?order=-id") + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(5) + expect(response.data.products[0].id).toEqual(testProductFilteringId2) + expect(response.data.products[1].id).toEqual(testProductFilteringId1) + expect(response.data.products[2].id).toEqual(testProductId1) + expect(response.data.products[3].id).toEqual(testProductId) + expect(response.data.products[4].id).toEqual(giftCardId) + }) + + it("returns a list of ordered products by variants title DESC", async () => { + const api = useApi() + + const response = await api.get("/store/products?order=-variants.title") + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(5) + + const testProductIndex = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId) + ) + const testProduct1Index = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId1) + ) + + expect(testProductIndex).toBe(3) + expect(testProduct1Index).toBe(4) + }) + + it("returns a list of ordered products by variants title ASC", async () => { + const api = useApi() + + const response = await api.get("/store/products?order=variants.title") + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(5) + + const testProductIndex = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId) + ) + const testProduct1Index = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId1) + ) + + expect(testProductIndex).toBe(0) + expect(testProduct1Index).toBe(1) + }) + + it("returns a list of ordered products by variants prices DESC", async () => { + const api = useApi() + + await simpleProductFactory(dbConnection, { + id: "test-product2", + status: "published", + variants: [ + { + id: "test_variant_5", + prices: [ + { + currency: "usd", + amount: 200, + }, + ], + }, + ], + }) + + const response = await api.get( + "/store/products?order=-variants.prices.amount" + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(6) + + const testProductIndex = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId) + ) + const testProduct1Index = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId1) + ) + const testProduct2Index = response.data.products.indexOf( + response.data.products.find((p) => p.id === "test-product2") + ) + + expect(testProduct2Index).toBe(3) // 200 + expect(testProductIndex).toBe(4) // 100 + expect(testProduct1Index).toBe(5) // 100 + }) + + it("returns a list of ordered products by variants prices ASC", async () => { + const api = useApi() + + await simpleProductFactory(dbConnection, { + id: "test-product2", + status: "published", + variants: [ + { + id: "test_variant_5", + prices: [ + { + currency: "usd", + amount: 200, + }, + ], + }, + ], + }) + + const response = await api.get( + "/store/products?order=variants.prices.amount" + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(6) + + const testProductIndex = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId) + ) + const testProduct1Index = response.data.products.indexOf( + response.data.products.find((p) => p.id === testProductId1) + ) + const testProduct2Index = response.data.products.indexOf( + response.data.products.find((p) => p.id === "test-product2") + ) + + expect(testProductIndex).toBe(0) // 100 + expect(testProduct1Index).toBe(1) // 100 + expect(testProduct2Index).toBe(2) // 200 + }) + + it("returns a list of ordered products by id ASC and filtered with free text search", async () => { + const api = useApi() + + const response = await api.get("/store/products?q=filtering&order=id") + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(2) + + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: testProductFilteringId1, + }), + expect.objectContaining({ + id: testProductFilteringId2, + }), + ]) + }) + + it("returns a list of ordered products by id DESC and filtered with free text search", async () => { + const api = useApi() + + const response = await api.get("/store/products?q=filtering&order=-id") + + expect(response.status).toEqual(200) + expect(response.data.products).toHaveLength(2) + + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: testProductFilteringId2, + }), + expect.objectContaining({ + id: testProductFilteringId1, + }), + ]) + }) + it("returns a list of products in collection", async () => { const api = useApi() @@ -54,7 +245,7 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product_filtering_2", + id: testProductFilteringId2, collection_id: "test-collection2", }), ]) @@ -67,7 +258,7 @@ describe("/store/products", () => { } }) - it("returns a list of products in with a given tag", async () => { + it("returns a list of products with a given tag", async () => { const api = useApi() const notExpected = [expect.objectContaining({ id: "tag4" })] @@ -83,7 +274,7 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product_filtering_1", + id: testProductFilteringId1, collection_id: "test-collection1", }), ]) @@ -110,7 +301,7 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "giftcard", + id: giftCardId, is_giftcard: true, }), ]) @@ -151,7 +342,7 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product_filtering_1", + id: testProductFilteringId1, collection_id: "test-collection1", }), ]) @@ -168,7 +359,7 @@ describe("/store/products", () => { const api = useApi() const notExpected = [ - expect.objectContaining({ handle: "test-product_filtering_1" }), + expect.objectContaining({ handle: testProductFilteringId1 }), ] const response = await api @@ -182,8 +373,8 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product_filtering_2", - handle: "test-product_filtering_2", + id: testProductFilteringId2, + handle: testProductFilteringId2, }), ]) ) @@ -231,23 +422,23 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product1", + id: testProductId1, collection_id: "test-collection", }), expect.objectContaining({ - id: "test-product", + id: testProductId, collection_id: "test-collection", }), expect.objectContaining({ - id: "test-product_filtering_2", + id: testProductFilteringId2, collection_id: "test-collection2", }), expect.objectContaining({ - id: "test-product_filtering_1", + id: testProductFilteringId1, collection_id: "test-collection1", }), expect.objectContaining({ - id: "giftcard", + id: giftCardId, }), ]) ) @@ -284,11 +475,11 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product1", + id: testProductId1, collection_id: "test-collection", }), expect.objectContaining({ - id: "test-product", + id: testProductId, collection_id: "test-collection", variants: [ expect.objectContaining({ @@ -307,22 +498,6 @@ describe("/store/products", () => { }), ], }), - expect.objectContaining({ - original_price: 100, - calculated_price: 80, - prices: [ - expect.objectContaining({ - id: "test-price2", - currency_code: "usd", - amount: 100, - }), - expect.objectContaining({ - id: "test-price2-discount", - currency_code: "usd", - amount: 80, - }), - ], - }), expect.objectContaining({ original_price: 100, calculated_price: 80, @@ -339,18 +514,34 @@ describe("/store/products", () => { }), ], }), + expect.objectContaining({ + original_price: 100, + calculated_price: 80, + prices: [ + expect.objectContaining({ + id: "test-price2", + currency_code: "usd", + amount: 100, + }), + expect.objectContaining({ + id: "test-price2-discount", + currency_code: "usd", + amount: 80, + }), + ], + }), ], }), expect.objectContaining({ - id: "test-product_filtering_2", + id: testProductFilteringId2, collection_id: "test-collection2", }), expect.objectContaining({ - id: "test-product_filtering_1", + id: testProductFilteringId1, collection_id: "test-collection1", }), expect.objectContaining({ - id: "giftcard", + id: giftCardId, }), ]) ) @@ -394,29 +585,31 @@ describe("/store/products", () => { "/store/products?expand=variants,variants.prices&fields=id,title&limit=1" ) - expect(response.data).toMatchSnapshot({ - products: [ - { - id: expect.any(String), - variants: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - id: expect.any(String), - product_id: expect.any(String), - prices: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - id: expect.any(String), - variant_id: expect.any(String), - }, - ], - }, - ], - }, - ], - }) + expect(response.data).toEqual( + expect.objectContaining({ + products: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + updated_at: expect.any(String), + id: expect.any(String), + product_id: expect.any(String), + prices: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + updated_at: expect.any(String), + id: expect.any(String), + variant_id: expect.any(String), + }), + ]), + }), + ]), + }), + ]), + }) + ) }) }) @@ -436,284 +629,286 @@ describe("/store/products", () => { const response = await api.get("/store/products/test-product") - expect(response.data).toMatchSnapshot({ - product: { - id: "test-product", - variants: [ - { - id: "test-variant", - inventory_quantity: 10, - allow_backorder: false, - title: "Test variant", - sku: "test-sku", - ean: "test-ean", - upc: "test-upc", - length: null, - manage_inventory: true, - material: null, - metadata: null, - mid_code: null, - height: null, - hs_code: null, - origin_country: null, - calculated_price: null, - original_price: null, - barcode: "test-barcode", - product_id: "test-product", - created_at: expect.any(String), - updated_at: expect.any(String), - options: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - prices: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 100, - currency_code: "usd", - deleted_at: null, - min_quantity: null, - max_quantity: null, - price_list_id: null, - id: "test-price", - region_id: null, - variant_id: "test-variant", - }, - { - id: "test-price-discount", - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 80, - currency_code: "usd", - price_list_id: "pl", - deleted_at: null, - region_id: null, - variant_id: "test-variant", - price_list: { - id: "pl", - type: "sale", + expect(response.data).toEqual( + expect.objectContaining({ + product: expect.objectContaining({ + id: testProductId, + variants: expect.arrayContaining([ + expect.objectContaining({ + id: "test-variant", + inventory_quantity: 10, + allow_backorder: false, + title: "Test variant", + sku: "test-sku", + ean: "test-ean", + upc: "test-upc", + length: null, + manage_inventory: true, + material: null, + metadata: null, + mid_code: null, + height: null, + hs_code: null, + origin_country: null, + calculated_price: null, + original_price: null, + barcode: "test-barcode", + product_id: testProductId, + created_at: expect.any(String), + updated_at: expect.any(String), + options: expect.arrayContaining([ + expect.objectContaining({ created_at: expect.any(String), updated_at: expect.any(String), - }, - }, - ], - }, - { - id: "test-variant_2", - inventory_quantity: 10, - allow_backorder: false, - title: "Test variant rank (2)", - sku: "test-sku2", - ean: "test-ean2", - upc: "test-upc2", - length: null, - manage_inventory: true, - material: null, - metadata: null, - mid_code: null, - height: null, - hs_code: null, - origin_country: null, - barcode: null, - calculated_price: null, - original_price: null, - product_id: "test-product", - created_at: expect.any(String), - updated_at: expect.any(String), - options: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - prices: [ - { - id: "test-price2", - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 100, - currency_code: "usd", - price_list_id: null, - deleted_at: null, - region_id: null, - variant_id: "test-variant_2", - }, - { - id: "test-price2-discount", - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 80, - currency_code: "usd", - price_list_id: "pl", - deleted_at: null, - region_id: null, - variant_id: "test-variant_2", - price_list: { - id: "pl", - type: "sale", + }), + ]), + prices: expect.arrayContaining([ + expect.objectContaining({ created_at: expect.any(String), updated_at: expect.any(String), - }, - }, - ], - }, - { - id: "test-variant_1", - inventory_quantity: 10, - allow_backorder: false, - title: "Test variant rank (1)", - sku: "test-sku1", - ean: "test-ean1", - upc: "test-upc1", - length: null, - manage_inventory: true, - material: null, - metadata: null, - mid_code: null, - height: null, - hs_code: null, - origin_country: null, - calculated_price: null, - original_price: null, - barcode: "test-barcode 1", - product_id: "test-product", - created_at: expect.any(String), - updated_at: expect.any(String), - options: [ - { - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - prices: [ - { - id: "test-price1", - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 100, - currency_code: "usd", - min_quantity: null, - max_quantity: null, - price_list_id: null, - deleted_at: null, - region_id: null, - variant_id: "test-variant_1", - }, - { - id: "test-price1-discount", - created_at: expect.any(String), - updated_at: expect.any(String), - amount: 80, - currency_code: "usd", - price_list_id: "pl", - deleted_at: null, - region_id: null, - variant_id: "test-variant_1", - price_list: { - id: "pl", - type: "sale", + amount: 100, + currency_code: "usd", + deleted_at: null, + min_quantity: null, + max_quantity: null, + price_list_id: null, + id: "test-price", + region_id: null, + variant_id: "test-variant", + }), + expect.objectContaining({ + id: "test-price-discount", created_at: expect.any(String), updated_at: expect.any(String), - }, - }, - ], - }, - ], - images: [ - { - id: "test-image", + amount: 80, + currency_code: "usd", + price_list_id: "pl", + deleted_at: null, + region_id: null, + variant_id: "test-variant", + price_list: expect.objectContaining({ + id: "pl", + type: "sale", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }), + ]), + }), + expect.objectContaining({ + id: "test-variant_2", + inventory_quantity: 10, + allow_backorder: false, + title: "Test variant rank (2)", + sku: "test-sku2", + ean: "test-ean2", + upc: "test-upc2", + length: null, + manage_inventory: true, + material: null, + metadata: null, + mid_code: null, + height: null, + hs_code: null, + origin_country: null, + barcode: null, + calculated_price: null, + original_price: null, + product_id: testProductId, + created_at: expect.any(String), + updated_at: expect.any(String), + options: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: "test-price2", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 100, + currency_code: "usd", + price_list_id: null, + deleted_at: null, + region_id: null, + variant_id: "test-variant_2", + }), + expect.objectContaining({ + id: "test-price2-discount", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + price_list_id: "pl", + deleted_at: null, + region_id: null, + variant_id: "test-variant_2", + price_list: expect.objectContaining({ + id: "pl", + type: "sale", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }), + ]), + }), + expect.objectContaining({ + id: "test-variant_1", + inventory_quantity: 10, + allow_backorder: false, + title: "Test variant rank (1)", + sku: "test-sku1", + ean: "test-ean1", + upc: "test-upc1", + length: null, + manage_inventory: true, + material: null, + metadata: null, + mid_code: null, + height: null, + hs_code: null, + origin_country: null, + calculated_price: null, + original_price: null, + barcode: "test-barcode 1", + product_id: testProductId, + created_at: expect.any(String), + updated_at: expect.any(String), + options: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: "test-price1", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 100, + currency_code: "usd", + min_quantity: null, + max_quantity: null, + price_list_id: null, + deleted_at: null, + region_id: null, + variant_id: "test-variant_1", + }), + expect.objectContaining({ + id: "test-price1-discount", + created_at: expect.any(String), + updated_at: expect.any(String), + amount: 80, + currency_code: "usd", + price_list_id: "pl", + deleted_at: null, + region_id: null, + variant_id: "test-variant_1", + price_list: expect.objectContaining({ + id: "pl", + type: "sale", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + }), + ]), + }), + ]), + images: expect.arrayContaining([ + expect.objectContaining({ + id: "test-image", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + handle: testProductId, + title: "Test product", + profile_id: expect.stringMatching(/^sp_*/), + description: "test-product-description", + collection_id: "test-collection", + collection: expect.objectContaining({ + id: "test-collection", created_at: expect.any(String), updated_at: expect.any(String), - }, - ], - handle: "test-product", - title: "Test product", - profile_id: expect.stringMatching(/^sp_*/), - description: "test-product-description", - collection_id: "test-collection", - collection: { - id: "test-collection", + }), + type: expect.objectContaining({ + id: "test-type", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + tags: expect.arrayContaining([ + expect.objectContaining({ + id: "tag1", + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: "test-option", + values: expect.arrayContaining([ + expect.objectContaining({ + id: "test-variant-option", + value: "Default variant", + option_id: "test-option", + variant_id: "test-variant", + metadata: null, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-variant-option-1", + value: "Default variant 1", + option_id: "test-option", + variant_id: "test-variant_1", + metadata: null, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-variant-option-2", + value: "Default variant 2", + option_id: "test-option", + variant_id: "test-variant_2", + metadata: null, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-variant-option-3", + value: "Default variant 3", + option_id: "test-option", + variant_id: "test-variant_3", + metadata: null, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }), + expect.objectContaining({ + id: "test-variant-option-4", + value: "Default variant 4", + option_id: "test-option", + variant_id: "test-variant_4", + metadata: null, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), created_at: expect.any(String), updated_at: expect.any(String), - }, - type: { - id: "test-type", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - tags: [ - { - id: "tag1", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: "test-option", - values: [ - { - id: "test-variant-option", - value: "Default variant", - option_id: "test-option", - variant_id: "test-variant", - metadata: null, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: "test-variant-option-1", - value: "Default variant 1", - option_id: "test-option", - variant_id: "test-variant_1", - metadata: null, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: "test-variant-option-2", - value: "Default variant 2", - option_id: "test-option", - variant_id: "test-variant_2", - metadata: null, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: "test-variant-option-3", - value: "Default variant 3", - option_id: "test-option", - variant_id: "test-variant_3", - metadata: null, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: "test-variant-option-4", - value: "Default variant 4", - option_id: "test-option", - variant_id: "test-variant_4", - metadata: null, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - created_at: expect.any(String), - updated_at: expect.any(String), - }, - }) + }), + }) + ) }) it("lists all published products", async () => { @@ -742,7 +937,7 @@ describe("/store/products", () => { expect(response.data.products).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: "test-product", + id: testProductId, status: "published", }), ]) diff --git a/integration-tests/api/helpers/store-product-seeder.js b/integration-tests/api/helpers/store-product-seeder.js index a675ec352e..2290465c1a 100644 --- a/integration-tests/api/helpers/store-product-seeder.js +++ b/integration-tests/api/helpers/store-product-seeder.js @@ -10,8 +10,6 @@ const { Image, Cart, PriceList, - CustomerGroup, - Customer, } = require("@medusajs/medusa") module.exports = async (connection, data = {}) => { diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index 83a8a4f3a1..d399b97a80 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -157,6 +157,7 @@ import { FilterableProductProps } from "../../../../types/product" * - (query) limit=50 {integer} Limit the number of products returned. * - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result. * - (query) fields {string} (Comma separated) Which fields should be included in each product of the result. + * - (query) order {string} the field used to order the products. * x-codeSamples: * - lang: JavaScript * label: JS Client @@ -258,4 +259,8 @@ export class AdminGetProductsParams extends FilterableProductProps { @IsString() @IsOptional() fields?: string + + @IsString() + @IsOptional() + order?: string } diff --git a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js index d99efbe365..dd686d719a 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/list-products.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/list-products.js @@ -23,6 +23,10 @@ describe("GET /store/products", () => { relations: defaultStoreProductsRelations, skip: 0, take: 100, + select: undefined, + order: { + created_at: "DESC", + }, } ) }) @@ -50,6 +54,10 @@ describe("GET /store/products", () => { relations: defaultStoreProductsRelations, skip: 0, take: 100, + order: { + created_at: "DESC", + }, + select: undefined, } ) }) diff --git a/packages/medusa/src/api/routes/store/products/index.ts b/packages/medusa/src/api/routes/store/products/index.ts index b9eb230541..6f875e127c 100644 --- a/packages/medusa/src/api/routes/store/products/index.ts +++ b/packages/medusa/src/api/routes/store/products/index.ts @@ -2,13 +2,14 @@ import { RequestHandler, Router } from "express" import "reflect-metadata" import { Product } from "../../../.." -import middlewares from "../../../middlewares" +import middlewares, { transformQuery } from "../../../middlewares" import { FlagRouter } from "../../../../utils/flag-router" import { PaginatedResponse } from "../../../../types/common" import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params" import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys" import { validateProductSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-product-sales-channel-association" import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param" +import { StoreGetProductsParams } from "./list-products" const route = Router() @@ -24,7 +25,14 @@ export default (app, featureFlagRouter: FlagRouter) => { route.use("/:id", validateProductSalesChannelAssociation) } - route.get("/", middlewares.wrap(require("./list-products").default)) + route.get( + "/", + transformQuery(StoreGetProductsParams, { + defaultRelations: defaultStoreProductsRelations, + isList: true, + }), + middlewares.wrap(require("./list-products").default) + ) route.get("/:id", middlewares.wrap(require("./get-product").default)) route.post("/search", middlewares.wrap(require("./search").default)) diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index cc5511c91f..673367d893 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -7,21 +7,16 @@ import { IsString, ValidateNested, } from "class-validator" -import { omit, pickBy } from "lodash" import { CartService, ProductService, RegionService, } from "../../../../services" -import { isDefined } from "medusa-core-utils" -import { defaultStoreProductsRelations } from "." import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" -import { Product } from "../../../../models" import PricingService from "../../../../services/pricing" import { DateComparisonOperator } from "../../../../types/common" import { PriceSelectionParams } from "../../../../types/price-selection" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" -import { validator } from "../../../../utils/validator" import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" import { IsType } from "../../../../utils/validators/is-type" import { FlagRouter } from "../../../../utils/flag-router" @@ -133,6 +128,7 @@ import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/pub * - (query) limit=100 {integer} Limit the number of products returned. * - (query) expand {string} (Comma separated) Which fields should be expanded in each order of the result. * - (query) fields {string} (Comma separated) Which fields should be included in each order of the result. + * - (query) order {string} the field used to order the products. * x-codeSamples: * - lang: JavaScript * label: JS Client @@ -196,59 +192,34 @@ export default async (req, res) => { const cartService: CartService = req.scope.resolve("cartService") const regionService: RegionService = req.scope.resolve("regionService") - const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") - - const validated = await validator(StoreGetProductsParams, req.query) - - if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) { - if (req.publishableApiKeyScopes?.sales_channel_id.length) { - validated.sales_channel_id = - validated.sales_channel_id || - req.publishableApiKeyScopes.sales_channel_id - } - } - - const filterableFields: StoreGetProductsParams = omit(validated, [ - "fields", - "expand", - "limit", - "offset", - "cart_id", - "region_id", - "currency_code", - ]) + const validated = req.validatedQuery as StoreGetProductsParams + let { + cart_id, + region_id: regionId, + currency_code: currencyCode, + ...filterableFields + } = req.filterableFields + const listConfig = req.listConfig // get only published products for store endpoint filterableFields["status"] = ["published"] - let includeFields: (keyof Product)[] = [] - if (validated.fields) { - const set = new Set(validated.fields.split(",")) as Set - set.add("id") - includeFields = [...set] - } + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) { + if (req.publishableApiKeyScopes?.sales_channel_id.length) { + filterableFields.sales_channel_id = + filterableFields.sales_channel_id || + req.publishableApiKeyScopes.sales_channel_id - let expandFields: string[] = [] - if (validated.expand) { - expandFields = validated.expand.split(",") - } - - const listConfig = { - select: includeFields.length ? includeFields : undefined, - relations: expandFields.length - ? expandFields - : defaultStoreProductsRelations, - skip: validated.offset, - take: validated.limit, + listConfig.relations.push("sales_channels") + } } const [rawProducts, count] = await productService.listAndCount( - pickBy(filterableFields, (val) => isDefined(val)), + filterableFields, listConfig ) - let regionId = validated.region_id - let currencyCode = validated.currency_code if (validated.cart_id) { const cart = await cartService.retrieve(validated.cart_id, { select: ["id", "region_id"], @@ -261,7 +232,7 @@ export default async (req, res) => { } const products = await pricingService.setProductPrices(rawProducts, { - cart_id: validated.cart_id, + cart_id: cart_id, region_id: regionId, currency_code: currencyCode, customer_id: req.user?.customer_id, @@ -294,6 +265,10 @@ export class StoreGetProductsPaginationParams extends PriceSelectionParams { @IsOptional() @Type(() => Number) limit?: number = 100 + + @IsString() + @IsOptional() + order?: string } export class StoreGetProductsParams extends StoreGetProductsPaginationParams { diff --git a/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js b/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js index eb24500219..c1c163764e 100644 --- a/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js +++ b/packages/medusa/src/api/routes/store/variants/__tests__/list-variants.js @@ -1,6 +1,5 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" describe("List variants", () => { describe("list variants successfull", () => { diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index 5e5b022500..e3c1b70b0c 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -1,17 +1,8 @@ import { flatten, groupBy, map, merge } from "lodash" -import { - Brackets, - EntityRepository, - FindOperator, - In, - Repository, -} from "typeorm" +import { Brackets, EntityRepository, FindOperator, In, Repository, } from "typeorm" import { PriceList, Product, SalesChannel } from "../models" -import { - ExtendedFindConfig, - Selector, - WithRequiredProperty, -} from "../types/common" +import { ExtendedFindConfig, Selector, WithRequiredProperty, } from "../types/common" +import { applyOrdering } from "../utils/repository" export type ProductSelector = Omit, "tags"> & { tags: FindOperator @@ -45,6 +36,8 @@ export class ProductRepository extends Repository { optionsWithoutRelations: FindWithoutRelationsOptions, shouldCount = false ): Promise<[Product[], number]> { + const productAlias = "product" + const tags = optionsWithoutRelations?.where?.tags delete optionsWithoutRelations?.where?.tags @@ -58,8 +51,8 @@ export class ProductRepository extends Repository { optionsWithoutRelations?.where?.discount_condition_id delete optionsWithoutRelations?.where?.discount_condition_id - const qb = this.createQueryBuilder("product") - .select(["product.id"]) + const qb = this.createQueryBuilder(productAlias) + .select([`${productAlias}.id`]) .skip(optionsWithoutRelations.skip) .take(optionsWithoutRelations.take) @@ -67,38 +60,26 @@ export class ProductRepository extends Repository { qb.where(optionsWithoutRelations.where) } - if (optionsWithoutRelations.order) { - const toSelect: string[] = [] - const parsed = Object.entries(optionsWithoutRelations.order).reduce( - (acc, [k, v]) => { - const key = `product.${k}` - toSelect.push(key) - acc[key] = v - return acc - }, - {} - ) - qb.addSelect(toSelect) - qb.orderBy(parsed) - } - if (tags) { - qb.leftJoin("product.tags", "tags").andWhere(`tags.id IN (:...tag_ids)`, { - tag_ids: tags.value, - }) + qb.leftJoin(`${productAlias}.tags`, "tags").andWhere( + `tags.id IN (:...tag_ids)`, + { + tag_ids: tags.value, + } + ) } if (price_lists) { - qb.leftJoin("product.variants", "variants") - .leftJoin("variants.prices", "ma") - .andWhere("ma.price_list_id IN (:...price_list_ids)", { + qb.leftJoin(`${productAlias}.variants`, "variants") + .leftJoin("variants.prices", "prices") + .andWhere("prices.price_list_id IN (:...price_list_ids)", { price_list_ids: price_lists.value, }) } if (sales_channels) { qb.innerJoin( - "product.sales_channels", + `${productAlias}.sales_channels`, "sales_channels", "sales_channels.id IN (:...sales_channels_ids)", { sales_channels_ids: sales_channels.value } @@ -109,11 +90,20 @@ export class ProductRepository extends Repository { qb.innerJoin( "discount_condition_product", "dc_product", - `dc_product.product_id = product.id AND dc_product.condition_id = :dcId`, + `dc_product.product_id = ${productAlias}.id AND dc_product.condition_id = :dcId`, { dcId: discount_condition_id } ) } + const joinedWithPriceLists = !!price_lists + applyOrdering({ + repository: this, + order: optionsWithoutRelations.order ?? {}, + qb, + alias: productAlias, + shouldJoin: (relation) => relation !== "prices" || !joinedWithPriceLists, + }) + if (optionsWithoutRelations.withDeleted) { qb.withDeleted() } @@ -151,7 +141,8 @@ export class ProductRepository extends Repository { entityIds: string[], groupedRelations: { [toplevel: string]: string[] }, withDeleted = false, - select: (keyof Product)[] = [] + select: (keyof Product)[] = [], + order: { [column: string]: "ASC" | "DESC" } = {} ): Promise { const entitiesIdsWithRelations = await Promise.all( Object.entries(groupedRelations).map(async ([toplevel, rels]) => { @@ -162,15 +153,13 @@ export class ProductRepository extends Repository { } if (toplevel === "variants") { - querybuilder = querybuilder - .leftJoinAndSelect( - `products.${toplevel}`, - toplevel, - "variants.deleted_at IS NULL" - ) - .orderBy({ - "variants.variant_rank": "ASC", - }) + querybuilder = querybuilder.leftJoinAndSelect( + `products.${toplevel}`, + toplevel, + "variants.deleted_at IS NULL" + ) + + order["variants.variant_rank"] = "ASC" } else { querybuilder = querybuilder.leftJoinAndSelect( `products.${toplevel}`, @@ -251,12 +240,14 @@ export class ProductRepository extends Repository { entitiesIds, groupedRelations, idsOrOptionsWithoutRelations.withDeleted, - idsOrOptionsWithoutRelations.select + idsOrOptionsWithoutRelations.select, + idsOrOptionsWithoutRelations.order ) - const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) - const entitiesToReturn = - this.mergeEntitiesWithRelations(entitiesAndRelations) + const entitiesAndRelations = groupBy(entitiesIdsWithRelations, "id") + const entitiesToReturn = map(entitiesIds, (id) => + merge({}, ...entitiesAndRelations[id]) + ) return [entitiesToReturn, count] } @@ -353,6 +344,12 @@ export class ProductRepository extends Repository { options: FindWithoutRelationsOptions = { where: {} }, relations: string[] = [] ): Promise<[Product[], number]> { + const productAlias = "product" + const pricesAlias = "prices" + const variantsAlias = "variants" + const collectionAlias = "collection" + const tagsAlias = "tags" + const tags = options.where.tags delete options.where.tags @@ -367,18 +364,18 @@ export class ProductRepository extends Repository { const cleanedOptions = this._cleanOptions(options) - let qb = this.createQueryBuilder("product") - .leftJoinAndSelect("product.variants", "variant") - .leftJoinAndSelect("product.collection", "collection") - .select(["product.id"]) + let qb = this.createQueryBuilder(`${productAlias}`) + .leftJoinAndSelect(`${productAlias}.variants`, variantsAlias) + .leftJoinAndSelect(`${productAlias}.collection`, `${collectionAlias}`) + .select([`${productAlias}.id`]) .where(cleanedOptions.where) .andWhere( new Brackets((qb) => { - qb.where(`product.description ILIKE :q`, { q: `%${q}%` }) - .orWhere(`product.title ILIKE :q`, { q: `%${q}%` }) - .orWhere(`variant.title ILIKE :q`, { q: `%${q}%` }) - .orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` }) - .orWhere(`collection.title ILIKE :q`, { q: `%${q}%` }) + qb.where(`${productAlias}.description ILIKE :q`, { q: `%${q}%` }) + .orWhere(`${productAlias}.title ILIKE :q`, { q: `%${q}%` }) + .orWhere(`${variantsAlias}.title ILIKE :q`, { q: `%${q}%` }) + .orWhere(`${variantsAlias}.sku ILIKE :q`, { q: `%${q}%` }) + .orWhere(`${collectionAlias}.title ILIKE :q`, { q: `%${q}%` }) }) ) .skip(cleanedOptions.skip) @@ -388,47 +385,72 @@ export class ProductRepository extends Repository { qb.innerJoin( "discount_condition_product", "dc_product", - `dc_product.product_id = product.id AND dc_product.condition_id = :dcId`, + `dc_product.product_id = ${productAlias}.id AND dc_product.condition_id = :dcId`, { dcId: discount_condition_id } ) } if (tags) { - qb.leftJoin("product.tags", "tags").andWhere(`tags.id IN (:...tag_ids)`, { - tag_ids: tags.value, - }) + qb.leftJoin(`${productAlias}.tags`, tagsAlias).andWhere( + `${tagsAlias}.id IN (:...tag_ids)`, + { + tag_ids: tags.value, + } + ) } if (price_lists) { - qb.leftJoin("product.variants", "variants") - .leftJoin("variants.prices", "ma") - .andWhere("ma.price_list_id IN (:...price_list_ids)", { + const variantPricesAlias = `${variantsAlias}_prices` + qb.leftJoin(`${productAlias}.variants`, variantPricesAlias) + .leftJoin(`${variantPricesAlias}.prices`, pricesAlias) + .andWhere(`${pricesAlias}.price_list_id IN (:...price_list_ids)`, { price_list_ids: price_lists.value, }) } if (sales_channels) { qb.innerJoin( - "product.sales_channels", + `${productAlias}.sales_channels`, "sales_channels", "sales_channels.id IN (:...sales_channels_ids)", { sales_channels_ids: sales_channels.value } ) } + const joinedWithTags = !!tags + const joinedWithPriceLists = !!price_lists + applyOrdering({ + repository: this, + order: options.order ?? {}, + qb, + alias: productAlias, + shouldJoin: (relation) => + relation !== variantsAlias && + (relation !== pricesAlias || !joinedWithPriceLists) && + (relation !== tagsAlias || !joinedWithTags), + }) + if (cleanedOptions.withDeleted) { qb = qb.withDeleted() } const [results, count] = await qb.getManyAndCount() + const orderedResultsSet = new Set(results.map((p) => p.id)) const products = await this.findWithRelations( relations, - results.map((r) => r.id), + [...orderedResultsSet], cleanedOptions.withDeleted ) + const productsMap = new Map(products.map((p) => [p.id, p])) - return [products, count] + // Looping through the orderedResultsSet in order to maintain the original order and assign the data returned by findWithRelations + const orderedProducts: Product[] = [] + orderedResultsSet.forEach((id) => { + orderedProducts.push(productsMap.get(id)!) + }) + + return [orderedProducts, count] } public async isProductInSalesChannels( diff --git a/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts b/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts index 2112f9841c..d6d926be4a 100644 --- a/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts +++ b/packages/medusa/src/strategies/__tests__/batch-jobs/product/export.ts @@ -3,10 +3,7 @@ import { IdMap, MockManager } from "medusa-test-utils" import { User } from "../../../../models" import { BatchJobStatus } from "../../../../types/batch-job" import { productsToExport } from "../../../__fixtures__/product-export-data" -import { - AdminPostBatchesReq, - defaultAdminProductRelations, -} from "../../../../api" +import { AdminPostBatchesReq, defaultAdminProductRelations, } from "../../../../api" import { ProductExportBatchJob } from "../../../batch-jobs/product/types" import { Request } from "express" import { FlagRouter } from "../../../../utils/flag-router" diff --git a/packages/medusa/src/utils/get-query-config.ts b/packages/medusa/src/utils/get-query-config.ts index 377ebd2142..57fd867cb6 100644 --- a/packages/medusa/src/utils/get-query-config.ts +++ b/packages/medusa/src/utils/get-query-config.ts @@ -50,7 +50,7 @@ export function getListConfig( expand?: string[], limit = 50, offset = 0, - order?: { [k: symbol]: "DESC" | "ASC" } + order: { [k: string | symbol]: "DESC" | "ASC" } = {} ): FindConfig { let includeFields: (keyof TModel)[] = [] if (isDefined(fields)) { @@ -66,8 +66,10 @@ export function getListConfig( expandFields = expand } - const orderBy: Record = order ?? { - created_at: "DESC", + const orderBy = order + + if (!Object.keys(order).length) { + orderBy["created_at"] = "DESC" } return { diff --git a/packages/medusa/src/utils/repository.ts b/packages/medusa/src/utils/repository.ts index 5c1eea4481..b7155a3d44 100644 --- a/packages/medusa/src/utils/repository.ts +++ b/packages/medusa/src/utils/repository.ts @@ -1,7 +1,9 @@ import { flatten, groupBy, map, merge } from "lodash" -import { Repository, SelectQueryBuilder } from "typeorm" +import { EntityMetadata, Repository, SelectQueryBuilder } from "typeorm" import { FindWithoutRelationsOptions } from "../repositories/customer-group" +// TODO: All the utilities except applyOrdering needs to be re worked depending on the outcome of the product repository + /** * Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs. * Allow to query the relations for the specified entity ids @@ -163,3 +165,80 @@ export function mergeEntitiesWithRelations( merge({}, ...entityAndRelations) ) } + +/** + * Apply the appropriate order depending on the requirements + * @param repository + * @param order The field on which to apply the order (e.g { "variants.prices.amount": "DESC" }) + * @param qb + * @param alias + * @param shouldJoin In case a join is already applied elsewhere and therefore you want to avoid to re joining the data in that case you can return false for specific relations + */ +export function applyOrdering({ + repository, + order, + qb, + alias, + shouldJoin, +}: { + repository: Repository + order: Record + qb: SelectQueryBuilder + alias: string + shouldJoin: (relation: string) => boolean +}) { + const toSelect: string[] = [] + + const parsed = Object.entries(order).reduce( + (acc, [orderPath, orderDirection]) => { + // If the orderPath (e.g variants.prices.amount) includes a point it means that it is to access + // a child relation of an unknown depth + if (orderPath.includes(".")) { + // We are spliting the path and separating the relations from the property to order. (e.g relations ["variants", "prices"] and property "amount" + const relationsToJoin = orderPath.split(".") + const propToOrder = relationsToJoin.pop() + + // For each relation we will retrieve the metadata in order to use the right property name from the relation registered in the entity. + // Each time we will return the child (i.e the relation) and the inverse metadata (corresponding to the child metadata from the parent point of view) + // In order for the next child to know its parent + relationsToJoin.reduce( + ([parent, parentMetadata], child) => { + // Find the relation metadata from the parent entity + const relationMetadata = ( + parentMetadata as EntityMetadata + ).relations.find( + (relationMetadata) => relationMetadata.propertyName === child + ) + + // The consumer can refuse to apply a join on a relation if the join has already been applied before calling this util + const shouldApplyJoin = shouldJoin(child) + if (shouldApplyJoin) { + qb.leftJoin(`${parent}.${relationMetadata!.propertyPath}`, child) + } + + // Return the child relation to be the parent for the next one, as well as the metadata corresponding the child in order + // to find the next relation metadata for the next child + return [child, relationMetadata!.inverseEntityMetadata] + }, + [alias, repository.metadata] + ) + + // The key for variants.prices.amount will be "prices.amount" since we are ordering on the join added to its parent "variants" in this example + const key = `${ + relationsToJoin[relationsToJoin.length - 1] + }.${propToOrder}` + acc[key] = orderDirection + toSelect.push(key) + return acc + } + + const key = `${alias}.${orderPath}` + toSelect.push(key) + acc[key] = orderDirection + return acc + }, + {} + ) + qb.addSelect(toSelect) + qb.orderBy(parsed) +}