diff --git a/.eslintrc.js b/.eslintrc.js index 976fd049c..9b420a463 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,9 @@ module.exports = { '@vue-storefront/eslint-config-vue', '@vue-storefront/eslint-config-jest', ], + globals: { + "__ENV": "readonly", + }, rules: { "@typescript-eslint/no-floating-promises": "off", "jest/expect-expect": [ diff --git a/.github/workflows/run-k6-load-test.yml b/.github/workflows/run-k6-load-test.yml new file mode 100644 index 000000000..f29bb4ebd --- /dev/null +++ b/.github/workflows/run-k6-load-test.yml @@ -0,0 +1,52 @@ +name: Run K6 test file +on: + workflow_dispatch: + inputs: + cloud: + description: True = run in K6 cloud; False = run on the test on GitHub Actions agent + required: true + default: false + type: boolean + + filename: + description: The K6 test file to run relative to repository root + required: true + default: packages/theme/tests/load/searchProduct.js + type: string + + environment: + description: The full URL of the environment on which load tests will be ran + required: true + default: https://demo-magento2-canary.europe-west1.gcp.storefrontcloud.io + type: choice + options: + - https://demo-magento2-canary.europe-west1.gcp.storefrontcloud.io + - https://demo-magento2-dev.europe-west1.gcp.storefrontcloud.io + - https://demo-magento2.europe-west1.gcp.storefrontcloud.cloud + - https://demo-magento2-enterprise.europe-west1.gcp.storefrontcloud.io + + flags: + description: Additional argument and flags to provide to the k6 CLI. See https://k6.io/docs/using-k6/options for details. + required: false + default: '' + type: string + +jobs: + build: + name: Run k6 cloud test + runs-on: ubuntu-latest + environment: test + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Run k6 cloud test + uses: grafana/k6-action@v0.2.0 + with: + cloud: ${{ inputs.cloud }} + token: ${{ secrets.K6_CLOUD_API_TOKEN }} + filename: ${{ inputs.filename }} + flags: ${{ inputs.flags }} + env: + BASE_URL: ${{ inputs.environment }} diff --git a/packages/api-client/src/api/customMutation/index.ts b/packages/api-client/src/api/customMutation/index.ts index dbe9d36cc..bb6d2c25f 100644 --- a/packages/api-client/src/api/customMutation/index.ts +++ b/packages/api-client/src/api/customMutation/index.ts @@ -1,5 +1,5 @@ +import gql from 'graphql-tag'; import { FetchPolicy, FetchResult } from '@apollo/client/core'; -import { DocumentNode } from 'graphql'; import { Context } from '../../types/context'; import getHeaders from '../getHeaders'; @@ -10,12 +10,12 @@ export default async ( mutationVariables, fetchPolicy, }: { - mutation: DocumentNode, + mutation: string, mutationVariables: MUTATION_VARIABLES, fetchPolicy?: Extract, }, ): Promise> => context.client.mutate({ - mutation, + mutation: gql`${mutation}`, variables: { ...mutationVariables }, fetchPolicy: fetchPolicy || 'no-cache', context: { diff --git a/packages/api-client/src/api/customQuery/index.ts b/packages/api-client/src/api/customQuery/index.ts index 970f3c1e7..903c2cadb 100644 --- a/packages/api-client/src/api/customQuery/index.ts +++ b/packages/api-client/src/api/customQuery/index.ts @@ -1,5 +1,5 @@ +import gql from 'graphql-tag'; import { ApolloQueryResult, FetchPolicy } from '@apollo/client/core'; -import { DocumentNode } from 'graphql'; import { Context } from '../../types/context'; import getHeaders from '../getHeaders'; @@ -10,12 +10,12 @@ export default async ( queryVariables, fetchPolicy, }: { - query: DocumentNode, + query: string, queryVariables?: QUERY_VARIABLES, fetchPolicy?: FetchPolicy, }, ): Promise> => context.client.query({ - query, + query: gql`${query}`, variables: { ...queryVariables }, fetchPolicy: fetchPolicy || 'no-cache', context: { diff --git a/packages/api-client/src/types/API.ts b/packages/api-client/src/types/API.ts index a01a1f4dd..debc3e52d 100644 --- a/packages/api-client/src/types/API.ts +++ b/packages/api-client/src/types/API.ts @@ -1,5 +1,5 @@ import { ApolloQueryResult, FetchPolicy, FetchResult } from '@apollo/client/core'; -import { DocumentNode, ExecutionResult } from 'graphql'; +import { ExecutionResult } from 'graphql'; import { CustomQuery } from '@vue-storefront/core'; import { AddConfigurableProductsToCartInput, @@ -279,13 +279,13 @@ export interface MagentoApiMethods { ): Promise>; customQuery(params: { - query: DocumentNode, + query: string, queryVariables?: QUERY_VARIABLES, fetchPolicy?: FetchPolicy, }): Promise>; customMutation(params: { - mutation: DocumentNode, + mutation: string, mutationVariables: MUTATION_VARIABLES, fetchPolicy?: Extract, }): Promise>; diff --git a/packages/theme/components/TopBar/checkStoresAndCurrency.gql.ts b/packages/theme/components/TopBar/checkStoresAndCurrency.gql.ts index 55536b414..5dbc99ddf 100644 --- a/packages/theme/components/TopBar/checkStoresAndCurrency.gql.ts +++ b/packages/theme/components/TopBar/checkStoresAndCurrency.gql.ts @@ -1,6 +1,4 @@ -import gql from 'graphql-tag'; - -export default gql` +export default ` query getStoresAndCurrencies { availableStores { store_code diff --git a/packages/theme/composables/useApi/index.ts b/packages/theme/composables/useApi/index.ts index e6782b031..9ad887848 100644 --- a/packages/theme/composables/useApi/index.ts +++ b/packages/theme/composables/useApi/index.ts @@ -1,5 +1,4 @@ import { useContext } from '@nuxtjs/composition-api'; -import type { DocumentNode } from 'graphql'; import { Logger } from '~/helpers/logger'; export type FetchPolicy = 'cache-first' | 'network-only' | 'cache-only' | 'no-cache' | 'standby'; @@ -19,7 +18,7 @@ export type Error = { }; export type Request = ( - request: DocumentNode, + request: string, variables?: VARIABLES, fetchPolicy?: FetchPolicy, ) => Promise<{ data: DATA, errors: Error[] }>; @@ -75,10 +74,6 @@ export interface UseApiInterface { mutate: Request; } -function getGqlString(doc: DocumentNode) { - return doc.loc && doc.loc.source.body; -} - /** * Allows executing arbitrary GraphQL queries and mutations. * @@ -93,7 +88,7 @@ export function useApi(): UseApiInterface { variables, ) => { const reqID = `id${Math.random().toString(16).slice(2)}`; - Logger.debug(`customQuery/request/${reqID}`, getGqlString(request)); + Logger.debug(`customQuery/request/${reqID}`, request); const { data, errors } = await context.app.$vsf.$magento.api.customQuery({ query: request, queryVariables: variables }); Logger.debug(`customQuery/result/${reqID}`, { data, errors }); @@ -106,7 +101,7 @@ export function useApi(): UseApiInterface { variables, ) => { const reqID = `id${Math.random().toString(16).slice(2)}`; - Logger.debug(`customQuery/request/${reqID}`, getGqlString(request)); + Logger.debug(`customQuery/request/${reqID}`, request); const { data, errors } = await context.app.$vsf.$magento.api.customMutation({ mutation: request, mutationVariables: variables }); Logger.debug(`customQuery/result/${reqID}`, { data, errors }); diff --git a/packages/theme/modules/catalog/category/components/cms/categoryContent.gql.ts b/packages/theme/modules/catalog/category/components/cms/categoryContent.gql.ts index 88c745af2..dce23a8e0 100644 --- a/packages/theme/modules/catalog/category/components/cms/categoryContent.gql.ts +++ b/packages/theme/modules/catalog/category/components/cms/categoryContent.gql.ts @@ -1,6 +1,4 @@ -import gql from 'graphql-tag'; - -export default gql` +export default ` query getCategoryContentData($filters: CategoryFilterInput) { categoryList(filters: $filters) { uid diff --git a/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts b/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts index 573a1404e..c0cedc9b9 100644 --- a/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts +++ b/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts @@ -1,6 +1,4 @@ -import gql from 'graphql-tag'; - -export default gql` +export default ` query getProductFiltersByCategory($categoryIdFilter: FilterEqualTypeInput!) { products(filter: { category_uid: $categoryIdFilter }) { aggregations { diff --git a/packages/theme/modules/catalog/category/composables/useFacet/getFacetData.gql.ts b/packages/theme/modules/catalog/category/composables/useFacet/getFacetData.gql.ts index 4a9a821c1..41073bdf8 100644 --- a/packages/theme/modules/catalog/category/composables/useFacet/getFacetData.gql.ts +++ b/packages/theme/modules/catalog/category/composables/useFacet/getFacetData.gql.ts @@ -1,10 +1,8 @@ -import gql from 'graphql-tag'; - /** * GraphQL Query that fetches products using received search term and the params * for filter, sort and pagination. */ -export default gql` +export default ` query getFacetData($search: String = "", $filter: ProductAttributeFilterInput, $pageSize: Int = 10, $currentPage: Int = 1, $sort: ProductAttributeSortInput) { products(search: $search, filter: $filter, pageSize: $pageSize, currentPage: $currentPage, sort: $sort) { items { diff --git a/packages/theme/modules/catalog/category/stores/graphql/categoryList.gql.ts b/packages/theme/modules/catalog/category/stores/graphql/categoryList.gql.ts index 1435131eb..aa5582c55 100644 --- a/packages/theme/modules/catalog/category/stores/graphql/categoryList.gql.ts +++ b/packages/theme/modules/catalog/category/stores/graphql/categoryList.gql.ts @@ -1,6 +1,4 @@ -import gql from 'graphql-tag'; - -const fragmentCategory = gql` +const fragmentCategory = ` fragment CategoryFields on CategoryTree { is_anchor name @@ -13,7 +11,7 @@ const fragmentCategory = gql` } `; -export default gql` +export default ` query categoryList { categories { items { diff --git a/packages/theme/modules/catalog/pricing/getPricesQuery.gql.ts b/packages/theme/modules/catalog/pricing/getPricesQuery.gql.ts index 441fa1221..771ec3551 100644 --- a/packages/theme/modules/catalog/pricing/getPricesQuery.gql.ts +++ b/packages/theme/modules/catalog/pricing/getPricesQuery.gql.ts @@ -1,6 +1,4 @@ -import gql from 'graphql-tag'; - -export default gql` +export default ` query productsList($search: String = "", $filter: ProductAttributeFilterInput, $pageSize: Int = 20, $currentPage: Int = 1, $sort: ProductAttributeSortInput) { products(search: $search, filter: $filter, pageSize: $pageSize, currentPage: $currentPage, sort: $sort) { items { diff --git a/packages/theme/modules/catalog/product/queries/getProductPriceBySku.gql.ts b/packages/theme/modules/catalog/product/queries/getProductPriceBySku.gql.ts new file mode 100644 index 000000000..9e98d3525 --- /dev/null +++ b/packages/theme/modules/catalog/product/queries/getProductPriceBySku.gql.ts @@ -0,0 +1,125 @@ +const fragmentPriceRangeFields = ` + fragment PriceRangeFields on PriceRange { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } +`; + +export default ` + query getProductPriceBySku($sku: String) { + products(filter: {sku: {eq: $sku}}) { + items { + price_range { + ...PriceRangeFields + } + + ... on BundleProduct { + items { + position + required + sku + title + type + uid + options { + can_change_quantity + is_default + position + uid + quantity + product { + uid + sku + name + price_range { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + } + } + } + } + + ... on GroupedProduct { + items { + position + qty + product { + uid + sku + name + stock_status + only_x_left_in_stock + price_range { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + thumbnail { + url + position + disabled + label + } + } + } + } + + } + } + } + ${fragmentPriceRangeFields} +`; diff --git a/packages/theme/package.json b/packages/theme/package.json index afb30e6fb..30b4ed55e 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -46,7 +46,6 @@ "cookie-universal-nuxt": "^2.1.5", "deepdash": "^5.3.9", "express": "4.17.3", - "graphql-tag": "^2.12.6", "is-https": "^4.0.0", "isomorphic-dompurify": "^0.18.0", "lodash.debounce": "^4.0.8", @@ -73,6 +72,7 @@ "@types/lodash.debounce": "^4.0.6", "@types/lodash.merge": "^4.6.7", "@types/lodash.unescape": "^4.0.7", + "@types/k6": "^0.37.0", "@vue/test-utils": "^1.3.0", "babel-core": "7.0.0-bridge.0", "babel-jest": "^27.4.6", diff --git a/packages/theme/plugins/query/StoreConfig.gql.ts b/packages/theme/plugins/query/StoreConfig.gql.ts index 1d36bb7bd..8bcadb908 100644 --- a/packages/theme/plugins/query/StoreConfig.gql.ts +++ b/packages/theme/plugins/query/StoreConfig.gql.ts @@ -1,7 +1,5 @@ -import gql from 'graphql-tag'; - /** GraphQL Query that fetches store configuration from the API */ -export const StoreConfigQuery = gql` +export const StoreConfigQuery = ` query storeConfig { storeConfig { store_code, diff --git a/packages/theme/tests/load/searchProduct.js b/packages/theme/tests/load/searchProduct.js new file mode 100644 index 000000000..edb21cc2e --- /dev/null +++ b/packages/theme/tests/load/searchProduct.js @@ -0,0 +1,355 @@ +// Creator: k6 Browser Recorder 0.6.2 (+ handmade cleanups) +import { sleep, group } from 'k6'; +import http from 'k6/http'; + +import jsonpath from 'https://jslib.k6.io/jsonpath/1.0.2/index.js'; + +export const options = { + vus: 10, + duration: '5m', +}; + +const { BASE_URL } = __ENV; + +export default function main() { + let response; + + const vars = {}; + + group(`page_1 - ${BASE_URL}/default`, () => { + response = http.get(`${BASE_URL}/default`); + sleep(0.5); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query storeConfig { + storeConfig { + store_code, + default_title, + store_name, + default_display_currency_code, + locale, + header_logo_src, + logo_width, + logo_height, + logo_alt + } + } +"}]`, + ); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query getStoresAndCurrencies { + availableStores { + store_code + } + currency { + available_currency_codes + } + } +"}]`, + ); + sleep(2.6); + + response = http.post( + `${BASE_URL}/api/magento/route`, + '["/women/tops-women/jackets-women.html",null]', + ); + + [vars.uid1] = jsonpath.query(response.json(), '$.data.route.uid'); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query categoryList { + categories { + items { + ...CategoryFields + children { + ...CategoryFields + children { + ...CategoryFields + children { + ...CategoryFields + } + } + } + } + } + } + + fragment CategoryFields on CategoryTree { + is_anchor + name + position + product_count + uid + url_path + url_suffix + include_in_menu + } + +"}]`, + ); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query getCategoryContentData($filters: CategoryFilterInput) { + categoryList(filters: $filters) { + uid + display_mode + landing_page + cms_block { + identifier + content + } + } + } +","queryVariables":{"filters":{"category_uid":{"eq":"${vars.uid1}"}}}}]`, + ); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query getFacetData($search: String = \\"\\", $filter: ProductAttributeFilterInput, $pageSize: Int = 10, $currentPage: Int = 1, $sort: ProductAttributeSortInput) { + products(search: $search, filter: $filter, pageSize: $pageSize, currentPage: $currentPage, sort: $sort) { + items { + __typename + uid + sku + name + stock_status + only_x_left_in_stock + thumbnail { + url + position + disabled + label + } + url_key + url_rewrites { + url + } + price_range { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + } + page_info { + current_page + page_size + total_pages + } + total_count + } + } +","queryVariables":{"pageSize":10,"search":"","filter":{"category_uid":{"in":["${vars.uid1}"]}},"sort":{},"currentPage":1}}]`, + ); + sleep(0.5); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query getProductFiltersByCategory($categoryIdFilter: FilterEqualTypeInput!) { + products(filter: { category_uid: $categoryIdFilter }) { + aggregations { + label + count + attribute_code + options { + count + label + value + __typename + } + position + __typename + } + __typename + } + } +","queryVariables":{"categoryIdFilter":{"eq":"${vars.uid1}"}}}]`, + ); + sleep(1.8); + + response = http.post( + `${BASE_URL}/api/magento/productDetail`, + '[{"filter":{"sku":{"eq":"WJ12"}},"configurations":[]},{"productDetail":"productDetail"}]', + ); + + response = http.post( + `${BASE_URL}/api/magento/customQuery`, + `[{"query":" + query getProductPriceBySku($sku: String) { + products(filter: {sku: {eq: $sku}}) { + items { + price_range { + ...PriceRangeFields + } + + ... on BundleProduct { + items { + position + required + sku + title + type + uid + options { + can_change_quantity + is_default + position + uid + quantity + product { + uid + sku + name + price_range { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + } + } + } + } + + ... on GroupedProduct { + items { + position + qty + product { + uid + sku + name + stock_status + only_x_left_in_stock + price_range { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + thumbnail { + url + position + disabled + label + } + } + } + } + + } + } + } + + fragment PriceRangeFields on PriceRange { + maximum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + minimum_price { + final_price { + currency + value + } + regular_price { + currency + value + } + } + } + +","queryVariables":{"sku":"WJ12"}}]`, + ); + + response = http.post( + `${BASE_URL}/api/magento/upsellProduct`, + '[{"filter":{"sku":{"eq":"WJ12"}}},null]', + ); + + response = http.post( + `${BASE_URL}/api/magento/relatedProduct`, + '[{"filter":{"sku":{"eq":"WJ12"}}},null]', + ); + + response = http.post( + `${BASE_URL}/api/magento/productReviewRatingsMetadata`, + '[null]', + ); + sleep(2.3); + + response = http.post( + `${BASE_URL}/api/magento/productReview`, + '[{"filter":{"sku":{"eq":"WJ12"}}},null]', + ); + + response = http.post( + `${BASE_URL}/api/magento/productReviewRatingsMetadata`, + '[null]', + ); + }); +} diff --git a/yarn.lock b/yarn.lock index 23b4da8fd..8511a95b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3938,6 +3938,11 @@ dependencies: "@types/node" "*" +"@types/k6@^0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@types/k6/-/k6-0.37.0.tgz#fb357d5218c8c2b5bc197dae818eebe7085906fc" + integrity sha512-8ou8tK3i/8HJFiyxz9ktrT1CWvtdItlq5ybaysnnwX7wSNLZv3TdR28Q2kW9byYVJKNhuwOnoecB2oM/NytN2Q== + "@types/less@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.2.tgz#2761d477678c8374cb9897666871662eb1d1115e"