From e5a287a321c9cb790d854fc2855a62a71de546a3 Mon Sep 17 00:00:00 2001 From: Sujith Date: Sat, 3 Dec 2022 00:16:10 +0530 Subject: [PATCH 01/11] feat: filterSearch initial version Signed-off-by: Sujith --- apps/reaction/plugins.json | 3 +- packages/api-core/src/graphql/schema.graphql | 115 ++++ .../src/queries/filterSearchAccounts.js | 27 + .../src/queries/filterSearchCustomers.js | 28 + .../api-plugin-accounts/src/queries/index.js | 4 + .../resolvers/Query/filterSearchAccounts.js | 37 ++ .../resolvers/Query/filterSearchCustomers.js | 37 ++ .../src/resolvers/Query/index.js | 4 + .../src/schemas/account.graphql | 78 +++ .../src/queries/filterSearchOrders.js | 28 + .../api-plugin-orders/src/queries/index.js | 2 + .../src/resolvers/Query/filterSearchOrders.js | 37 ++ .../src/resolvers/Query/index.js | 2 + .../src/schemas/schema.graphql | 39 ++ .../src/queries/filterSearchProducts.js | 28 + .../api-plugin-products/src/queries/index.js | 2 + .../resolvers/Query/filterSearchProducts.js | 37 ++ .../src/resolvers/Query/index.js | 4 +- .../src/schemas/product.graphql | 39 ++ packages/api-utils/lib/generateFilterQuery.js | 619 ++++++++++++++++++ packages/api-utils/package.json | 3 +- pnpm-lock.yaml | 2 + 22 files changed, 1172 insertions(+), 3 deletions(-) create mode 100644 packages/api-plugin-accounts/src/queries/filterSearchAccounts.js create mode 100644 packages/api-plugin-accounts/src/queries/filterSearchCustomers.js create mode 100644 packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js create mode 100644 packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js create mode 100644 packages/api-plugin-orders/src/queries/filterSearchOrders.js create mode 100644 packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js create mode 100644 packages/api-plugin-products/src/queries/filterSearchProducts.js create mode 100644 packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js create mode 100644 packages/api-utils/lib/generateFilterQuery.js diff --git a/apps/reaction/plugins.json b/apps/reaction/plugins.json index 429aa5fd64e..4811e730cfd 100644 --- a/apps/reaction/plugins.json +++ b/apps/reaction/plugins.json @@ -35,5 +35,6 @@ "navigation": "@reactioncommerce/api-plugin-navigation", "sitemapGenerator": "@reactioncommerce/api-plugin-sitemap-generator", "notifications": "@reactioncommerce/api-plugin-notifications", - "addressValidationTest": "@reactioncommerce/api-plugin-address-validation-test" + "addressValidationTest": "@reactioncommerce/api-plugin-address-validation-test", + "sampleData": "../../packages/api-plugin-sample-data/index.js" } diff --git a/packages/api-core/src/graphql/schema.graphql b/packages/api-core/src/graphql/schema.graphql index 1d3fb6633b0..f4c5e8d262e 100644 --- a/packages/api-core/src/graphql/schema.graphql +++ b/packages/api-core/src/graphql/schema.graphql @@ -34,6 +34,121 @@ enum MassUnit { oz } +"Logical Operator Types for filtering" +enum LogOpTypes{ + "AND" + AND + + "OR" + OR +} + +"Relational Operator Types for filtering" +enum RelOpTypes{ + "Equal" + eq + + "Not Equal" + ne + + "Greater Than" + gt + + "Greater Than or Equal" + gte + + "Less Than" + lt + + "Less Than or Equal" + lte + + "Begins With used with String types" + beginsWith + + "Ends With used with String types" + endsWith + + "In" + in + + "Not In" + nin + + "Regex" + regex +} + +"Flag to specify the number of levels used in Filter Search" +enum FilterLevels { + "Use Filter with 1 level" + ONE + + "Use Filter with 2 levels" + TWO + + "Use Filter with 3 levels" + THREE +} + +"Single Condition for filterSearch" +input SingleConditionInput { + "Field name" + key : String! + + "Value to filter if it is String input" + stringValue: String + + "Value to filter if it is Int input" + intValue: Int + + "Value to filter if it is Float input" + floatValue: Float + + "Value to filter if it is Boolean input" + boolValue: Boolean + + "Value to filter if it is Date input" + dateValue: DateTime + + "Value to filter if it is String Array input" + stringArrayValue: [String] + + "Value to filter if it is Int Array input" + intArrayValue: [Int] + + "Value to filter if it is Float Array input" + floatArrayValue: [Float] + + "Relational Operator to join the key and value" + relOper: RelOpTypes! + + "Logical NOT operator to negate the condition" + logNOT: Boolean + + "Flag to set if the regex is case insensitive" + caseSensitive: Boolean +} + +"Filter search with Three levels of input" +input FilterThreeLevelInput { + all: [FilterTwoLevelInput] + any: [FilterTwoLevelInput] +} + +"Filter search with Two levels of input" +input FilterTwoLevelInput { + all: [FilterOneLevelInput] + any: [FilterOneLevelInput] +} + +"Filter search with One level of input" +input FilterOneLevelInput { + all: [SingleConditionInput] + any: [SingleConditionInput] +} + + "A list of URLs for various sizes of an image" type ImageSizes { "Use this URL to get a large resolution file for this image" diff --git a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js b/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js new file mode 100644 index 00000000000..b055a7bff91 --- /dev/null +++ b/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js @@ -0,0 +1,27 @@ +import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; + +/** + * @name filterSearchAccounts + * @method + * @memberof GraphQL/Accounts + * @summary Query the Accounts collection for a list of customers/accounts + * @param {Object} context - an object containing the per-request state + * @param {Object} filter1level - an object containing ONE level of filters to apply + * @param {Object} filter2level - an object containing TWO levels of filters to apply + * @param {Object} filter3level - an object containing THREE levels of filters to apply + * @param {String} level - number of levels used in filter object + * @param {String} shopId - shopID to filter by + * @returns {Promise} Accounts object Promise + */ +export default async function filterSearchAccounts(context, filter1level, filter2level, filter3level, level, shopId) { + const { collections: { Accounts } } = context; + + if (!shopId) { + throw new Error("shopId is required"); + } + await context.validatePermissions("reaction:legacy:accounts", "read", { shopId }); + + const { filterQuery } = generateFilterQuery(context, "Account", filter1level, filter2level, filter3level, level, shopId); + + return Accounts.find(filterQuery); +} diff --git a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js b/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js new file mode 100644 index 00000000000..04ae06bdec7 --- /dev/null +++ b/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js @@ -0,0 +1,28 @@ +import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; + +/** + * @name filterSearchCustomers + * @method + * @memberof GraphQL/Customers + * @summary Query the Accounts collection for a list of customers/accounts + * @param {Object} context - an object containing the per-request state + * @param {Object} filter1level - an object containing ONE level of filters to apply + * @param {Object} filter2level - an object containing TWO levels of filters to apply + * @param {Object} filter3level - an object containing THREE levels of filters to apply + * @param {String} level - number of levels used in filter object + * @param {String} shopId - shopID to filter by + * @returns {Promise} Accounts object Promise + */ +export default async function filterSearchCustomers(context, filter1level, filter2level, filter3level, level, shopId) { + const { collections: { Accounts } } = context; + + if (!shopId) { + throw new Error("shopId is required"); + } + await context.validatePermissions("reaction:legacy:accounts", "read", { shopId }); + + const { filterQuery } = generateFilterQuery(context, "Account", filter1level, filter2level, filter3level, level, shopId); + + filterQuery.groups = { $in: [null, []] }; // filter out non-customer accounts + return Accounts.find(filterQuery); +} diff --git a/packages/api-plugin-accounts/src/queries/index.js b/packages/api-plugin-accounts/src/queries/index.js index 0ac5fb9d56b..d2db6ec012e 100644 --- a/packages/api-plugin-accounts/src/queries/index.js +++ b/packages/api-plugin-accounts/src/queries/index.js @@ -7,8 +7,12 @@ import groupsByAccount from "./groupsByAccount.js"; import groupsById from "./groupsById.js"; import invitations from "./invitations.js"; import userAccount from "./userAccount.js"; +import filterSearchAccounts from "./filterSearchAccounts.js"; +import filterSearchCustomers from "./filterSearchCustomers.js"; export default { + filterSearchAccounts, + filterSearchCustomers, accountByUserId, accounts, customers, diff --git a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js new file mode 100644 index 00000000000..d2f4c09fbd8 --- /dev/null +++ b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js @@ -0,0 +1,37 @@ +import getPaginatedResponse from "@reactioncommerce/api-utils/graphql/getPaginatedResponse.js"; +import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldRequested.js"; + +/** + * @name Query/accounts + * @method + * @memberof Accounts/Query + * @summary Query for a list of accounts + * @param {Object} _ - unused + * @param {Object} args - an object of all arguments that were sent by the client + * @param {String} args.shopId - id of shop to query + * @param {Object} args.filter1level - filter conditions with 1 level + * @param {Object} args.filter2level - filter conditions with 2 levels + * @param {Object} args.filter3level - filter conditions with 3 levels + * @param {String} args.level - filter level used + * @param {Object} context - an object containing the per-request state + * @param {Object} info Info about the GraphQL request + * @returns {Promise} Accounts + */ +export default async function filterSearchAccounts(_, args, context, info) { + const { + shopId, + filter1level, + filter2level, + filter3level, + level, + ...connectionArgs + } = args; + + const query = await context.queries.filterSearchAccounts(context, filter1level, filter2level, filter3level, level, shopId); + + return getPaginatedResponse(query, connectionArgs, { + includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), + includeHasPreviousPage: wasFieldRequested("pageInfo.hasPreviousPage", info), + includeTotalCount: wasFieldRequested("totalCount", info) + }); +} diff --git a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js new file mode 100644 index 00000000000..5afdac8bb82 --- /dev/null +++ b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js @@ -0,0 +1,37 @@ +import getPaginatedResponse from "@reactioncommerce/api-utils/graphql/getPaginatedResponse.js"; +import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldRequested.js"; + +/** + * @name Query/accounts + * @method + * @memberof Customers/Query + * @summary Query for a list of customers + * @param {Object} _ - unused + * @param {Object} args - an object of all arguments that were sent by the client + * @param {String} args.shopId - id of shop to query + * @param {Object} args.filter1level - filter conditions with 1 level + * @param {Object} args.filter2level - filter conditions with 2 levels + * @param {Object} args.filter3level - filter conditions with 3 levels + * @param {String} args.level - filter level used + * @param {Object} context - an object containing the per-request state + * @param {Object} info Info about the GraphQL request + * @returns {Promise} Accounts + */ +export default async function filterSearchCustomers(_, args, context, info) { + const { + shopId, + filter1level, + filter2level, + filter3level, + level, + ...connectionArgs + } = args; + + const query = await context.queries.filterSearchCustomers(context, filter1level, filter2level, filter3level, level, shopId); + + return getPaginatedResponse(query, connectionArgs, { + includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), + includeHasPreviousPage: wasFieldRequested("pageInfo.hasPreviousPage", info), + includeTotalCount: wasFieldRequested("totalCount", info) + }); +} diff --git a/packages/api-plugin-accounts/src/resolvers/Query/index.js b/packages/api-plugin-accounts/src/resolvers/Query/index.js index 2676a0568a3..41c93611511 100644 --- a/packages/api-plugin-accounts/src/resolvers/Query/index.js +++ b/packages/api-plugin-accounts/src/resolvers/Query/index.js @@ -5,8 +5,12 @@ import group from "./group.js"; import groups from "./groups.js"; import invitations from "./invitations.js"; import viewer from "./viewer.js"; +import filterSearchAccounts from "./filterSearchAccounts.js"; +import filterSearchCustomers from "./filterSearchCustomers.js"; export default { + filterSearchAccounts, + filterSearchCustomers, account, accounts, customers, diff --git a/packages/api-plugin-accounts/src/schemas/account.graphql b/packages/api-plugin-accounts/src/schemas/account.graphql index 45c6e91f150..ece2bd3fc28 100644 --- a/packages/api-plugin-accounts/src/schemas/account.graphql +++ b/packages/api-plugin-accounts/src/schemas/account.graphql @@ -496,6 +496,84 @@ extend type Query { id: ID! ): Account + # "filterSearch Query for list of Accounts" + filterSearchAccounts( + "Shop ID" + shopId: ID!, + + "fliterSearch Conditions with 3 levels" + filter3level: FilterThreeLevelInput, + + "fliterSearch Conditions with 2 levels" + filter2level: FilterTwoLevelInput, + + "fliterSearch Conditions with 1 level" + filter1level: FilterOneLevelInput, + + "fliterSearch level used specifier" + level: FilterLevels!, + + "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." + after: ConnectionCursor, + + "Return only results that come before this cursor. Use this with `last` to specify the number of results to return." + before: ConnectionCursor, + + "Return at most this many results. This parameter may be used with either `after` or `offset` parameters." + first: ConnectionLimitInt, + + "Return at most this many results. This parameter may be used with the `before` parameter." + last: ConnectionLimitInt, + + "Return only results that come after the Nth result. This parameter may be used with the `first` parameter." + offset: Int + + "Return results sorted in this order" + sortOrder: SortOrder = desc, + + "By default, accounts are sorted by createdAt. Set this to sort by one of the other allowed fields" + sortBy: AccountSortByField = createdAt + ): AccountConnection + + # "filterSearch Query for list of Customers" + filterSearchCustomers( + "Shop ID" + shopId: ID!, + + "fliterSearch Conditions with 3 levels" + filter3level: FilterThreeLevelInput, + + "fliterSearch Conditions with 2 levels" + filter2level: FilterTwoLevelInput, + + "fliterSearch Conditions with 1 level" + filter1level: FilterOneLevelInput, + + "fliterSearch level used specifier" + level: FilterLevels!, + + "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." + after: ConnectionCursor, + + "Return only results that come before this cursor. Use this with `last` to specify the number of results to return." + before: ConnectionCursor, + + "Return at most this many results. This parameter may be used with either `after` or `offset` parameters." + first: ConnectionLimitInt, + + "Return at most this many results. This parameter may be used with the `before` parameter." + last: ConnectionLimitInt, + + "Return only results that come after the Nth result. This parameter may be used with the `first` parameter." + offset: Int + + "Return results sorted in this order" + sortOrder: SortOrder = desc, + + "By default, customers are sorted by createdAt. Set this to sort by one of the other allowed fields" + sortBy: AccountSortByField = createdAt + ): AccountConnection + "Returns accounts optionally filtered by account groups" accounts( "Return only accounts in any of these groups" diff --git a/packages/api-plugin-orders/src/queries/filterSearchOrders.js b/packages/api-plugin-orders/src/queries/filterSearchOrders.js new file mode 100644 index 00000000000..141e7444ede --- /dev/null +++ b/packages/api-plugin-orders/src/queries/filterSearchOrders.js @@ -0,0 +1,28 @@ +import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; + +/** + * @name filterSearchOrders + * @method + * @memberof GraphQL/Orders + * @summary Query the Orders collection for a list of orders + * @param {Object} context - an object containing the per-request state + * @param {Object} filter1level - an object containing ONE level of filters to apply + * @param {Object} filter2level - an object containing TWO levels of filters to apply + * @param {Object} filter3level - an object containing THREE levels of filters to apply + * @param {String} level - number of levels used in filter object + * @param {String} shopId - shopID to filter by + * @returns {Promise} Orders object Promise + */ +export default async function filterSearchOrders(context, filter1level, filter2level, filter3level, level, shopId) { + const { collections: { Orders } } = context; + + if (!shopId) { + throw new Error("shopId is required"); + } + + await context.validatePermissions("reaction:legacy:orders", "read", { shopId }); + + const { filterQuery } = generateFilterQuery(context, "Order", filter1level, filter2level, filter3level, level, shopId); + + return Orders.find(filterQuery); +} diff --git a/packages/api-plugin-orders/src/queries/index.js b/packages/api-plugin-orders/src/queries/index.js index d86caab24bb..b32482056e2 100644 --- a/packages/api-plugin-orders/src/queries/index.js +++ b/packages/api-plugin-orders/src/queries/index.js @@ -4,8 +4,10 @@ import orders from "./orders.js"; import ordersByAccountId from "./ordersByAccountId.js"; import refunds from "./refunds.js"; import refundsByPaymentId from "./refundsByPaymentId.js"; +import filterSearchOrders from "./filterSearchOrders.js"; export default { + filterSearchOrders, orderById, orderByReferenceId, orders, diff --git a/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js b/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js new file mode 100644 index 00000000000..ff3612a1547 --- /dev/null +++ b/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js @@ -0,0 +1,37 @@ +import getPaginatedResponse from "@reactioncommerce/api-utils/graphql/getPaginatedResponse.js"; +import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldRequested.js"; + +/** + * @name Query/orders + * @method + * @memberof Orders/Query + * @summary Query for a list of orders + * @param {Object} _ - unused + * @param {Object} args - an object of all arguments that were sent by the client + * @param {String} args.shopId - id of shop to query + * @param {Object} args.filter1level - filter conditions with 1 level + * @param {Object} args.filter2level - filter conditions with 2 levels + * @param {Object} args.filter3level - filter conditions with 3 levels + * @param {String} args.level - filter level used + * @param {Object} context - an object containing the per-request state + * @param {Object} info Info about the GraphQL request + * @returns {Promise} Orders + */ +export default async function filterSearchOrders(_, args, context, info) { + const { + shopId, + filter1level, + filter2level, + filter3level, + level, + ...connectionArgs + } = args; + + const query = await context.queries.filterSearchOrders(context, filter1level, filter2level, filter3level, level, shopId); + + return getPaginatedResponse(query, connectionArgs, { + includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), + includeHasPreviousPage: wasFieldRequested("pageInfo.hasPreviousPage", info), + includeTotalCount: wasFieldRequested("totalCount", info) + }); +} diff --git a/packages/api-plugin-orders/src/resolvers/Query/index.js b/packages/api-plugin-orders/src/resolvers/Query/index.js index d86caab24bb..b32482056e2 100644 --- a/packages/api-plugin-orders/src/resolvers/Query/index.js +++ b/packages/api-plugin-orders/src/resolvers/Query/index.js @@ -4,8 +4,10 @@ import orders from "./orders.js"; import ordersByAccountId from "./ordersByAccountId.js"; import refunds from "./refunds.js"; import refundsByPaymentId from "./refundsByPaymentId.js"; +import filterSearchOrders from "./filterSearchOrders.js"; export default { + filterSearchOrders, orderById, orderByReferenceId, orders, diff --git a/packages/api-plugin-orders/src/schemas/schema.graphql b/packages/api-plugin-orders/src/schemas/schema.graphql index c0d935743ef..d68f4207b57 100644 --- a/packages/api-plugin-orders/src/schemas/schema.graphql +++ b/packages/api-plugin-orders/src/schemas/schema.graphql @@ -1,4 +1,43 @@ extend type Query { + # "filterSearch Query for list of Orders" + filterSearchOrders( + "Shop ID" + shopId: ID!, + + "fliterSearch Conditions with 3 levels" + filter3level: FilterThreeLevelInput, + + "fliterSearch Conditions with 2 levels" + filter2level: FilterTwoLevelInput, + + "fliterSearch Conditions with 1 level" + filter1level: FilterOneLevelInput, + + "fliterSearch level used specifier" + level: FilterLevels!, + + "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." + after: ConnectionCursor, + + "Return only results that come before this cursor. Use this with `last` to specify the number of results to return." + before: ConnectionCursor, + + "Return at most this many results. This parameter may be used with either `after` or `offset` parameters." + first: ConnectionLimitInt, + + "Return at most this many results. This parameter may be used with the `before` parameter." + last: ConnectionLimitInt, + + "Return only results that come after the Nth result. This parameter may be used with the `first` parameter." + offset: Int + + "Return results sorted in this order" + sortOrder: SortOrder = desc, + + "By default, orders are sorted by createdAt. Set this to sort by one of the other allowed fields" + sortBy: OrdersSortByField = createdAt + ): OrderConnection + "Get an order by its ID" orderById( "The order ID" diff --git a/packages/api-plugin-products/src/queries/filterSearchProducts.js b/packages/api-plugin-products/src/queries/filterSearchProducts.js new file mode 100644 index 00000000000..33ec37f3c97 --- /dev/null +++ b/packages/api-plugin-products/src/queries/filterSearchProducts.js @@ -0,0 +1,28 @@ +import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; + +/** + * @name filterSearchProducts + * @method + * @memberof GraphQL/Products + * @summary Query the Products collection for a list of products + * @param {Object} context - an object containing the per-request state + * @param {Object} filter1level - an object containing ONE level of filters to apply + * @param {Object} filter2level - an object containing TWO levels of filters to apply + * @param {Object} filter3level - an object containing THREE levels of filters to apply + * @param {String} level - number of levels used in filter object + * @param {String} shopId - shopID to filter by + * @returns {Promise} Products object Promise + */ +export default async function filterSearchProducts(context, filter1level, filter2level, filter3level, level, shopId) { + const { collections: { Products } } = context; + + if (!shopId) { + throw new Error("shopId is required"); + } + + await context.validatePermissions("reaction:legacy:products", "read", { shopId }); + + const { filterQuery } = generateFilterQuery(context, "Product", filter1level, filter2level, filter3level, level, shopId); + + return Products.find(filterQuery); +} diff --git a/packages/api-plugin-products/src/queries/index.js b/packages/api-plugin-products/src/queries/index.js index 10a623e8f7e..022a30b6a5e 100644 --- a/packages/api-plugin-products/src/queries/index.js +++ b/packages/api-plugin-products/src/queries/index.js @@ -1,7 +1,9 @@ import product from "./product.js"; import products from "./products.js"; +import filterSearchProducts from "./filterSearchProducts.js"; export default { + filterSearchProducts, product, products }; diff --git a/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js b/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js new file mode 100644 index 00000000000..6ab0f01ead7 --- /dev/null +++ b/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js @@ -0,0 +1,37 @@ +import getPaginatedResponse from "@reactioncommerce/api-utils/graphql/getPaginatedResponse.js"; +import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldRequested.js"; + +/** + * @name Query/products + * @method + * @memberof Products/Query + * @summary Query for a list of products + * @param {Object} _ - unused + * @param {Object} args - an object of all arguments that were sent by the client + * @param {String} args.shopId - id of shop to query + * @param {Object} args.filter1level - filter conditions with 1 level + * @param {Object} args.filter2level - filter conditions with 2 levels + * @param {Object} args.filter3level - filter conditions with 3 levels + * @param {String} args.level - filter level used + * @param {Object} context - an object containing the per-request state + * @param {Object} info Info about the GraphQL request + * @returns {Promise} Products + */ +export default async function filterSearchProducts(_, args, context, info) { + const { + shopId, + filter1level, + filter2level, + filter3level, + level, + ...connectionArgs + } = args; + + const query = await context.queries.filterSearchProducts(context, filter1level, filter2level, filter3level, level, shopId); + + return getPaginatedResponse(query, connectionArgs, { + includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), + includeHasPreviousPage: wasFieldRequested("pageInfo.hasPreviousPage", info), + includeTotalCount: wasFieldRequested("totalCount", info) + }); +} diff --git a/packages/api-plugin-products/src/resolvers/Query/index.js b/packages/api-plugin-products/src/resolvers/Query/index.js index 10a623e8f7e..6c21521fab4 100644 --- a/packages/api-plugin-products/src/resolvers/Query/index.js +++ b/packages/api-plugin-products/src/resolvers/Query/index.js @@ -1,7 +1,9 @@ import product from "./product.js"; import products from "./products.js"; +import filterSearchProducts from "./filterSearchProducts.js"; export default { product, - products + products, + filterSearchProducts }; diff --git a/packages/api-plugin-products/src/schemas/product.graphql b/packages/api-plugin-products/src/schemas/product.graphql index 490816860d9..1211d54137a 100644 --- a/packages/api-plugin-products/src/schemas/product.graphql +++ b/packages/api-plugin-products/src/schemas/product.graphql @@ -625,6 +625,45 @@ extend type Mutation { } extend type Query { + # "filterSearch Query for list of Products" + filterSearchProducts( + "Shop ID" + shopId: ID!, + + "fliterSearch Conditions with 3 levels" + filter3level: FilterThreeLevelInput, + + "fliterSearch Conditions with 2 levels" + filter2level: FilterTwoLevelInput, + + "fliterSearch Conditions with 1 level" + filter1level: FilterOneLevelInput, + + "fliterSearch level used specifier" + level: FilterLevels!, + + "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." + after: ConnectionCursor, + + "Return only results that come before this cursor. Use this with `last` to specify the number of results to return." + before: ConnectionCursor, + + "Return at most this many results. This parameter may be used with either `after` or `offset` parameters." + first: ConnectionLimitInt, + + "Return at most this many results. This parameter may be used with the `before` parameter." + last: ConnectionLimitInt, + + "Return only results that come after the Nth result. This parameter may be used with the `first` parameter." + offset: Int + + "Return results sorted in this order" + sortOrder: SortOrder = desc, + + "By default, products are sorted by createdAt. Set this to sort by one of the other allowed fields" + sortBy: ProductSortByField = createdAt + ): ProductConnection + "Query for a single Product" product( "Product ID" diff --git a/packages/api-utils/lib/generateFilterQuery.js b/packages/api-utils/lib/generateFilterQuery.js new file mode 100644 index 00000000000..0ff3da64264 --- /dev/null +++ b/packages/api-utils/lib/generateFilterQuery.js @@ -0,0 +1,619 @@ +import SimpleSchema from "simpl-schema"; +import _ from "lodash"; + +const SingleConditionSchema = new SimpleSchema({ + "key": { + type: String + }, + "stringValue": { + type: String, + optional: true + }, + "intValue": { + type: SimpleSchema.Integer, + optional: true + }, + "floatValue": { + type: Number, + optional: true + }, + "boolValue": { + type: Boolean, + optional: true + }, + "dateValue": { + type: Date, + optional: true + }, + "stringArrayValue": { + type: Array, + optional: true + }, + "stringArrayValue.$": { + type: String + }, + "intArrayValue": { + type: Array, + optional: true + }, + "intArrayValue.$": { + type: SimpleSchema.Integer + }, + "floatArrayValue": { + type: Array, + optional: true + }, + "floatArrayValue.$": { + type: Number + }, + "relOper": { + type: String, + allowedValues: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "beginsWith", "endsWith"] + }, + "logNOT": { + type: Boolean, + optional: true + }, + "caseSensitive": { + type: Boolean, + optional: true + } +}); + +const FilterOneLevelSchema = new SimpleSchema({ + "any": { + type: Array, + optional: true + }, + "any.$": { + type: SingleConditionSchema + }, + "all": { + type: Array, + optional: true + }, + "all.$": { + type: SingleConditionSchema + } +}); + +const FilterTwoLevelSchema = new SimpleSchema({ + "any": { + type: Array, + optional: true + }, + "any.$": { + type: FilterOneLevelSchema + }, + "all": { + type: Array, + optional: true + }, + "all.$": { + type: FilterOneLevelSchema + } +}); + +const FilterThreeLevelSchema = new SimpleSchema({ + "any": { + type: Array, + optional: true + }, + "any.$": { + type: FilterTwoLevelSchema + }, + "all": { + type: Array, + optional: true + }, + "all.$": { + type: FilterTwoLevelSchema + } +}); + + +const validCombos = { + "SimpleSchema.String": { + relOper: ["eq", "ne", "in", "nin", "regex", "beginsWith", "endsWith"], + typeOf: ["string"] + }, + "SimpleSchema.Integer": { + relOper: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"], + typeOf: ["number"] + }, + "SimpleSchema.Number": { + relOper: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"], + typeOf: ["number"] + }, + "SimpleSchema.Array": { + relOper: ["in", "nin", "eq", "ne"], + typeOf: ["array"] + }, + "SimpleSchema.Boolean": { + relOper: ["eq", "ne"], + typeOf: ["boolean"] + }, + "SimpleSchema.Date": { + relOper: ["eq", "ne", "gt", "gte", "lt", "lte"], + typeOf: ["date"] + } +}; + +const REL_OPS_KEYS = ["any", "all"]; + +const FIELD_KEYS = [ + "key", "stringValue", "boolValue", "intValue", "floatValue", "dateValue", + "stringArrayValue", "intArrayValue", "floatArrayValue", + "relOper", "caseSensitive", "logNOT" +]; + +const keyMap = { + all: "$and", + any: "$or" +}; + +/** + * @name verifyAllFieldKeys + * @method + * @memberof GraphQL/Filter + * @summary Verifies if the input array of keys are all field keys + * @param {String[]} keys - array of key to be verified + * @returns {Boolean} - verfication result + */ +function verifyAllFieldKeys(keys) { + // verify all keys in input array are valid and present in FIELD_KEYS + for (const key of keys) { + if (!FIELD_KEYS.includes(key)) { + return false; + } + } + return true; +} + +/** + * @name verifyAllRelOpKeys + * @method + * @memberof GraphQL/Filter + * @summary Verifies if the input array of keys are all Relational operator keys + * @param {String[]} keys - array of key to be verified + * @returns {Boolean} - verfication result + */ +function verifyAllRelOpKeys(keys) { + // verify all keys in input array are valid and present in REL_OPS_KEYS + for (const key of keys) { + if (!REL_OPS_KEYS.includes(key)) { + return false; + } + } + return true; +} + + +/** + * @name checkIfCompoundCondition + * @method + * @memberof GraphQL/Filter + * @summary Checks if the input filter condition is a compound condition + * @param {Object} filterQuery - condition object to be verified + * @returns {Boolean} - verfication result + */ +function checkIfCompoundCondition(filterQuery) { + const allKeys = Object.keys(filterQuery); + if (!allKeys || allKeys.length === 0) { + throw new Error("Filter condition must have at least one key"); + } + + if (allKeys.length > 1) { // compound condition will have only one key (all/any) + return false; + } + + const isTopLevelRelOpKeys = verifyAllRelOpKeys(allKeys); // verify if the key is a valid relational operator key + if (!isTopLevelRelOpKeys) { + return false; + } + + const filterConditions = filterQuery[allKeys[0]]; // get the array of filter conditions for the relational operator + if (!filterConditions || !Array.isArray(filterConditions) || filterConditions.length === 0) { + throw new Error("Filter condition array must have at least one condition"); + } + + const conditionKeys = []; // collect all the keys in the next level of the filter condition + for (const condition of filterConditions) { + const keys = Object.keys(condition); + conditionKeys.push(...keys); + } + + const allAreRelOpKeys = verifyAllRelOpKeys(conditionKeys); // verify the next level is also relational operator keys + if (!allAreRelOpKeys) { + return false; + } + + return true; +} + + +/** + * @name collectAtomicFilters + * @method + * @memberof GraphQL/Filter + * @summary Collects atomic filters from a filter query + * @param {Object} filter - an object containing the filters to apply + * @returns {Array} - array of atomic filters + */ +function collectAtomicFilters(filter) { + const atomicFilters = []; + if (!filter) return atomicFilters; + + const isCompoundCondition = checkIfCompoundCondition(filter); + if (!isCompoundCondition) { + const currKey = Object.keys(filter)[0]; + const filters = filter[currKey]; + for (const eachFilter of filters) { + atomicFilters.push(eachFilter); + } + return atomicFilters; + } + + for (const fqKey of Object.keys(filter)) { + if (fqKey === "any" || fqKey === "all") { + const fq = filter[fqKey]; + if (Array.isArray(fq)) { + for (const fqItem of fq) { + atomicFilters.push(...collectAtomicFilters(fqItem)); + } + } + } + } + return atomicFilters; +} + +/** + * @name collectCollectionFields + * @method + * @memberof GraphQL/Filter + * @summary collects all the fields of the specific collection along with metadata + * @param {Object} context - an object containing the per-request state + * @param {String} collectionName - name of the collection + * @returns {Object} - Object with each field as key and type as value + */ +function collectCollectionFields(context, collectionName) { // #TODO: Move this out as a common endpoint + const currentSchema = context.simpleSchemas[collectionName]; + const mergedSchemaObject = currentSchema.mergedSchema(); + const allKeys = Object.keys(mergedSchemaObject); + const returnFieldTypes = {}; + allKeys.forEach((element) => { + const definitionObj = currentSchema.getDefinition(element); + const definition = definitionObj.type[0].type; + if (!SimpleSchema.isSimpleSchema(definition)) { // skip SimpleSchema definition names + if (typeof definition === "function") { + returnFieldTypes[element] = `SimpleSchema.${definition.name}`; + } else { + returnFieldTypes[element] = definition; + } + } + }); + return returnFieldTypes; +} + +/** + * @name countInputValueFields + * @method + * @memberof GraphQL/Filter + * @summary Counts the number of fields received with the input value + * @param {Object} inputValue - input value object + * @returns {Number} - number of fields in the input value + */ +function countInputValueFields(inputValue) { + let count = 0; + for (const key of Object.keys(inputValue)) { + if (inputValue[key] !== null && inputValue[key] !== undefined) { + count += 1; + } + } + return count; +} + +/** + * @name validateConditions + * @method + * @memberof GraphQL/Filter + * @summary Validates the Filter conditions + * @param {Object} allConditions - array of conditions to validate + * @param {Object} allCollectionFields - array of fields from collection with metadata + * @returns {undefined} + */ +function validateConditions(allConditions, allCollectionFields) { + for (const condition of allConditions) { + const { + key, stringValue, intValue, floatValue, boolValue, dateValue, + stringArrayValue, intArrayValue, floatArrayValue, relOper + } = condition; // logNOT, caseSensitive are optional + const expectedValueType = allCollectionFields[key]; + + const inputValuesObject = { stringValue, intValue, floatValue, boolValue, dateValue, stringArrayValue, intArrayValue, floatArrayValue }; + const inputValuesCount = countInputValueFields(inputValuesObject); + if (inputValuesCount > 1) { + throw new Error(`Only one value must be provided for the condition with key: ${key}`); + } + + // if key not in list of collection fields, throw error + if (!Object.keys(allCollectionFields).includes(key)) { + throw new Error(`Invalid key: ${key}`); + } + + // if expectedValueType does not match the type of value, throw error + if (expectedValueType === "SimpleSchema.String" && stringValue === undefined && stringArrayValue === undefined) { + throw new Error(`Key '${key}' expects either stringValue & stringArrayValue`); + } else if (expectedValueType === "SimpleSchema.Integer" && intValue === undefined && intArrayValue === undefined) { + throw new Error(`Key '${key}' expects either intValue & intArrayValue`); + } else if (expectedValueType === "SimpleSchema.Number" && floatValue === undefined && floatArrayValue === undefined) { + throw new Error(`Key '${key}' expects either floatValue & floatArrayValue`); + } else if (expectedValueType === "SimpleSchema.Boolean" && boolValue === undefined) { + throw new Error(`Key '${key}' expects boolValue`); + } else if (expectedValueType === "SimpleSchema.Date" && dateValue === undefined) { + throw new Error(`Key '${key}' expects dateValue`); + } // array can be compared with any of the above types, skipping this check + + if (validCombos[expectedValueType].relOper.indexOf(relOper) === -1) { + throw new Error(`Invalid relational operator '${relOper}' for : ${expectedValueType}`); + } + + if (expectedValueType === "SimpleSchema.Array" && stringArrayValue?.length === 0 && intArrayValue?.length === 0 && floatArrayValue?.length === 0) { + throw new Error("Array value cannot be empty"); + } + } +} + + +/** + * @name simpleConditionToQuery + * @method + * @memberof GraphQL/Filter + * @summary Converts a simple condition to a MongoDB query + * @param {Object} condition The condition to convert + * @param {String} condition.key The key to convert + * @param {String} condition.stringValue The value in String format + * @param {Number} condition.intValue The value in Integer format + * @param {Number} condition.floatValue The value in Integer format + * @param {Boolean} condition.boolValue The value in Boolean format + * @param {String} condition.dateValue The value in Date/String format + * @param {String[]} [condition.stringArrayValue] The value in String Array format + * @param {Number[]} [condition.intArrayValue] The value in Integer Array format + * @param {Number[]} [condition.floatArrayValue] The value in Integer Array format + * @param {String} condition.relOper The relational operator to use + * @param {String} condition.logNOT Whether to negate the condition + * @param {String} condition.caseSensitive Whether regex search is caseSensitive + * @returns {Object} The MongoDB query + */ +function simpleConditionToQuery(condition) { + const { + key, stringValue, intValue, floatValue, boolValue, dateValue, + stringArrayValue, intArrayValue, floatArrayValue, + relOper, logNOT, caseSensitive + } = condition; + const query = {}; + const valueToUse = stringValue || intValue || floatValue || boolValue || dateValue || + stringArrayValue || intArrayValue || floatArrayValue; + + let tempQuery; + switch (relOper) { + case "eq": + if (boolValue !== undefined) { + tempQuery = { $eq: boolValue }; + } else { + tempQuery = { $eq: valueToUse }; + } + break; + case "ne": + if (boolValue !== undefined) { + tempQuery = { $ne: boolValue }; + } else { + tempQuery = { $ne: valueToUse }; + } + break; + case "gt": + tempQuery = { $gt: valueToUse }; + break; + case "gte": + tempQuery = { $gte: valueToUse }; + break; + case "lt": + tempQuery = { $lt: valueToUse }; + break; + case "lte": + tempQuery = { $lte: valueToUse }; + break; + case "in": + tempQuery = { $in: valueToUse }; + break; + case "nin": + tempQuery = { $nin: valueToUse }; + break; + case "regex": + tempQuery = { $regex: valueToUse }; + if (!caseSensitive) { + tempQuery.$options = "i"; + } else { + tempQuery.$options = ""; + } + break; + case "beginsWith": + tempQuery = { $regex: `^${valueToUse}` }; + if (!caseSensitive) { + tempQuery.$options = "i"; + } + break; + case "endsWith": + tempQuery = { $regex: `${valueToUse}$` }; + if (!caseSensitive) { + tempQuery.$options = "i"; + } + break; + default: + throw new Error(`Invalid relational operator: ${relOper}`); + } + + query[key] = logNOT ? { $not: tempQuery } : tempQuery; + + return query; +} + + +/** + * @name processArrayElements + * @method + * @memberof GraphQL/Filter + * @summary Process a simple/single condition by calling simpleConditionToQuery + * @param {Object} element - simple/single condition to be processed + * @returns {Boolean} - query object for a single condition + */ +function processArrayElements(element) { + const allKeys = Object.keys(element); + if (allKeys.length !== 1) { + throw new Error("Invalid input. Array element must have exactly one key"); + } + + const relOp = allKeys[0]; + if (!REL_OPS_KEYS.includes(relOp)) { + throw new Error(`Invalid relational operator: ${relOp}`); + } + + const value = element[relOp]; + if (!Array.isArray(value)) { + throw new Error("Invalid input. Value must be an array"); + } + + const subQueryArray = []; + for (const item of value) { + const fieldKeys = Object.keys(item); + const validFieldKeys = verifyAllFieldKeys(fieldKeys); + if (!validFieldKeys) { + throw new Error("Invalid input. Invalid key in array element"); + } + const singleQueryObject = simpleConditionToQuery(item); + subQueryArray.push(singleQueryObject); + } + + const queryObject = {}; + queryObject[keyMap[relOp]] = subQueryArray; + return queryObject; +} + +/** + * @name newProcessFilterConditions + * @method + * @memberof GraphQL/Filter + * @summary This function is recursively called for all compound conditions + * till it reaches the simple/single condition when it calls processArrayElements + * @param {Object} filterCondition - filter condition to be processed + * @returns {Boolean} - final query object + */ +function newProcessFilterConditions(filterCondition) { + const isCompoundCondition = checkIfCompoundCondition(filterCondition); + + let returnObject; + if (isCompoundCondition) { + const allKeys = Object.keys(filterCondition); + const singleKey = allKeys[0]; + const subConditions = filterCondition[singleKey]; + const subQueryArray = []; + for (const subCondition of subConditions) { + const subQuery = newProcessFilterConditions(subCondition); + subQueryArray.push(subQuery); + } + const key = keyMap[singleKey]; + const query = {}; + query[key] = subQueryArray; + returnObject = query; + } else { + const singleQueryObject = processArrayElements(filterCondition); + returnObject = singleQueryObject; + } + return returnObject; +} + + +/** + * @name generateQuery + * @method + * @memberof GraphQL/Filter + * @summary Builds a selector for Products collection, given a set of filters + * @param {Object} filterQuery - an object containing the filters to apply + * @param {String} shopId - the shop ID + * @returns {Object} - selector + */ +function generateQuery(filterQuery, shopId) { + if (!filterQuery) return {}; + + if (_.size(filterQuery) === 0) return {}; + + const keysTopLevel = Object.keys(filterQuery); + if (keysTopLevel.length !== 1) { + throw new Error("filterQuery must have exactly one key"); + } + const topLevelKey = keysTopLevel[0]; + if (!REL_OPS_KEYS.includes(topLevelKey)) { + throw new Error(`Invalid top level key: ${topLevelKey}. Expected one of: ${REL_OPS_KEYS.join(", ")}`); + } + + const selectorObject = newProcessFilterConditions(filterQuery); + + // If a shopId was provided, add it + if (shopId) { + selectorObject.shopId = shopId; + } + + return selectorObject; +} + +/** + * @name filterSearchProducts + * @method + * @memberof GraphQL/Filter + * @summary Query the Products collection for a list of products + * @param {Object} context - an object containing the per-request state + * @param {String} collectionName - Collection against which to run the query + * @param {Object} filter1level - an object containing ONE level of filters to apply + * @param {Object} filter2level - an object containing TWO levels of filters to apply + * @param {Object} filter3level - an object containing THREE levels of filters to apply + * @param {Object} level - number of levels used in filter object + * @param {String} shopId - shopID to filter by + * @returns {Promise} Products object Promise + */ +export default function generateFilterQuery(context, collectionName, filter1level, filter2level, filter3level, level, shopId) { + let filterQuery; + switch (level) { + case "ONE": + if (!filter1level) { + throw new Error("filter1level is required when level ONE is used"); + } + FilterOneLevelSchema.validate(filter1level); + filterQuery = filter1level; + break; + case "TWO": + if (!filter2level) { + throw new Error("filter2level is required when level TWO is used"); + } + FilterTwoLevelSchema.validate(filter2level); + filterQuery = filter2level; + break; + case "THREE": + if (!filter3level) { + throw new Error("filter3level is required when level THREE is used"); + } + FilterThreeLevelSchema.validate(filter3level); + filterQuery = filter3level; + break; + default: + throw new Error("Invalid level"); + } + + const allConditions = collectAtomicFilters(filterQuery); + const allCollectionFields = collectCollectionFields(context, collectionName); + validateConditions(allConditions, allCollectionFields); + + const selector = generateQuery(filterQuery, shopId); + return { + filterQuery: selector + }; +} diff --git a/packages/api-utils/package.json b/packages/api-utils/package.json index dd05a82aaba..d4ba026db29 100644 --- a/packages/api-utils/package.json +++ b/packages/api-utils/package.json @@ -51,7 +51,8 @@ "graphql-relay": "^0.9.0", "lodash": "^4.17.15", "ramda": "^0.28.0", - "transliteration": "^2.1.9" + "transliteration": "^2.1.9", + "simpl-schema": "^1.12.0" }, "devDependencies": { "@babel/core": "^7.9.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdc9d5f9920..0ba996bc420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1331,6 +1331,7 @@ importers: graphql-relay: ^0.9.0 lodash: ^4.17.15 ramda: ^0.28.0 + simpl-schema: ^1.12.0 transliteration: ^2.1.9 dependencies: '@jest/globals': 26.6.2 @@ -1344,6 +1345,7 @@ importers: graphql-relay: 0.9.0_graphql@14.7.0 lodash: 4.17.21 ramda: 0.28.0 + simpl-schema: 1.12.3 transliteration: 2.3.5 devDependencies: '@babel/core': 7.19.0 From d59ee6f4ff4a30890e7aa3910e85439108181e7d Mon Sep 17 00:00:00 2001 From: Sujith Date: Sat, 3 Dec 2022 00:49:57 +0530 Subject: [PATCH 02/11] fix: graphql linter errors and reversal sampledata Signed-off-by: Sujith --- apps/reaction/plugins.json | 3 +- packages/api-core/src/graphql/schema.graphql | 77 +++++++++++--------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/apps/reaction/plugins.json b/apps/reaction/plugins.json index 4811e730cfd..429aa5fd64e 100644 --- a/apps/reaction/plugins.json +++ b/apps/reaction/plugins.json @@ -35,6 +35,5 @@ "navigation": "@reactioncommerce/api-plugin-navigation", "sitemapGenerator": "@reactioncommerce/api-plugin-sitemap-generator", "notifications": "@reactioncommerce/api-plugin-notifications", - "addressValidationTest": "@reactioncommerce/api-plugin-address-validation-test", - "sampleData": "../../packages/api-plugin-sample-data/index.js" + "addressValidationTest": "@reactioncommerce/api-plugin-address-validation-test" } diff --git a/packages/api-core/src/graphql/schema.graphql b/packages/api-core/src/graphql/schema.graphql index f4c5e8d262e..07a268de7e1 100644 --- a/packages/api-core/src/graphql/schema.graphql +++ b/packages/api-core/src/graphql/schema.graphql @@ -45,32 +45,32 @@ enum LogOpTypes{ "Relational Operator Types for filtering" enum RelOpTypes{ + "Begins With used with String types" + beginsWith + + "Ends With used with String types" + endsWith + "Equal" eq - "Not Equal" - ne - "Greater Than" gt "Greater Than or Equal" gte + "In" + in + "Less Than" lt "Less Than or Equal" lte - "Begins With used with String types" - beginsWith - - "Ends With used with String types" - endsWith - - "In" - in + "Not Equal" + ne "Not In" nin @@ -84,67 +84,76 @@ enum FilterLevels { "Use Filter with 1 level" ONE - "Use Filter with 2 levels" - TWO - "Use Filter with 3 levels" THREE + + "Use Filter with 2 levels" + TWO } "Single Condition for filterSearch" input SingleConditionInput { - "Field name" - key : String! - - "Value to filter if it is String input" - stringValue: String - - "Value to filter if it is Int input" - intValue: Int - - "Value to filter if it is Float input" - floatValue: Float - "Value to filter if it is Boolean input" boolValue: Boolean + "Flag to set if the regex is case insensitive" + caseSensitive: Boolean + "Value to filter if it is Date input" dateValue: DateTime - "Value to filter if it is String Array input" - stringArrayValue: [String] + "Value to filter if it is Float Array input" + floatArrayValue: [Float] + + "Value to filter if it is Float input" + floatValue: Float "Value to filter if it is Int Array input" intArrayValue: [Int] - "Value to filter if it is Float Array input" - floatArrayValue: [Float] + "Value to filter if it is Int input" + intValue: Int - "Relational Operator to join the key and value" - relOper: RelOpTypes! + "Field name" + key : String! "Logical NOT operator to negate the condition" logNOT: Boolean - "Flag to set if the regex is case insensitive" - caseSensitive: Boolean + "Relational Operator to join the key and value" + relOper: RelOpTypes! + + "Value to filter if it is String Array input" + stringArrayValue: [String] + + "Value to filter if it is String input" + stringValue: String } "Filter search with Three levels of input" input FilterThreeLevelInput { + "Array of conditions where all have to be true ($and)" all: [FilterTwoLevelInput] + + "Array of conditions where any have to be true ($or)" any: [FilterTwoLevelInput] } "Filter search with Two levels of input" input FilterTwoLevelInput { + "Array of conditions where all have to be true ($and)" all: [FilterOneLevelInput] + + "Array of conditions where any have to be true ($or)" any: [FilterOneLevelInput] } "Filter search with One level of input" input FilterOneLevelInput { + "Array of conditions where all have to be true ($and)" all: [SingleConditionInput] + + "Array of conditions where any have to be true ($or)" any: [SingleConditionInput] } From 566714e2cb46be43df8164cbbc480740ac58186d Mon Sep 17 00:00:00 2001 From: Sujith Date: Tue, 6 Dec 2022 18:38:20 +0530 Subject: [PATCH 03/11] fix: review comment fixes Signed-off-by: Sujith --- packages/api-core/src/graphql/schema.graphql | 41 ++--- .../src/queries/filterSearchAccounts.js | 9 +- .../src/queries/filterSearchCustomers.js | 9 +- .../resolvers/Query/filterSearchAccounts.js | 12 +- .../resolvers/Query/filterSearchCustomers.js | 12 +- .../src/schemas/account.graphql | 30 +--- .../src/queries/filterSearchOrders.js | 9 +- .../src/resolvers/Query/filterSearchOrders.js | 12 +- .../src/schemas/schema.graphql | 15 +- .../src/queries/filterSearchProducts.js | 9 +- .../resolvers/Query/filterSearchProducts.js | 12 +- .../src/schemas/product.graphql | 15 +- .../api-utils/lib/collectCollectionFields.js | 28 ++++ packages/api-utils/lib/generateFilterQuery.js | 141 +++++------------- 14 files changed, 114 insertions(+), 240 deletions(-) create mode 100644 packages/api-utils/lib/collectCollectionFields.js diff --git a/packages/api-core/src/graphql/schema.graphql b/packages/api-core/src/graphql/schema.graphql index 07a268de7e1..a135474749e 100644 --- a/packages/api-core/src/graphql/schema.graphql +++ b/packages/api-core/src/graphql/schema.graphql @@ -44,7 +44,7 @@ enum LogOpTypes{ } "Relational Operator Types for filtering" -enum RelOpTypes{ +enum RelationalOperatorTypes{ "Begins With used with String types" beginsWith @@ -91,7 +91,7 @@ enum FilterLevels { TWO } -"Single Condition for filterSearch" +"Single Condition for filterSearch, use exactly one of the optional input value type" input SingleConditionInput { "Value to filter if it is Boolean input" boolValue: Boolean @@ -118,10 +118,10 @@ input SingleConditionInput { key : String! "Logical NOT operator to negate the condition" - logNOT: Boolean + logicalNOT: Boolean "Relational Operator to join the key and value" - relOper: RelOpTypes! + relationalOperator: RelationalOperatorTypes! "Value to filter if it is String Array input" stringArrayValue: [String] @@ -130,31 +130,22 @@ input SingleConditionInput { stringValue: String } -"Filter search with Three levels of input" -input FilterThreeLevelInput { - "Array of conditions where all have to be true ($and)" - all: [FilterTwoLevelInput] - - "Array of conditions where any have to be true ($or)" - any: [FilterTwoLevelInput] -} - -"Filter search with Two levels of input" -input FilterTwoLevelInput { - "Array of conditions where all have to be true ($and)" - all: [FilterOneLevelInput] +"Filter search with One level of conditions (use either 'any' or 'all' not both)" +input ConditionsArray { + "Array of single-conditions" + all: [SingleConditionInput] - "Array of conditions where any have to be true ($or)" - any: [FilterOneLevelInput] + "Array of single-conditions" + any: [SingleConditionInput] } -"Filter search with One level of input" -input FilterOneLevelInput { - "Array of conditions where all have to be true ($and)" - all: [SingleConditionInput] +"Filter search with nested conditions of input (use either 'any' or 'all' not both)" +input FilterConditionsInput { + "Array holding Nested conditions (use either 'any' or 'all' not both)" + all: [ConditionsArray] - "Array of conditions where any have to be true ($or)" - any: [SingleConditionInput] + "Array holding Nested conditions (use either 'any' or 'all' not both)" + any: [ConditionsArray] } diff --git a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js b/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js index b055a7bff91..ea5529d48cf 100644 --- a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js +++ b/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js @@ -6,14 +6,11 @@ import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQ * @memberof GraphQL/Accounts * @summary Query the Accounts collection for a list of customers/accounts * @param {Object} context - an object containing the per-request state - * @param {Object} filter1level - an object containing ONE level of filters to apply - * @param {Object} filter2level - an object containing TWO levels of filters to apply - * @param {Object} filter3level - an object containing THREE levels of filters to apply - * @param {String} level - number of levels used in filter object + * @param {Object} conditions - object containing the filter conditions * @param {String} shopId - shopID to filter by * @returns {Promise} Accounts object Promise */ -export default async function filterSearchAccounts(context, filter1level, filter2level, filter3level, level, shopId) { +export default async function filterSearchAccounts(context, conditions, shopId) { const { collections: { Accounts } } = context; if (!shopId) { @@ -21,7 +18,7 @@ export default async function filterSearchAccounts(context, filter1level, filter } await context.validatePermissions("reaction:legacy:accounts", "read", { shopId }); - const { filterQuery } = generateFilterQuery(context, "Account", filter1level, filter2level, filter3level, level, shopId); + const { filterQuery } = generateFilterQuery(context, "Account", conditions, shopId); return Accounts.find(filterQuery); } diff --git a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js b/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js index 04ae06bdec7..6b91f569da0 100644 --- a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js +++ b/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js @@ -6,14 +6,11 @@ import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQ * @memberof GraphQL/Customers * @summary Query the Accounts collection for a list of customers/accounts * @param {Object} context - an object containing the per-request state - * @param {Object} filter1level - an object containing ONE level of filters to apply - * @param {Object} filter2level - an object containing TWO levels of filters to apply - * @param {Object} filter3level - an object containing THREE levels of filters to apply - * @param {String} level - number of levels used in filter object + * @param {Object} conditions - object containing the filter conditions * @param {String} shopId - shopID to filter by * @returns {Promise} Accounts object Promise */ -export default async function filterSearchCustomers(context, filter1level, filter2level, filter3level, level, shopId) { +export default async function filterSearchCustomers(context, conditions, shopId) { const { collections: { Accounts } } = context; if (!shopId) { @@ -21,7 +18,7 @@ export default async function filterSearchCustomers(context, filter1level, filte } await context.validatePermissions("reaction:legacy:accounts", "read", { shopId }); - const { filterQuery } = generateFilterQuery(context, "Account", filter1level, filter2level, filter3level, level, shopId); + const { filterQuery } = generateFilterQuery(context, "Account", conditions, shopId); filterQuery.groups = { $in: [null, []] }; // filter out non-customer accounts return Accounts.find(filterQuery); diff --git a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js index d2f4c09fbd8..2d5f75fc497 100644 --- a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js +++ b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js @@ -9,10 +9,7 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} _ - unused * @param {Object} args - an object of all arguments that were sent by the client * @param {String} args.shopId - id of shop to query - * @param {Object} args.filter1level - filter conditions with 1 level - * @param {Object} args.filter2level - filter conditions with 2 levels - * @param {Object} args.filter3level - filter conditions with 3 levels - * @param {String} args.level - filter level used + * @param {Object} args.conditions - object containing the filter conditions * @param {Object} context - an object containing the per-request state * @param {Object} info Info about the GraphQL request * @returns {Promise} Accounts @@ -20,14 +17,11 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque export default async function filterSearchAccounts(_, args, context, info) { const { shopId, - filter1level, - filter2level, - filter3level, - level, + conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchAccounts(context, filter1level, filter2level, filter3level, level, shopId); + const query = await context.queries.filterSearchAccounts(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js index 5afdac8bb82..6d9e9556ab3 100644 --- a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js +++ b/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js @@ -9,10 +9,7 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} _ - unused * @param {Object} args - an object of all arguments that were sent by the client * @param {String} args.shopId - id of shop to query - * @param {Object} args.filter1level - filter conditions with 1 level - * @param {Object} args.filter2level - filter conditions with 2 levels - * @param {Object} args.filter3level - filter conditions with 3 levels - * @param {String} args.level - filter level used + * @param {Object} args.conditions - object containing the filter conditions * @param {Object} context - an object containing the per-request state * @param {Object} info Info about the GraphQL request * @returns {Promise} Accounts @@ -20,14 +17,11 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque export default async function filterSearchCustomers(_, args, context, info) { const { shopId, - filter1level, - filter2level, - filter3level, - level, + conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchCustomers(context, filter1level, filter2level, filter3level, level, shopId); + const query = await context.queries.filterSearchCustomers(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-accounts/src/schemas/account.graphql b/packages/api-plugin-accounts/src/schemas/account.graphql index ece2bd3fc28..de1c66a81c6 100644 --- a/packages/api-plugin-accounts/src/schemas/account.graphql +++ b/packages/api-plugin-accounts/src/schemas/account.graphql @@ -496,22 +496,13 @@ extend type Query { id: ID! ): Account - # "filterSearch Query for list of Accounts" + "Query to get a filtered list of Accounts" filterSearchAccounts( "Shop ID" shopId: ID!, - "fliterSearch Conditions with 3 levels" - filter3level: FilterThreeLevelInput, - - "fliterSearch Conditions with 2 levels" - filter2level: FilterTwoLevelInput, - - "fliterSearch Conditions with 1 level" - filter1level: FilterOneLevelInput, - - "fliterSearch level used specifier" - level: FilterLevels!, + "Input Conditions for fliter (use either 'any' or 'all' not both)" + conditions: FilterConditionsInput, "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." after: ConnectionCursor, @@ -535,22 +526,13 @@ extend type Query { sortBy: AccountSortByField = createdAt ): AccountConnection - # "filterSearch Query for list of Customers" + "Query to get a filtered list of Customers" filterSearchCustomers( "Shop ID" shopId: ID!, - "fliterSearch Conditions with 3 levels" - filter3level: FilterThreeLevelInput, - - "fliterSearch Conditions with 2 levels" - filter2level: FilterTwoLevelInput, - - "fliterSearch Conditions with 1 level" - filter1level: FilterOneLevelInput, - - "fliterSearch level used specifier" - level: FilterLevels!, + "Input Conditions for fliter (use either 'any' or 'all' not both)" + conditions: FilterConditionsInput, "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." after: ConnectionCursor, diff --git a/packages/api-plugin-orders/src/queries/filterSearchOrders.js b/packages/api-plugin-orders/src/queries/filterSearchOrders.js index 141e7444ede..abd7c2f69b3 100644 --- a/packages/api-plugin-orders/src/queries/filterSearchOrders.js +++ b/packages/api-plugin-orders/src/queries/filterSearchOrders.js @@ -6,14 +6,11 @@ import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQ * @memberof GraphQL/Orders * @summary Query the Orders collection for a list of orders * @param {Object} context - an object containing the per-request state - * @param {Object} filter1level - an object containing ONE level of filters to apply - * @param {Object} filter2level - an object containing TWO levels of filters to apply - * @param {Object} filter3level - an object containing THREE levels of filters to apply - * @param {String} level - number of levels used in filter object + * @param {Object} conditions - object containing the filter conditions * @param {String} shopId - shopID to filter by * @returns {Promise} Orders object Promise */ -export default async function filterSearchOrders(context, filter1level, filter2level, filter3level, level, shopId) { +export default async function filterSearchOrders(context, conditions, shopId) { const { collections: { Orders } } = context; if (!shopId) { @@ -22,7 +19,7 @@ export default async function filterSearchOrders(context, filter1level, filter2l await context.validatePermissions("reaction:legacy:orders", "read", { shopId }); - const { filterQuery } = generateFilterQuery(context, "Order", filter1level, filter2level, filter3level, level, shopId); + const { filterQuery } = generateFilterQuery(context, "Order", conditions, shopId); return Orders.find(filterQuery); } diff --git a/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js b/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js index ff3612a1547..44af89d5bd1 100644 --- a/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js +++ b/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js @@ -9,10 +9,7 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} _ - unused * @param {Object} args - an object of all arguments that were sent by the client * @param {String} args.shopId - id of shop to query - * @param {Object} args.filter1level - filter conditions with 1 level - * @param {Object} args.filter2level - filter conditions with 2 levels - * @param {Object} args.filter3level - filter conditions with 3 levels - * @param {String} args.level - filter level used + * @param {Object} args.conditions - object containing the filter conditions * @param {Object} context - an object containing the per-request state * @param {Object} info Info about the GraphQL request * @returns {Promise} Orders @@ -20,14 +17,11 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque export default async function filterSearchOrders(_, args, context, info) { const { shopId, - filter1level, - filter2level, - filter3level, - level, + conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchOrders(context, filter1level, filter2level, filter3level, level, shopId); + const query = await context.queries.filterSearchOrders(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-orders/src/schemas/schema.graphql b/packages/api-plugin-orders/src/schemas/schema.graphql index d68f4207b57..94c8769d601 100644 --- a/packages/api-plugin-orders/src/schemas/schema.graphql +++ b/packages/api-plugin-orders/src/schemas/schema.graphql @@ -1,20 +1,11 @@ extend type Query { - # "filterSearch Query for list of Orders" + "Query to get a filtered list of Orders" filterSearchOrders( "Shop ID" shopId: ID!, - "fliterSearch Conditions with 3 levels" - filter3level: FilterThreeLevelInput, - - "fliterSearch Conditions with 2 levels" - filter2level: FilterTwoLevelInput, - - "fliterSearch Conditions with 1 level" - filter1level: FilterOneLevelInput, - - "fliterSearch level used specifier" - level: FilterLevels!, + "Input Conditions for fliter (use either 'any' or 'all' not both)" + conditions: FilterConditionsInput, "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." after: ConnectionCursor, diff --git a/packages/api-plugin-products/src/queries/filterSearchProducts.js b/packages/api-plugin-products/src/queries/filterSearchProducts.js index 33ec37f3c97..f5132d53898 100644 --- a/packages/api-plugin-products/src/queries/filterSearchProducts.js +++ b/packages/api-plugin-products/src/queries/filterSearchProducts.js @@ -6,14 +6,11 @@ import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQ * @memberof GraphQL/Products * @summary Query the Products collection for a list of products * @param {Object} context - an object containing the per-request state - * @param {Object} filter1level - an object containing ONE level of filters to apply - * @param {Object} filter2level - an object containing TWO levels of filters to apply - * @param {Object} filter3level - an object containing THREE levels of filters to apply - * @param {String} level - number of levels used in filter object + * @param {Object} conditions - object containing the filter conditions * @param {String} shopId - shopID to filter by * @returns {Promise} Products object Promise */ -export default async function filterSearchProducts(context, filter1level, filter2level, filter3level, level, shopId) { +export default async function filterSearchProducts(context, conditions, shopId) { const { collections: { Products } } = context; if (!shopId) { @@ -22,7 +19,7 @@ export default async function filterSearchProducts(context, filter1level, filter await context.validatePermissions("reaction:legacy:products", "read", { shopId }); - const { filterQuery } = generateFilterQuery(context, "Product", filter1level, filter2level, filter3level, level, shopId); + const { filterQuery } = generateFilterQuery(context, "Product", conditions, shopId); return Products.find(filterQuery); } diff --git a/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js b/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js index 6ab0f01ead7..0236c14ce17 100644 --- a/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js +++ b/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js @@ -9,10 +9,7 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} _ - unused * @param {Object} args - an object of all arguments that were sent by the client * @param {String} args.shopId - id of shop to query - * @param {Object} args.filter1level - filter conditions with 1 level - * @param {Object} args.filter2level - filter conditions with 2 levels - * @param {Object} args.filter3level - filter conditions with 3 levels - * @param {String} args.level - filter level used + * @param {Object} args.conditions - object containing the filter conditions * @param {Object} context - an object containing the per-request state * @param {Object} info Info about the GraphQL request * @returns {Promise} Products @@ -20,14 +17,11 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque export default async function filterSearchProducts(_, args, context, info) { const { shopId, - filter1level, - filter2level, - filter3level, - level, + conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchProducts(context, filter1level, filter2level, filter3level, level, shopId); + const query = await context.queries.filterSearchProducts(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-products/src/schemas/product.graphql b/packages/api-plugin-products/src/schemas/product.graphql index 1211d54137a..6d76850d9f8 100644 --- a/packages/api-plugin-products/src/schemas/product.graphql +++ b/packages/api-plugin-products/src/schemas/product.graphql @@ -625,22 +625,13 @@ extend type Mutation { } extend type Query { - # "filterSearch Query for list of Products" + "Query to get a filtered list of Products" filterSearchProducts( "Shop ID" shopId: ID!, - "fliterSearch Conditions with 3 levels" - filter3level: FilterThreeLevelInput, - - "fliterSearch Conditions with 2 levels" - filter2level: FilterTwoLevelInput, - - "fliterSearch Conditions with 1 level" - filter1level: FilterOneLevelInput, - - "fliterSearch level used specifier" - level: FilterLevels!, + "Input Conditions for fliter (use either 'any' or 'all' not both)" + conditions: FilterConditionsInput, "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." after: ConnectionCursor, diff --git a/packages/api-utils/lib/collectCollectionFields.js b/packages/api-utils/lib/collectCollectionFields.js new file mode 100644 index 00000000000..4c9e9b6ab4f --- /dev/null +++ b/packages/api-utils/lib/collectCollectionFields.js @@ -0,0 +1,28 @@ +import SimpleSchema from "simpl-schema"; +/** + * @name collectCollectionFields + * @method + * @memberof GraphQL/Filter + * @summary collects all the fields of the specific collection along with metadata + * @param {Object} context - an object containing the per-request state + * @param {String} collectionName - name of the collection + * @returns {Object} - Object with each field as key and type as value + */ +export default function collectCollectionFields(context, collectionName) { + const currentSchema = context.simpleSchemas[collectionName]; + const mergedSchemaObject = currentSchema.mergedSchema(); + const allKeys = Object.keys(mergedSchemaObject); + const returnFieldTypes = {}; + allKeys.forEach((element) => { + const definitionObj = currentSchema.getDefinition(element); + const definition = definitionObj.type[0].type; + if (!SimpleSchema.isSimpleSchema(definition)) { // skip SimpleSchema definition names + if (typeof definition === "function") { + returnFieldTypes[element] = `SimpleSchema.${definition.name}`; + } else { + returnFieldTypes[element] = definition; + } + } + }); + return returnFieldTypes; +} diff --git a/packages/api-utils/lib/generateFilterQuery.js b/packages/api-utils/lib/generateFilterQuery.js index 0ff3da64264..4046cbb8b9c 100644 --- a/packages/api-utils/lib/generateFilterQuery.js +++ b/packages/api-utils/lib/generateFilterQuery.js @@ -1,5 +1,6 @@ import SimpleSchema from "simpl-schema"; import _ from "lodash"; +import collectCollectionFields from "./collectCollectionFields.js"; const SingleConditionSchema = new SimpleSchema({ "key": { @@ -46,11 +47,11 @@ const SingleConditionSchema = new SimpleSchema({ "floatArrayValue.$": { type: Number }, - "relOper": { + "relationalOperator": { type: String, allowedValues: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "beginsWith", "endsWith"] }, - "logNOT": { + "logicalNOT": { type: Boolean, optional: true }, @@ -60,7 +61,7 @@ const SingleConditionSchema = new SimpleSchema({ } }); -const FilterOneLevelSchema = new SimpleSchema({ +const ConditionsSchema = new SimpleSchema({ "any": { type: Array, optional: true @@ -77,64 +78,46 @@ const FilterOneLevelSchema = new SimpleSchema({ } }); -const FilterTwoLevelSchema = new SimpleSchema({ +const ConditionsArraySchema = new SimpleSchema({ "any": { type: Array, optional: true }, "any.$": { - type: FilterOneLevelSchema + type: ConditionsSchema }, "all": { type: Array, optional: true }, "all.$": { - type: FilterOneLevelSchema + type: ConditionsSchema } }); -const FilterThreeLevelSchema = new SimpleSchema({ - "any": { - type: Array, - optional: true - }, - "any.$": { - type: FilterTwoLevelSchema - }, - "all": { - type: Array, - optional: true - }, - "all.$": { - type: FilterTwoLevelSchema - } -}); - - const validCombos = { "SimpleSchema.String": { - relOper: ["eq", "ne", "in", "nin", "regex", "beginsWith", "endsWith"], + relationalOperator: ["eq", "ne", "in", "nin", "regex", "beginsWith", "endsWith"], typeOf: ["string"] }, "SimpleSchema.Integer": { - relOper: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"], + relationalOperator: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"], typeOf: ["number"] }, "SimpleSchema.Number": { - relOper: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"], + relationalOperator: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"], typeOf: ["number"] }, "SimpleSchema.Array": { - relOper: ["in", "nin", "eq", "ne"], + relationalOperator: ["in", "nin", "eq", "ne"], typeOf: ["array"] }, "SimpleSchema.Boolean": { - relOper: ["eq", "ne"], + relationalOperator: ["eq", "ne"], typeOf: ["boolean"] }, "SimpleSchema.Date": { - relOper: ["eq", "ne", "gt", "gte", "lt", "lte"], + relationalOperator: ["eq", "ne", "gt", "gte", "lt", "lte"], typeOf: ["date"] } }; @@ -144,7 +127,7 @@ const REL_OPS_KEYS = ["any", "all"]; const FIELD_KEYS = [ "key", "stringValue", "boolValue", "intValue", "floatValue", "dateValue", "stringArrayValue", "intArrayValue", "floatArrayValue", - "relOper", "caseSensitive", "logNOT" + "relationalOperator", "caseSensitive", "logicalNOT" ]; const keyMap = { @@ -267,34 +250,6 @@ function collectAtomicFilters(filter) { return atomicFilters; } -/** - * @name collectCollectionFields - * @method - * @memberof GraphQL/Filter - * @summary collects all the fields of the specific collection along with metadata - * @param {Object} context - an object containing the per-request state - * @param {String} collectionName - name of the collection - * @returns {Object} - Object with each field as key and type as value - */ -function collectCollectionFields(context, collectionName) { // #TODO: Move this out as a common endpoint - const currentSchema = context.simpleSchemas[collectionName]; - const mergedSchemaObject = currentSchema.mergedSchema(); - const allKeys = Object.keys(mergedSchemaObject); - const returnFieldTypes = {}; - allKeys.forEach((element) => { - const definitionObj = currentSchema.getDefinition(element); - const definition = definitionObj.type[0].type; - if (!SimpleSchema.isSimpleSchema(definition)) { // skip SimpleSchema definition names - if (typeof definition === "function") { - returnFieldTypes[element] = `SimpleSchema.${definition.name}`; - } else { - returnFieldTypes[element] = definition; - } - } - }); - return returnFieldTypes; -} - /** * @name countInputValueFields * @method @@ -326,8 +281,8 @@ function validateConditions(allConditions, allCollectionFields) { for (const condition of allConditions) { const { key, stringValue, intValue, floatValue, boolValue, dateValue, - stringArrayValue, intArrayValue, floatArrayValue, relOper - } = condition; // logNOT, caseSensitive are optional + stringArrayValue, intArrayValue, floatArrayValue, relationalOperator + } = condition; // logicalNOT, caseSensitive are optional const expectedValueType = allCollectionFields[key]; const inputValuesObject = { stringValue, intValue, floatValue, boolValue, dateValue, stringArrayValue, intArrayValue, floatArrayValue }; @@ -354,8 +309,8 @@ function validateConditions(allConditions, allCollectionFields) { throw new Error(`Key '${key}' expects dateValue`); } // array can be compared with any of the above types, skipping this check - if (validCombos[expectedValueType].relOper.indexOf(relOper) === -1) { - throw new Error(`Invalid relational operator '${relOper}' for : ${expectedValueType}`); + if (validCombos[expectedValueType].relationalOperator.indexOf(relationalOperator) === -1) { + throw new Error(`Invalid relational operator '${relationalOperator}' for : ${expectedValueType}`); } if (expectedValueType === "SimpleSchema.Array" && stringArrayValue?.length === 0 && intArrayValue?.length === 0 && floatArrayValue?.length === 0) { @@ -380,8 +335,8 @@ function validateConditions(allConditions, allCollectionFields) { * @param {String[]} [condition.stringArrayValue] The value in String Array format * @param {Number[]} [condition.intArrayValue] The value in Integer Array format * @param {Number[]} [condition.floatArrayValue] The value in Integer Array format - * @param {String} condition.relOper The relational operator to use - * @param {String} condition.logNOT Whether to negate the condition + * @param {String} condition.relationalOperator The relational operator to use + * @param {String} condition.logicalNOT Whether to negate the condition * @param {String} condition.caseSensitive Whether regex search is caseSensitive * @returns {Object} The MongoDB query */ @@ -389,14 +344,14 @@ function simpleConditionToQuery(condition) { const { key, stringValue, intValue, floatValue, boolValue, dateValue, stringArrayValue, intArrayValue, floatArrayValue, - relOper, logNOT, caseSensitive + relationalOperator, logicalNOT, caseSensitive } = condition; const query = {}; const valueToUse = stringValue || intValue || floatValue || boolValue || dateValue || stringArrayValue || intArrayValue || floatArrayValue; let tempQuery; - switch (relOper) { + switch (relationalOperator) { case "eq": if (boolValue !== undefined) { tempQuery = { $eq: boolValue }; @@ -450,10 +405,10 @@ function simpleConditionToQuery(condition) { } break; default: - throw new Error(`Invalid relational operator: ${relOper}`); + throw new Error(`Invalid relational operator: ${relationalOperator}`); } - query[key] = logNOT ? { $not: tempQuery } : tempQuery; + query[key] = logicalNOT ? { $not: tempQuery } : tempQuery; return query; } @@ -500,7 +455,7 @@ function processArrayElements(element) { } /** - * @name newProcessFilterConditions + * @name processFilterConditions * @method * @memberof GraphQL/Filter * @summary This function is recursively called for all compound conditions @@ -508,7 +463,7 @@ function processArrayElements(element) { * @param {Object} filterCondition - filter condition to be processed * @returns {Boolean} - final query object */ -function newProcessFilterConditions(filterCondition) { +function processFilterConditions(filterCondition) { const isCompoundCondition = checkIfCompoundCondition(filterCondition); let returnObject; @@ -518,7 +473,7 @@ function newProcessFilterConditions(filterCondition) { const subConditions = filterCondition[singleKey]; const subQueryArray = []; for (const subCondition of subConditions) { - const subQuery = newProcessFilterConditions(subCondition); + const subQuery = processFilterConditions(subCondition); subQueryArray.push(subQuery); } const key = keyMap[singleKey]; @@ -549,14 +504,14 @@ function generateQuery(filterQuery, shopId) { const keysTopLevel = Object.keys(filterQuery); if (keysTopLevel.length !== 1) { - throw new Error("filterQuery must have exactly one key"); + throw new Error("Filter condition must have exactly one key at top level"); } const topLevelKey = keysTopLevel[0]; if (!REL_OPS_KEYS.includes(topLevelKey)) { throw new Error(`Invalid top level key: ${topLevelKey}. Expected one of: ${REL_OPS_KEYS.join(", ")}`); } - const selectorObject = newProcessFilterConditions(filterQuery); + const selectorObject = processFilterConditions(filterQuery); // If a shopId was provided, add it if (shopId) { @@ -573,46 +528,18 @@ function generateQuery(filterQuery, shopId) { * @summary Query the Products collection for a list of products * @param {Object} context - an object containing the per-request state * @param {String} collectionName - Collection against which to run the query - * @param {Object} filter1level - an object containing ONE level of filters to apply - * @param {Object} filter2level - an object containing TWO levels of filters to apply - * @param {Object} filter3level - an object containing THREE levels of filters to apply - * @param {Object} level - number of levels used in filter object + * @param {Object} conditions - the conditions for the filter * @param {String} shopId - shopID to filter by * @returns {Promise} Products object Promise */ -export default function generateFilterQuery(context, collectionName, filter1level, filter2level, filter3level, level, shopId) { - let filterQuery; - switch (level) { - case "ONE": - if (!filter1level) { - throw new Error("filter1level is required when level ONE is used"); - } - FilterOneLevelSchema.validate(filter1level); - filterQuery = filter1level; - break; - case "TWO": - if (!filter2level) { - throw new Error("filter2level is required when level TWO is used"); - } - FilterTwoLevelSchema.validate(filter2level); - filterQuery = filter2level; - break; - case "THREE": - if (!filter3level) { - throw new Error("filter3level is required when level THREE is used"); - } - FilterThreeLevelSchema.validate(filter3level); - filterQuery = filter3level; - break; - default: - throw new Error("Invalid level"); - } +export default function generateFilterQuery(context, collectionName, conditions, shopId) { + ConditionsArraySchema.validate(conditions); - const allConditions = collectAtomicFilters(filterQuery); + const allConditions = collectAtomicFilters(conditions); const allCollectionFields = collectCollectionFields(context, collectionName); validateConditions(allConditions, allCollectionFields); - const selector = generateQuery(filterQuery, shopId); + const selector = generateQuery(conditions, shopId); return { filterQuery: selector }; From 21c62f1876f55bfec98ddb2d2efc7063249121a5 Mon Sep 17 00:00:00 2001 From: Sujith Date: Wed, 7 Dec 2022 09:17:49 +0530 Subject: [PATCH 04/11] fix: review comment fixes 2 Signed-off-by: Sujith --- packages/api-core/src/graphql/schema.graphql | 29 +++---------------- .../src/queries/filterSearchAccounts.js | 2 +- .../src/queries/filterSearchCustomers.js | 2 +- .../src/queries/filterSearchOrders.js | 2 +- .../src/queries/filterSearchProducts.js | 2 +- 5 files changed, 8 insertions(+), 29 deletions(-) diff --git a/packages/api-core/src/graphql/schema.graphql b/packages/api-core/src/graphql/schema.graphql index a135474749e..4e149b27b0d 100644 --- a/packages/api-core/src/graphql/schema.graphql +++ b/packages/api-core/src/graphql/schema.graphql @@ -34,16 +34,7 @@ enum MassUnit { oz } -"Logical Operator Types for filtering" -enum LogOpTypes{ - "AND" - AND - - "OR" - OR -} - -"Relational Operator Types for filtering" +"Relational Operator Types used in filtering inside a single condition" enum RelationalOperatorTypes{ "Begins With used with String types" beginsWith @@ -79,25 +70,13 @@ enum RelationalOperatorTypes{ regex } -"Flag to specify the number of levels used in Filter Search" -enum FilterLevels { - "Use Filter with 1 level" - ONE - - "Use Filter with 3 levels" - THREE - - "Use Filter with 2 levels" - TWO -} - "Single Condition for filterSearch, use exactly one of the optional input value type" input SingleConditionInput { "Value to filter if it is Boolean input" boolValue: Boolean "Flag to set if the regex is case insensitive" - caseSensitive: Boolean + caseSensitive: Boolean "Value to filter if it is Date input" dateValue: DateTime @@ -120,8 +99,8 @@ input SingleConditionInput { "Logical NOT operator to negate the condition" logicalNOT: Boolean - "Relational Operator to join the key and value" - relationalOperator: RelationalOperatorTypes! + "Relational Operator to join the key and value" + relationalOperator: RelationalOperatorTypes! "Value to filter if it is String Array input" stringArrayValue: [String] diff --git a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js b/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js index ea5529d48cf..73420bf44a8 100644 --- a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js +++ b/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js @@ -1,4 +1,4 @@ -import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** * @name filterSearchAccounts diff --git a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js b/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js index 6b91f569da0..23bfb3debea 100644 --- a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js +++ b/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js @@ -1,4 +1,4 @@ -import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** * @name filterSearchCustomers diff --git a/packages/api-plugin-orders/src/queries/filterSearchOrders.js b/packages/api-plugin-orders/src/queries/filterSearchOrders.js index abd7c2f69b3..74fca73c746 100644 --- a/packages/api-plugin-orders/src/queries/filterSearchOrders.js +++ b/packages/api-plugin-orders/src/queries/filterSearchOrders.js @@ -1,4 +1,4 @@ -import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** * @name filterSearchOrders diff --git a/packages/api-plugin-products/src/queries/filterSearchProducts.js b/packages/api-plugin-products/src/queries/filterSearchProducts.js index f5132d53898..1d7a80c4f27 100644 --- a/packages/api-plugin-products/src/queries/filterSearchProducts.js +++ b/packages/api-plugin-products/src/queries/filterSearchProducts.js @@ -1,4 +1,4 @@ -import generateFilterQuery from "@reactioncommerce/api-utils/lib/generateFilterQuery.js"; +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** * @name filterSearchProducts From 315bb97abc3e70dcb1a89da8adca5468302b24be Mon Sep 17 00:00:00 2001 From: Sujith Date: Wed, 7 Dec 2022 15:03:04 +0530 Subject: [PATCH 05/11] fix: add changeset and unit test Signed-off-by: Sujith --- .changeset/lazy-zoos-listen.md | 9 + .../api-utils/lib/generateFilterQuery.test.js | 265 ++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 .changeset/lazy-zoos-listen.md create mode 100644 packages/api-utils/lib/generateFilterQuery.test.js diff --git a/.changeset/lazy-zoos-listen.md b/.changeset/lazy-zoos-listen.md new file mode 100644 index 00000000000..21e7c89fd36 --- /dev/null +++ b/.changeset/lazy-zoos-listen.md @@ -0,0 +1,9 @@ +--- +"@reactioncommerce/api-core": minor +"@reactioncommerce/api-plugin-accounts": minor +"@reactioncommerce/api-plugin-orders": minor +"@reactioncommerce/api-plugin-products": minor +"@reactioncommerce/api-utils": minor +--- + +Filter feature. This new feature provides a common function that can be used in a new query endpoint to get filtered results from any collection. diff --git a/packages/api-utils/lib/generateFilterQuery.test.js b/packages/api-utils/lib/generateFilterQuery.test.js new file mode 100644 index 00000000000..08accdf0c25 --- /dev/null +++ b/packages/api-utils/lib/generateFilterQuery.test.js @@ -0,0 +1,265 @@ +import generateFilterQuery from "./generateFilterQuery.js"; +import mockCollection from "./tests/mockCollection.js"; +import mockContext from "./tests/mockContext.js"; + +mockContext.collections.Products = mockCollection("Products"); + +jest.mock("./collectCollectionFields", () => jest.fn().mockImplementation(() => ({ + "_id": "SimpleSchema.String", + "ancestors": "SimpleSchema.Array", + "ancestors.$": "SimpleSchema.String", + "createdAt": "SimpleSchema.Date", + "currentProductHash": "SimpleSchema.String", + "description": "SimpleSchema.String", + "facebookMsg": "SimpleSchema.String", + "googleplusMsg": "SimpleSchema.String", + "handle": "SimpleSchema.String", + "hashtags": "SimpleSchema.Array", + "hashtags.$": "SimpleSchema.String", + "isDeleted": "SimpleSchema.Boolean", + "isVisible": "SimpleSchema.Boolean", + "metaDescription": "SimpleSchema.String", + "metafields": "SimpleSchema.Array", + "metafields.$.key": "SimpleSchema.String", + "metafields.$.namespace": "SimpleSchema.String", + "metafields.$.scope": "SimpleSchema.String", + "metafields.$.value": "SimpleSchema.String", + "metafields.$.valueType": "SimpleSchema.String", + "metafields.$.description": "SimpleSchema.String", + "originCountry": "SimpleSchema.String", + "pageTitle": "SimpleSchema.String", + "parcel.containers": "SimpleSchema.String", + "parcel.length": "SimpleSchema.Number", + "parcel.width": "SimpleSchema.Number", + "parcel.height": "SimpleSchema.Number", + "parcel.weight": "SimpleSchema.Number", + "pinterestMsg": "SimpleSchema.String", + "productType": "SimpleSchema.String", + "publishedAt": "SimpleSchema.Date", + "publishedProductHash": "SimpleSchema.String", + "shopId": "SimpleSchema.String", + "shouldAppearInSitemap": "SimpleSchema.Boolean", + "supportedFulfillmentTypes": "SimpleSchema.Array", + "supportedFulfillmentTypes.$": "SimpleSchema.String", + "template": "SimpleSchema.String", + "title": "SimpleSchema.String", + "twitterMsg": "SimpleSchema.String", + "type": "SimpleSchema.String", + "updatedAt": "SimpleSchema.Date", + "vendor": "SimpleSchema.String", + "workflow.status": "SimpleSchema.String", + "workflow.workflow": "SimpleSchema.Array", + "workflow.workflow.$": "SimpleSchema.String", + "price.range": "SimpleSchema.String", + "price.min": "SimpleSchema.Number", + "price.max": "SimpleSchema.Number" +}))); + + +test("returns the correct Query when single condition is given", () => { + const shopId = "SHOP123"; + const collectionName = "Product"; + const conditions = { + all: [{ + all: [ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: "eq", + logicalNOT: false + } + ] + }] + }; + + const { filterQuery } = generateFilterQuery(mockContext, collectionName, conditions, shopId); + + const expectedResult = { + $and: [{ + $and: [ + { + handle: { + $eq: "mens-waterproof-outdoor-rain-jacket" + } + } + ] + }], + shopId: "SHOP123" + }; + + expect(filterQuery).toStrictEqual(expectedResult); +}); + +test("returns the correct Query when two conditions are given", () => { + const shopId = "SHOP123"; + const collectionName = "Product"; + const conditions = { + any: [ + { + all: [ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: "eq", + logicalNOT: false + }, + { + key: "_id", + stringValue: "DZwLHk4EAzitRni8F", + relationalOperator: "eq", + logicalNOT: false + } + ] + } + ] + }; + + const { filterQuery } = generateFilterQuery(mockContext, collectionName, conditions, shopId); + + const expectedResult = { + $or: [ + { + $and: [ + { + handle: { + $eq: "mens-waterproof-outdoor-rain-jacket" + } + }, + { + _id: { + $eq: "DZwLHk4EAzitRni8F" + } + } + ] + } + ], + shopId: "SHOP123" + }; + expect(filterQuery).toStrictEqual(expectedResult); +}); + + +test("returns the correct Query when multiple conditions are given", () => { + const shopId = "SHOP123"; + const collectionName = "Product"; + const conditions = { + all: [ + { + any: [ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: "eq", + logicalNOT: false + }, + { + key: "title", + stringValue: "men", + relationalOperator: "beginsWith", + logicalNOT: false, + caseSensitive: false + } + ] + }, + { + all: [ + { + key: "_id", + stringArrayValue: ["DZwLHk4EAzitRni8F", "Hn4BRaBvLkYffMq36"], + relationalOperator: "in", + logicalNOT: false + }, + { + key: "isDeleted", + boolValue: false, + relationalOperator: "eq", + logicalNOT: false + }, + { + key: "workflow.status", + stringValue: "new", + relationalOperator: "eq", + logicalNOT: false + } + ] + }, + { + all: [ + { + key: "price.min", + floatValue: 19.99, + relationalOperator: "gte", + logicalNOT: false + }, + { + key: "type", + stringValue: "simple", + relationalOperator: "eq", + logicalNOT: false + } + ] + } + ] + }; + + const { filterQuery } = generateFilterQuery(mockContext, collectionName, conditions, shopId); + + const expectedResult = { + $and: [ + { + $or: [ + { + handle: { + $eq: "mens-waterproof-outdoor-rain-jacket" + } + }, + { + title: { + $regex: "^men", + $options: "i" + } + } + ] + }, + { + $and: [ + { + _id: { + $in: [ + "DZwLHk4EAzitRni8F", + "Hn4BRaBvLkYffMq36" + ] + } + }, + { + isDeleted: { + $eq: false + } + }, + { + "workflow.status": { + $eq: "new" + } + } + ] + }, + { + $and: [ + { + "price.min": { + $gte: 19.99 + } + }, + { + type: { + $eq: "simple" + } + } + ] + } + ], + shopId: "SHOP123" + }; + + expect(filterQuery).toStrictEqual(expectedResult); +}); From cde07fd97cf108e086c39876fb78a8f62490a51c Mon Sep 17 00:00:00 2001 From: Sujith Date: Thu, 8 Dec 2022 14:55:51 +0530 Subject: [PATCH 06/11] fix: update README of plugins - utils & products Signed-off-by: Sujith --- packages/api-plugin-products/README.md | 153 ++++++++++++++++++ packages/api-utils/docs/README.md | 1 + .../api-utils/docs/generateFilterQuery.md | 149 +++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 packages/api-utils/docs/generateFilterQuery.md diff --git a/packages/api-plugin-products/README.md b/packages/api-plugin-products/README.md index 1642f84021b..900e2d923ee 100644 --- a/packages/api-plugin-products/README.md +++ b/packages/api-plugin-products/README.md @@ -8,6 +8,159 @@ Products plugin for the [Reaction API](https://github.com/reactioncommerce/reaction) +### Example on how to use Filter Conditions + +We have a query endpoint defined in this plugin which allows us to query products collection based on the input GraphQL conditions object. This query endpoint is defined as `filterSearchProducts.js` and it calls the `generateFilterQuery` function from the `api-utils` plugin to generate the MongoDB filter query. + +The `generateFilterQuery` function expects the input GraphQL conditions object to be in the format of the `FilterConditionsInput` input type defined in the GraphQL Schemas (in api-core plugin) along with other parameters like `context`, `collectionName` and `shopId`. + +Please go through a general introduction of how to use this function which can be found in the [api-utils README](https://github.com/reactioncommerce/reaction/tree/trunk/packages/api-utils/docs) before going through the examples below on how to use this function in the context of the `products` plugin. + +In the query endpoint, we pass the `FilterConditionsInput` input type object as the `conditions` argument. This object is passed to the `generateFilterQuery` function along with other parameters like `context`, `collectionName` and `shopId` to generate the MongoDB filter query. The `generateFilterQuery` function is generic and can be used to generate filter queries for any collection. Since the parametes like `context`, `collectionName` and `shopId` are pretty self-explanatory, we shall focus on explaining the various ways in which the `conditions` object can be used. + +1. Single condition. +Here we are querying products collection for entries with the handle as 'mens-waterproof-outdoor-rain-jacket'. Since it is single condition, using either `all` or `any` will not make difference. + +```js +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; + +const conditions = { + all: [ + { + any:[ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: eq, + logicalNOT: false + } + ] + } + ] + } + + const { filterQuery } = generateFilterQuery(context, "Product", conditions, shopId); + + return Products.find(filterQuery); +``` +
+ +2. Two conditions. + +Here we are querying products collection for entries which have either the handle as 'mens-waterproof-outdoor-rain-jacket' or 'title' begins with the text 'men'. Since we are using the `any` to connect the conditions, it translates to a mongo DB `$or` condition. Please note that the top level `all` condition is only to maintain the structure of the input GraphQL conditions object. It does not impact the results of the inner query. + + +```js +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; + +const conditions = { + all: [ + { + any:[ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: eq, + logicalNOT: false + }, + { + key: "title", + stringValue: "men", + relationalOperator: beginsWith, + logicalNOT: false + caseSensitive: false + } + ] + } + ] + } + + const { filterQuery } = generateFilterQuery(context, "Product", conditions, shopId); + + return Products.find(filterQuery); +``` +
+ +3. Multiple conditions. + +Here we are querying products collection for entries which confirms to multiple conditions. +We have 3 distinct group of conditions in the inner level and the results of all these 3 are joined at the top level with `all` meaning `$and` in MongoDB. + +The first group looks for entries matching either of the conditions `handle` as 'mens-waterproof-outdoor-rain-jacket' or `title` begins with the text 'men'. Since we are using the `any` to connect the conditions, it translates to a mongo DB `$or` condition. + +The second group looks for entries matching the `_id` in the array `["DZwLHk4EAzitRni8F", "Hn4BRaBvLkYffMq36"]` and `isDeleted` as `false` and `workflow.status` as `new`. Since we are using the `all` to connect the conditions, it translates to a mongo DB `$and` condition. + +The third group looks for entries matching the `price.min` greater than 19.99 and `type` as `simple`. Since we are using the `all` to connect the conditions, it translates to a mongo DB `$and` condition. + +As explained above, the final results are joined at the top level with `all` meaning `$and` in MongoDB. + +```js +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; + +const conditions = { + all: [ + { + any:[ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: eq, + logicalNOT: false + }, + { + key: "title", + stringValue: "men", + relationalOperator: beginsWith, + logicalNOT: false + caseSensitive: false + } + ] + }, + { + all:[ + { + key: "_id", + stringArrayValue: ["DZwLHk4EAzitRni8F", "Hn4BRaBvLkYffMq36"], + relationalOperator: in, + logicalNOT: false + }, + { + key: "isDeleted", + boolValue: false, + relationalOperator: eq, + logicalNOT: false + }, + { + key: "workflow.status", + stringValue: "new", + relationalOperator: eq, + logicalNOT: false + } + ] + }, + { + all:[ + { + key: "price.min", + floatValue: 19.99, + relationalOperator: gte, + logicalNOT: false + }, + { + key: "type", + stringValue: "simple", + relationalOperator: eq, + logicalNOT: false + } + ] + } + ] + } + + const { filterQuery } = generateFilterQuery(context, "Product", conditions, shopId); + + return Products.find(filterQuery); +``` ## Developer Certificate of Origin We use the [Developer Certificate of Origin (DCO)](https://developercertificate.org/) in lieu of a Contributor License Agreement for all contributions to Reaction Commerce open source projects. We request that contributors agree to the terms of the DCO and indicate that agreement by signing all commits made to Reaction Commerce projects by adding a line with your name and email address to every Git commit message contributed: ``` diff --git a/packages/api-utils/docs/README.md b/packages/api-utils/docs/README.md index cdb22096034..1924f4d1d5b 100644 --- a/packages/api-utils/docs/README.md +++ b/packages/api-utils/docs/README.md @@ -5,3 +5,4 @@ - [getAbsoluteUrl](./getAbsoluteUrl.md) - [getPaginatedResponseFromAggregate](./getPaginatedResponseFromAggregate.md) - [tagsForCatalogProducts](./tagsForCatalogProducts.md) +- [generateFilterQuery](./generateFilterQuery.md) diff --git a/packages/api-utils/docs/generateFilterQuery.md b/packages/api-utils/docs/generateFilterQuery.md new file mode 100644 index 00000000000..1bdb18bce2c --- /dev/null +++ b/packages/api-utils/docs/generateFilterQuery.md @@ -0,0 +1,149 @@ + +# generateFilterQuery + +A function that generates a MongoDB filter query from an input GraphQL conditions object. It expects the input GraphQL conditions object to be in the format of the `FilterConditionsInput` input type (detailed below) defined in the GraphQL Schemas along with other parameters like `context`, `collectionName` and `shopId`. + +As seen in the format below, +* The input object is a nested object where you could use either `all` or `any` keys. +* Both the top level `all` & `any` keys are an array of objects with one more level of `all` or `any` keys. +* The inner `all` or `any` keys is an array of objects with the structure defined by `SingleConditionInput`. +* The `SingleConditionInput` object has the fields which define a single condition to filter on. +
+* The `all` key is equivalent of the `$and` operator in MongoDB and the `any` key is equivalent of the `$or` operator in MongoDB. +* In the `SingleConditionInput` object, mandatory fields are the `key`, `relationalOperator` and exactly any ONE of the value fields. The `key` is the field name to filter on. The `relationalOperator` is the relational operator to use to filter on the field (predefined as enum values). The `stringValue`, `intValue`, `floatValue`, `boolValue`, `dateValue`, `stringArrayValue`, `intArrayValue`, `floatArrayValue` are the values to filter on (use exactly one of this) depending on the key. +* Finally there are two more optional fields `caseSensitive` and `logicalNOT`. The `caseSensitive` is a boolean flag to set if the regex is case sensitive. The `logicalNOT` is a boolean flag to set if the condition is to be negated. + +FilterConditionsInput format below (from GraphQL Schemas). __Example__ follows the format below: + + +```graphql + +"Filter search with nested conditions of input (use either 'any' or 'all' not both)" +input FilterConditionsInput { + "Array holding Nested conditions (use either 'any' or 'all' not both)" + all: [ConditionsArray] + + "Array holding Nested conditions (use either 'any' or 'all' not both)" + any: [ConditionsArray] +} + + +"Filter search with One level of conditions (use either 'any' or 'all' not both)" +input ConditionsArray { + "Array of single-conditions" + all: [SingleConditionInput] + + "Array of single-conditions" + any: [SingleConditionInput] +} + + +"Single Condition for filterSearch, use exactly one of the optional input value type" +input SingleConditionInput { + "Value to filter if it is Boolean input" + boolValue: Boolean + + "Flag to set if the regex is case insensitive" + caseSensitive: Boolean + + "Value to filter if it is Date input" + dateValue: DateTime + + "Value to filter if it is Float Array input" + floatArrayValue: [Float] + + "Value to filter if it is Float input" + floatValue: Float + + "Value to filter if it is Int Array input" + intArrayValue: [Int] + + "Value to filter if it is Int input" + intValue: Int + + "Field name" + key : String! + + "Logical NOT operator to negate the condition" + logicalNOT: Boolean + + "Relational Operator to join the key and value" + relationalOperator: RelationalOperatorTypes! + + "Value to filter if it is String Array input" + stringArrayValue: [String] + + "Value to filter if it is String input" + stringValue: String +} + + +"Relational Operator Types used in filtering inside a single condition" +enum RelationalOperatorTypes{ + "Begins With used with String types" + beginsWith + + "Ends With used with String types" + endsWith + + "Equal" + eq + + "Greater Than" + gt + + "Greater Than or Equal" + gte + + "In" + in + + "Less Than" + lt + + "Less Than or Equal" + lte + + "Not Equal" + ne + + "Not In" + nin + + "Regex" + regex +} + + +``` + + +## Example + +Example of invoking the function with a simple conditions object. Here we are querying for products collection for entries with the handle as 'mens-waterproof-outdoor-rain-jacket'. Since it is single condition, using either `all` or `any` will not make difference. + +```js +import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; + +const conditions = { + all: [ + { + any:[ + { + key: "handle", + stringValue: "mens-waterproof-outdoor-rain-jacket", + relationalOperator: eq, + logicalNOT: false + } + ] + } + ] + } + + const { filterQuery } = generateFilterQuery(context, "Product", conditions, shopId); + + return Products.find(filterQuery); +``` + +Please refer to readme in the respective plugins for more detailed examples (example: Products). + From 9dc4fe928214e1df09d1389a095ce3ecbe81b68e Mon Sep 17 00:00:00 2001 From: Sujith Date: Wed, 14 Dec 2022 11:39:37 +0530 Subject: [PATCH 07/11] fix: more review comment fixes Signed-off-by: Sujith --- packages/api-core/src/graphql/schema.graphql | 36 +++++----- ...terSearchAccounts.js => filterAccounts.js} | 4 +- ...rSearchCustomers.js => filterCustomers.js} | 4 +- .../api-plugin-accounts/src/queries/index.js | 8 +-- ...terSearchAccounts.js => filterAccounts.js} | 4 +- ...rSearchCustomers.js => filterCustomers.js} | 4 +- .../src/resolvers/Query/index.js | 8 +-- .../src/schemas/account.graphql | 4 +- ...{filterSearchOrders.js => filterOrders.js} | 4 +- .../api-plugin-orders/src/queries/index.js | 4 +- ...{filterSearchOrders.js => filterOrders.js} | 4 +- .../src/resolvers/Query/index.js | 4 +- .../src/schemas/schema.graphql | 2 +- packages/api-plugin-products/README.md | 24 +++---- ...terSearchProducts.js => filterProducts.js} | 4 +- .../api-plugin-products/src/queries/index.js | 4 +- ...terSearchProducts.js => filterProducts.js} | 4 +- .../src/resolvers/Query/index.js | 4 +- .../src/schemas/product.graphql | 2 +- .../api-utils/docs/generateFilterQuery.md | 71 +++---------------- packages/api-utils/lib/generateFilterQuery.js | 70 +++++++++--------- .../api-utils/lib/generateFilterQuery.test.js | 20 +++--- 22 files changed, 120 insertions(+), 173 deletions(-) rename packages/api-plugin-accounts/src/queries/{filterSearchAccounts.js => filterAccounts.js} (87%) rename packages/api-plugin-accounts/src/queries/{filterSearchCustomers.js => filterCustomers.js} (88%) rename packages/api-plugin-accounts/src/resolvers/Query/{filterSearchAccounts.js => filterAccounts.js} (86%) rename packages/api-plugin-accounts/src/resolvers/Query/{filterSearchCustomers.js => filterCustomers.js} (86%) rename packages/api-plugin-orders/src/queries/{filterSearchOrders.js => filterOrders.js} (87%) rename packages/api-plugin-orders/src/resolvers/Query/{filterSearchOrders.js => filterOrders.js} (86%) rename packages/api-plugin-products/src/queries/{filterSearchProducts.js => filterProducts.js} (87%) rename packages/api-plugin-products/src/resolvers/Query/{filterSearchProducts.js => filterProducts.js} (86%) diff --git a/packages/api-core/src/graphql/schema.graphql b/packages/api-core/src/graphql/schema.graphql index 4e149b27b0d..4a73c345e67 100644 --- a/packages/api-core/src/graphql/schema.graphql +++ b/packages/api-core/src/graphql/schema.graphql @@ -36,13 +36,13 @@ enum MassUnit { "Relational Operator Types used in filtering inside a single condition" enum RelationalOperatorTypes{ - "Begins With used with String types" + "Begins With used with String types to filter based on the beginning of the string" beginsWith - "Ends With used with String types" + "Ends With used with String types to filter based on the end of the string" endsWith - "Equal" + "Equal to" eq "Greater Than" @@ -51,7 +51,7 @@ enum RelationalOperatorTypes{ "Greater Than or Equal" gte - "In" + "In used with Array types to filter based on the array containing the value" in "Less Than" @@ -60,20 +60,20 @@ enum RelationalOperatorTypes{ "Less Than or Equal" lte - "Not Equal" + "Not Equal to" ne - "Not In" + "Not In used with Array types to filter based on the array not containing the value" nin - "Regex" + "Regex used with String types to filter based on the regex pattern" regex } -"Single Condition for filterSearch, use exactly one of the optional input value type" +"Single Condition for filter, use exactly one of the optional input value type" input SingleConditionInput { "Value to filter if it is Boolean input" - boolValue: Boolean + booleanValue: Boolean "Flag to set if the regex is case insensitive" caseSensitive: Boolean @@ -87,17 +87,17 @@ input SingleConditionInput { "Value to filter if it is Float input" floatValue: Float - "Value to filter if it is Int Array input" - intArrayValue: [Int] + "Value to filter if it is Integer Array input" + integerArrayValue: [Int] - "Value to filter if it is Int input" - intValue: Int + "Value to filter if it is Integer input" + integerValue: Int "Field name" - key : String! + key: String! "Logical NOT operator to negate the condition" - logicalNOT: Boolean + logicalNot: Boolean "Relational Operator to join the key and value" relationalOperator: RelationalOperatorTypes! @@ -106,10 +106,10 @@ input SingleConditionInput { stringArrayValue: [String] "Value to filter if it is String input" - stringValue: String + stringValue: String } -"Filter search with One level of conditions (use either 'any' or 'all' not both)" +"Filter with One level of conditions (use either 'any' or 'all' not both)" input ConditionsArray { "Array of single-conditions" all: [SingleConditionInput] @@ -118,7 +118,7 @@ input ConditionsArray { any: [SingleConditionInput] } -"Filter search with nested conditions of input (use either 'any' or 'all' not both)" +"Filter with nested conditions of input (use either 'any' or 'all' not both)" input FilterConditionsInput { "Array holding Nested conditions (use either 'any' or 'all' not both)" all: [ConditionsArray] diff --git a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js b/packages/api-plugin-accounts/src/queries/filterAccounts.js similarity index 87% rename from packages/api-plugin-accounts/src/queries/filterSearchAccounts.js rename to packages/api-plugin-accounts/src/queries/filterAccounts.js index 73420bf44a8..6fdaab29650 100644 --- a/packages/api-plugin-accounts/src/queries/filterSearchAccounts.js +++ b/packages/api-plugin-accounts/src/queries/filterAccounts.js @@ -1,7 +1,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** - * @name filterSearchAccounts + * @name filterAccounts * @method * @memberof GraphQL/Accounts * @summary Query the Accounts collection for a list of customers/accounts @@ -10,7 +10,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery * @param {String} shopId - shopID to filter by * @returns {Promise} Accounts object Promise */ -export default async function filterSearchAccounts(context, conditions, shopId) { +export default async function filterAccounts(context, conditions, shopId) { const { collections: { Accounts } } = context; if (!shopId) { diff --git a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js b/packages/api-plugin-accounts/src/queries/filterCustomers.js similarity index 88% rename from packages/api-plugin-accounts/src/queries/filterSearchCustomers.js rename to packages/api-plugin-accounts/src/queries/filterCustomers.js index 23bfb3debea..a9964d15550 100644 --- a/packages/api-plugin-accounts/src/queries/filterSearchCustomers.js +++ b/packages/api-plugin-accounts/src/queries/filterCustomers.js @@ -1,7 +1,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** - * @name filterSearchCustomers + * @name filterCustomers * @method * @memberof GraphQL/Customers * @summary Query the Accounts collection for a list of customers/accounts @@ -10,7 +10,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery * @param {String} shopId - shopID to filter by * @returns {Promise} Accounts object Promise */ -export default async function filterSearchCustomers(context, conditions, shopId) { +export default async function filterCustomers(context, conditions, shopId) { const { collections: { Accounts } } = context; if (!shopId) { diff --git a/packages/api-plugin-accounts/src/queries/index.js b/packages/api-plugin-accounts/src/queries/index.js index d2db6ec012e..4309cf66768 100644 --- a/packages/api-plugin-accounts/src/queries/index.js +++ b/packages/api-plugin-accounts/src/queries/index.js @@ -7,12 +7,12 @@ import groupsByAccount from "./groupsByAccount.js"; import groupsById from "./groupsById.js"; import invitations from "./invitations.js"; import userAccount from "./userAccount.js"; -import filterSearchAccounts from "./filterSearchAccounts.js"; -import filterSearchCustomers from "./filterSearchCustomers.js"; +import filterAccounts from "./filterAccounts.js"; +import filterCustomers from "./filterCustomers.js"; export default { - filterSearchAccounts, - filterSearchCustomers, + filterAccounts, + filterCustomers, accountByUserId, accounts, customers, diff --git a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js b/packages/api-plugin-accounts/src/resolvers/Query/filterAccounts.js similarity index 86% rename from packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js rename to packages/api-plugin-accounts/src/resolvers/Query/filterAccounts.js index 2d5f75fc497..bfab2f45c0f 100644 --- a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchAccounts.js +++ b/packages/api-plugin-accounts/src/resolvers/Query/filterAccounts.js @@ -14,14 +14,14 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} info Info about the GraphQL request * @returns {Promise} Accounts */ -export default async function filterSearchAccounts(_, args, context, info) { +export default async function filterAccounts(_, args, context, info) { const { shopId, conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchAccounts(context, conditions, shopId); + const query = await context.queries.filterAccounts(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js b/packages/api-plugin-accounts/src/resolvers/Query/filterCustomers.js similarity index 86% rename from packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js rename to packages/api-plugin-accounts/src/resolvers/Query/filterCustomers.js index 6d9e9556ab3..490037732e4 100644 --- a/packages/api-plugin-accounts/src/resolvers/Query/filterSearchCustomers.js +++ b/packages/api-plugin-accounts/src/resolvers/Query/filterCustomers.js @@ -14,14 +14,14 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} info Info about the GraphQL request * @returns {Promise} Accounts */ -export default async function filterSearchCustomers(_, args, context, info) { +export default async function filterCustomers(_, args, context, info) { const { shopId, conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchCustomers(context, conditions, shopId); + const query = await context.queries.filterCustomers(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-accounts/src/resolvers/Query/index.js b/packages/api-plugin-accounts/src/resolvers/Query/index.js index 41c93611511..96033bee33d 100644 --- a/packages/api-plugin-accounts/src/resolvers/Query/index.js +++ b/packages/api-plugin-accounts/src/resolvers/Query/index.js @@ -5,12 +5,12 @@ import group from "./group.js"; import groups from "./groups.js"; import invitations from "./invitations.js"; import viewer from "./viewer.js"; -import filterSearchAccounts from "./filterSearchAccounts.js"; -import filterSearchCustomers from "./filterSearchCustomers.js"; +import filterAccounts from "./filterAccounts.js"; +import filterCustomers from "./filterCustomers.js"; export default { - filterSearchAccounts, - filterSearchCustomers, + filterAccounts, + filterCustomers, account, accounts, customers, diff --git a/packages/api-plugin-accounts/src/schemas/account.graphql b/packages/api-plugin-accounts/src/schemas/account.graphql index de1c66a81c6..785ddbb0c90 100644 --- a/packages/api-plugin-accounts/src/schemas/account.graphql +++ b/packages/api-plugin-accounts/src/schemas/account.graphql @@ -497,7 +497,7 @@ extend type Query { ): Account "Query to get a filtered list of Accounts" - filterSearchAccounts( + filterAccounts( "Shop ID" shopId: ID!, @@ -527,7 +527,7 @@ extend type Query { ): AccountConnection "Query to get a filtered list of Customers" - filterSearchCustomers( + filterCustomers( "Shop ID" shopId: ID!, diff --git a/packages/api-plugin-orders/src/queries/filterSearchOrders.js b/packages/api-plugin-orders/src/queries/filterOrders.js similarity index 87% rename from packages/api-plugin-orders/src/queries/filterSearchOrders.js rename to packages/api-plugin-orders/src/queries/filterOrders.js index 74fca73c746..0e4dd714f54 100644 --- a/packages/api-plugin-orders/src/queries/filterSearchOrders.js +++ b/packages/api-plugin-orders/src/queries/filterOrders.js @@ -1,7 +1,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** - * @name filterSearchOrders + * @name filterOrders * @method * @memberof GraphQL/Orders * @summary Query the Orders collection for a list of orders @@ -10,7 +10,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery * @param {String} shopId - shopID to filter by * @returns {Promise} Orders object Promise */ -export default async function filterSearchOrders(context, conditions, shopId) { +export default async function filterOrders(context, conditions, shopId) { const { collections: { Orders } } = context; if (!shopId) { diff --git a/packages/api-plugin-orders/src/queries/index.js b/packages/api-plugin-orders/src/queries/index.js index b32482056e2..7d1bfd3d93c 100644 --- a/packages/api-plugin-orders/src/queries/index.js +++ b/packages/api-plugin-orders/src/queries/index.js @@ -4,10 +4,10 @@ import orders from "./orders.js"; import ordersByAccountId from "./ordersByAccountId.js"; import refunds from "./refunds.js"; import refundsByPaymentId from "./refundsByPaymentId.js"; -import filterSearchOrders from "./filterSearchOrders.js"; +import filterOrders from "./filterOrders.js"; export default { - filterSearchOrders, + filterOrders, orderById, orderByReferenceId, orders, diff --git a/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js b/packages/api-plugin-orders/src/resolvers/Query/filterOrders.js similarity index 86% rename from packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js rename to packages/api-plugin-orders/src/resolvers/Query/filterOrders.js index 44af89d5bd1..7e35413bfad 100644 --- a/packages/api-plugin-orders/src/resolvers/Query/filterSearchOrders.js +++ b/packages/api-plugin-orders/src/resolvers/Query/filterOrders.js @@ -14,14 +14,14 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} info Info about the GraphQL request * @returns {Promise} Orders */ -export default async function filterSearchOrders(_, args, context, info) { +export default async function filterOrders(_, args, context, info) { const { shopId, conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchOrders(context, conditions, shopId); + const query = await context.queries.filterOrders(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-orders/src/resolvers/Query/index.js b/packages/api-plugin-orders/src/resolvers/Query/index.js index b32482056e2..7d1bfd3d93c 100644 --- a/packages/api-plugin-orders/src/resolvers/Query/index.js +++ b/packages/api-plugin-orders/src/resolvers/Query/index.js @@ -4,10 +4,10 @@ import orders from "./orders.js"; import ordersByAccountId from "./ordersByAccountId.js"; import refunds from "./refunds.js"; import refundsByPaymentId from "./refundsByPaymentId.js"; -import filterSearchOrders from "./filterSearchOrders.js"; +import filterOrders from "./filterOrders.js"; export default { - filterSearchOrders, + filterOrders, orderById, orderByReferenceId, orders, diff --git a/packages/api-plugin-orders/src/schemas/schema.graphql b/packages/api-plugin-orders/src/schemas/schema.graphql index 94c8769d601..439d7ace624 100644 --- a/packages/api-plugin-orders/src/schemas/schema.graphql +++ b/packages/api-plugin-orders/src/schemas/schema.graphql @@ -1,6 +1,6 @@ extend type Query { "Query to get a filtered list of Orders" - filterSearchOrders( + filterOrders( "Shop ID" shopId: ID!, diff --git a/packages/api-plugin-products/README.md b/packages/api-plugin-products/README.md index 900e2d923ee..c8c9bc30559 100644 --- a/packages/api-plugin-products/README.md +++ b/packages/api-plugin-products/README.md @@ -10,7 +10,7 @@ Products plugin for the [Reaction API](https://github.com/reactioncommerce/react ### Example on how to use Filter Conditions -We have a query endpoint defined in this plugin which allows us to query products collection based on the input GraphQL conditions object. This query endpoint is defined as `filterSearchProducts.js` and it calls the `generateFilterQuery` function from the `api-utils` plugin to generate the MongoDB filter query. +We have a query endpoint defined in this plugin which allows us to query products collection based on the input GraphQL conditions object. This query endpoint is defined as `filterProducts.js` and it calls the `generateFilterQuery` function from the `api-utils` plugin to generate the MongoDB filter query. The `generateFilterQuery` function expects the input GraphQL conditions object to be in the format of the `FilterConditionsInput` input type defined in the GraphQL Schemas (in api-core plugin) along with other parameters like `context`, `collectionName` and `shopId`. @@ -32,7 +32,7 @@ const conditions = { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: eq, - logicalNOT: false + logicalNot: false } ] } @@ -61,13 +61,13 @@ const conditions = { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: eq, - logicalNOT: false + logicalNot: false }, { key: "title", stringValue: "men", relationalOperator: beginsWith, - logicalNOT: false + logicalNot: false caseSensitive: false } ] @@ -105,13 +105,13 @@ const conditions = { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: eq, - logicalNOT: false + logicalNot: false }, { key: "title", stringValue: "men", relationalOperator: beginsWith, - logicalNOT: false + logicalNot: false caseSensitive: false } ] @@ -122,19 +122,19 @@ const conditions = { key: "_id", stringArrayValue: ["DZwLHk4EAzitRni8F", "Hn4BRaBvLkYffMq36"], relationalOperator: in, - logicalNOT: false + logicalNot: false }, { key: "isDeleted", - boolValue: false, + booleanValue: false, relationalOperator: eq, - logicalNOT: false + logicalNot: false }, { key: "workflow.status", stringValue: "new", relationalOperator: eq, - logicalNOT: false + logicalNot: false } ] }, @@ -144,13 +144,13 @@ const conditions = { key: "price.min", floatValue: 19.99, relationalOperator: gte, - logicalNOT: false + logicalNot: false }, { key: "type", stringValue: "simple", relationalOperator: eq, - logicalNOT: false + logicalNot: false } ] } diff --git a/packages/api-plugin-products/src/queries/filterSearchProducts.js b/packages/api-plugin-products/src/queries/filterProducts.js similarity index 87% rename from packages/api-plugin-products/src/queries/filterSearchProducts.js rename to packages/api-plugin-products/src/queries/filterProducts.js index 1d7a80c4f27..3944075da71 100644 --- a/packages/api-plugin-products/src/queries/filterSearchProducts.js +++ b/packages/api-plugin-products/src/queries/filterProducts.js @@ -1,7 +1,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery.js"; /** - * @name filterSearchProducts + * @name filterProducts * @method * @memberof GraphQL/Products * @summary Query the Products collection for a list of products @@ -10,7 +10,7 @@ import generateFilterQuery from "@reactioncommerce/api-utils/generateFilterQuery * @param {String} shopId - shopID to filter by * @returns {Promise} Products object Promise */ -export default async function filterSearchProducts(context, conditions, shopId) { +export default async function filterProducts(context, conditions, shopId) { const { collections: { Products } } = context; if (!shopId) { diff --git a/packages/api-plugin-products/src/queries/index.js b/packages/api-plugin-products/src/queries/index.js index 022a30b6a5e..64f5c844918 100644 --- a/packages/api-plugin-products/src/queries/index.js +++ b/packages/api-plugin-products/src/queries/index.js @@ -1,9 +1,9 @@ import product from "./product.js"; import products from "./products.js"; -import filterSearchProducts from "./filterSearchProducts.js"; +import filterProducts from "./filterProducts.js"; export default { - filterSearchProducts, + filterProducts, product, products }; diff --git a/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js b/packages/api-plugin-products/src/resolvers/Query/filterProducts.js similarity index 86% rename from packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js rename to packages/api-plugin-products/src/resolvers/Query/filterProducts.js index 0236c14ce17..b5b7071c4da 100644 --- a/packages/api-plugin-products/src/resolvers/Query/filterSearchProducts.js +++ b/packages/api-plugin-products/src/resolvers/Query/filterProducts.js @@ -14,14 +14,14 @@ import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldReque * @param {Object} info Info about the GraphQL request * @returns {Promise} Products */ -export default async function filterSearchProducts(_, args, context, info) { +export default async function filterProducts(_, args, context, info) { const { shopId, conditions, ...connectionArgs } = args; - const query = await context.queries.filterSearchProducts(context, conditions, shopId); + const query = await context.queries.filterProducts(context, conditions, shopId); return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), diff --git a/packages/api-plugin-products/src/resolvers/Query/index.js b/packages/api-plugin-products/src/resolvers/Query/index.js index 6c21521fab4..8f101b0bcd3 100644 --- a/packages/api-plugin-products/src/resolvers/Query/index.js +++ b/packages/api-plugin-products/src/resolvers/Query/index.js @@ -1,9 +1,9 @@ import product from "./product.js"; import products from "./products.js"; -import filterSearchProducts from "./filterSearchProducts.js"; +import filterProducts from "./filterProducts.js"; export default { product, products, - filterSearchProducts + filterProducts }; diff --git a/packages/api-plugin-products/src/schemas/product.graphql b/packages/api-plugin-products/src/schemas/product.graphql index 6d76850d9f8..76b6fb273ac 100644 --- a/packages/api-plugin-products/src/schemas/product.graphql +++ b/packages/api-plugin-products/src/schemas/product.graphql @@ -626,7 +626,7 @@ extend type Mutation { extend type Query { "Query to get a filtered list of Products" - filterSearchProducts( + filterProducts( "Shop ID" shopId: ID!, diff --git a/packages/api-utils/docs/generateFilterQuery.md b/packages/api-utils/docs/generateFilterQuery.md index 1bdb18bce2c..da84b2ddcd1 100644 --- a/packages/api-utils/docs/generateFilterQuery.md +++ b/packages/api-utils/docs/generateFilterQuery.md @@ -10,111 +10,58 @@ As seen in the format below, * The `SingleConditionInput` object has the fields which define a single condition to filter on.
* The `all` key is equivalent of the `$and` operator in MongoDB and the `any` key is equivalent of the `$or` operator in MongoDB. -* In the `SingleConditionInput` object, mandatory fields are the `key`, `relationalOperator` and exactly any ONE of the value fields. The `key` is the field name to filter on. The `relationalOperator` is the relational operator to use to filter on the field (predefined as enum values). The `stringValue`, `intValue`, `floatValue`, `boolValue`, `dateValue`, `stringArrayValue`, `intArrayValue`, `floatArrayValue` are the values to filter on (use exactly one of this) depending on the key. -* Finally there are two more optional fields `caseSensitive` and `logicalNOT`. The `caseSensitive` is a boolean flag to set if the regex is case sensitive. The `logicalNOT` is a boolean flag to set if the condition is to be negated. +* In the `SingleConditionInput` object, mandatory fields are the `key`, `relationalOperator` and exactly any ONE of the value fields. The `key` is the field name to filter on. The `relationalOperator` is the relational operator to use to filter on the field (predefined as enum values). The `stringValue`, `integerValue`, `floatValue`, `booleanValue`, `dateValue`, `stringArrayValue`, `integerArrayValue`, `floatArrayValue` are the values to filter on (use exactly one of this) depending on the key. +* Finally there are two more optional fields `caseSensitive` and `logicalNot`. The `caseSensitive` is a boolean flag to set if the regex is case sensitive. The `logicalNot` is a boolean flag to set if the condition is to be negated. FilterConditionsInput format below (from GraphQL Schemas). __Example__ follows the format below: ```graphql -"Filter search with nested conditions of input (use either 'any' or 'all' not both)" input FilterConditionsInput { - "Array holding Nested conditions (use either 'any' or 'all' not both)" all: [ConditionsArray] - "Array holding Nested conditions (use either 'any' or 'all' not both)" any: [ConditionsArray] } -"Filter search with One level of conditions (use either 'any' or 'all' not both)" input ConditionsArray { - "Array of single-conditions" all: [SingleConditionInput] - "Array of single-conditions" any: [SingleConditionInput] } -"Single Condition for filterSearch, use exactly one of the optional input value type" input SingleConditionInput { - "Value to filter if it is Boolean input" - boolValue: Boolean - - "Flag to set if the regex is case insensitive" + booleanValue: Boolean caseSensitive: Boolean - - "Value to filter if it is Date input" dateValue: DateTime - - "Value to filter if it is Float Array input" floatArrayValue: [Float] - - "Value to filter if it is Float input" floatValue: Float - - "Value to filter if it is Int Array input" - intArrayValue: [Int] - - "Value to filter if it is Int input" - intValue: Int - - "Field name" - key : String! - - "Logical NOT operator to negate the condition" - logicalNOT: Boolean - - "Relational Operator to join the key and value" + integerArrayValue: [Int] + integerValue: Int + key: String! + logicalNot: Boolean relationalOperator: RelationalOperatorTypes! - - "Value to filter if it is String Array input" stringArrayValue: [String] - - "Value to filter if it is String input" stringValue: String } -"Relational Operator Types used in filtering inside a single condition" enum RelationalOperatorTypes{ - "Begins With used with String types" beginsWith - - "Ends With used with String types" endsWith - - "Equal" eq - - "Greater Than" gt - - "Greater Than or Equal" gte - - "In" in - - "Less Than" lt - - "Less Than or Equal" lte - - "Not Equal" ne - - "Not In" nin - - "Regex" regex } - ``` @@ -133,7 +80,7 @@ const conditions = { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: eq, - logicalNOT: false + logicalNot: false } ] } @@ -145,5 +92,5 @@ const conditions = { return Products.find(filterQuery); ``` -Please refer to readme in the respective plugins for more detailed examples (example: Products). +Please refer to readme in the respective plugins for more detailed examples (example: api-plugin-products). diff --git a/packages/api-utils/lib/generateFilterQuery.js b/packages/api-utils/lib/generateFilterQuery.js index 4046cbb8b9c..15850f43666 100644 --- a/packages/api-utils/lib/generateFilterQuery.js +++ b/packages/api-utils/lib/generateFilterQuery.js @@ -10,7 +10,7 @@ const SingleConditionSchema = new SimpleSchema({ type: String, optional: true }, - "intValue": { + "integerValue": { type: SimpleSchema.Integer, optional: true }, @@ -18,7 +18,7 @@ const SingleConditionSchema = new SimpleSchema({ type: Number, optional: true }, - "boolValue": { + "booleanValue": { type: Boolean, optional: true }, @@ -33,11 +33,11 @@ const SingleConditionSchema = new SimpleSchema({ "stringArrayValue.$": { type: String }, - "intArrayValue": { + "integerArrayValue": { type: Array, optional: true }, - "intArrayValue.$": { + "integerArrayValue.$": { type: SimpleSchema.Integer }, "floatArrayValue": { @@ -51,7 +51,7 @@ const SingleConditionSchema = new SimpleSchema({ type: String, allowedValues: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "beginsWith", "endsWith"] }, - "logicalNOT": { + "logicalNot": { type: Boolean, optional: true }, @@ -125,9 +125,9 @@ const validCombos = { const REL_OPS_KEYS = ["any", "all"]; const FIELD_KEYS = [ - "key", "stringValue", "boolValue", "intValue", "floatValue", "dateValue", - "stringArrayValue", "intArrayValue", "floatArrayValue", - "relationalOperator", "caseSensitive", "logicalNOT" + "key", "stringValue", "booleanValue", "integerValue", "floatValue", "dateValue", + "stringArrayValue", "integerArrayValue", "floatArrayValue", + "relationalOperator", "caseSensitive", "logicalNot" ]; const keyMap = { @@ -280,12 +280,12 @@ function countInputValueFields(inputValue) { function validateConditions(allConditions, allCollectionFields) { for (const condition of allConditions) { const { - key, stringValue, intValue, floatValue, boolValue, dateValue, - stringArrayValue, intArrayValue, floatArrayValue, relationalOperator - } = condition; // logicalNOT, caseSensitive are optional + key, stringValue, integerValue, floatValue, booleanValue, dateValue, + stringArrayValue, integerArrayValue, floatArrayValue, relationalOperator + } = condition; // logicalNot, caseSensitive are optional const expectedValueType = allCollectionFields[key]; - const inputValuesObject = { stringValue, intValue, floatValue, boolValue, dateValue, stringArrayValue, intArrayValue, floatArrayValue }; + const inputValuesObject = { stringValue, integerValue, floatValue, booleanValue, dateValue, stringArrayValue, integerArrayValue, floatArrayValue }; const inputValuesCount = countInputValueFields(inputValuesObject); if (inputValuesCount > 1) { throw new Error(`Only one value must be provided for the condition with key: ${key}`); @@ -299,12 +299,12 @@ function validateConditions(allConditions, allCollectionFields) { // if expectedValueType does not match the type of value, throw error if (expectedValueType === "SimpleSchema.String" && stringValue === undefined && stringArrayValue === undefined) { throw new Error(`Key '${key}' expects either stringValue & stringArrayValue`); - } else if (expectedValueType === "SimpleSchema.Integer" && intValue === undefined && intArrayValue === undefined) { - throw new Error(`Key '${key}' expects either intValue & intArrayValue`); + } else if (expectedValueType === "SimpleSchema.Integer" && integerValue === undefined && integerArrayValue === undefined) { + throw new Error(`Key '${key}' expects either integerValue & integerArrayValue`); } else if (expectedValueType === "SimpleSchema.Number" && floatValue === undefined && floatArrayValue === undefined) { throw new Error(`Key '${key}' expects either floatValue & floatArrayValue`); - } else if (expectedValueType === "SimpleSchema.Boolean" && boolValue === undefined) { - throw new Error(`Key '${key}' expects boolValue`); + } else if (expectedValueType === "SimpleSchema.Boolean" && booleanValue === undefined) { + throw new Error(`Key '${key}' expects booleanValue`); } else if (expectedValueType === "SimpleSchema.Date" && dateValue === undefined) { throw new Error(`Key '${key}' expects dateValue`); } // array can be compared with any of the above types, skipping this check @@ -313,7 +313,7 @@ function validateConditions(allConditions, allCollectionFields) { throw new Error(`Invalid relational operator '${relationalOperator}' for : ${expectedValueType}`); } - if (expectedValueType === "SimpleSchema.Array" && stringArrayValue?.length === 0 && intArrayValue?.length === 0 && floatArrayValue?.length === 0) { + if (expectedValueType === "SimpleSchema.Array" && stringArrayValue?.length === 0 && integerArrayValue?.length === 0 && floatArrayValue?.length === 0) { throw new Error("Array value cannot be empty"); } } @@ -328,40 +328,40 @@ function validateConditions(allConditions, allCollectionFields) { * @param {Object} condition The condition to convert * @param {String} condition.key The key to convert * @param {String} condition.stringValue The value in String format - * @param {Number} condition.intValue The value in Integer format + * @param {Number} condition.integerValue The value in Integer format * @param {Number} condition.floatValue The value in Integer format - * @param {Boolean} condition.boolValue The value in Boolean format + * @param {Boolean} condition.booleanValue The value in Boolean format * @param {String} condition.dateValue The value in Date/String format * @param {String[]} [condition.stringArrayValue] The value in String Array format - * @param {Number[]} [condition.intArrayValue] The value in Integer Array format + * @param {Number[]} [condition.integerArrayValue] The value in Integer Array format * @param {Number[]} [condition.floatArrayValue] The value in Integer Array format * @param {String} condition.relationalOperator The relational operator to use - * @param {String} condition.logicalNOT Whether to negate the condition + * @param {String} condition.logicalNot Whether to negate the condition * @param {String} condition.caseSensitive Whether regex search is caseSensitive * @returns {Object} The MongoDB query */ function simpleConditionToQuery(condition) { const { - key, stringValue, intValue, floatValue, boolValue, dateValue, - stringArrayValue, intArrayValue, floatArrayValue, - relationalOperator, logicalNOT, caseSensitive + key, stringValue, integerValue, floatValue, booleanValue, dateValue, + stringArrayValue, integerArrayValue, floatArrayValue, + relationalOperator, logicalNot, caseSensitive } = condition; const query = {}; - const valueToUse = stringValue || intValue || floatValue || boolValue || dateValue || - stringArrayValue || intArrayValue || floatArrayValue; + const valueToUse = stringValue || integerValue || floatValue || booleanValue || dateValue || + stringArrayValue || integerArrayValue || floatArrayValue; let tempQuery; switch (relationalOperator) { case "eq": - if (boolValue !== undefined) { - tempQuery = { $eq: boolValue }; + if (booleanValue !== undefined) { + tempQuery = { $eq: booleanValue }; } else { tempQuery = { $eq: valueToUse }; } break; case "ne": - if (boolValue !== undefined) { - tempQuery = { $ne: boolValue }; + if (booleanValue !== undefined) { + tempQuery = { $ne: booleanValue }; } else { tempQuery = { $ne: valueToUse }; } @@ -408,7 +408,7 @@ function simpleConditionToQuery(condition) { throw new Error(`Invalid relational operator: ${relationalOperator}`); } - query[key] = logicalNOT ? { $not: tempQuery } : tempQuery; + query[key] = logicalNot ? { $not: tempQuery } : tempQuery; return query; } @@ -492,7 +492,7 @@ function processFilterConditions(filterCondition) { * @name generateQuery * @method * @memberof GraphQL/Filter - * @summary Builds a selector for Products collection, given a set of filters + * @summary Builds a selector for given collection, given a set of filters * @param {Object} filterQuery - an object containing the filters to apply * @param {String} shopId - the shop ID * @returns {Object} - selector @@ -522,15 +522,15 @@ function generateQuery(filterQuery, shopId) { } /** - * @name filterSearchProducts + * @name generateFilterQuery * @method * @memberof GraphQL/Filter - * @summary Query the Products collection for a list of products + * @summary Generates a filter Query for the collection in params based on incoming conditions * @param {Object} context - an object containing the per-request state * @param {String} collectionName - Collection against which to run the query * @param {Object} conditions - the conditions for the filter * @param {String} shopId - shopID to filter by - * @returns {Promise} Products object Promise + * @returns {Object} Filter query object */ export default function generateFilterQuery(context, collectionName, conditions, shopId) { ConditionsArraySchema.validate(conditions); diff --git a/packages/api-utils/lib/generateFilterQuery.test.js b/packages/api-utils/lib/generateFilterQuery.test.js index 08accdf0c25..b725c611bd9 100644 --- a/packages/api-utils/lib/generateFilterQuery.test.js +++ b/packages/api-utils/lib/generateFilterQuery.test.js @@ -66,7 +66,7 @@ test("returns the correct Query when single condition is given", () => { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: "eq", - logicalNOT: false + logicalNot: false } ] }] @@ -101,13 +101,13 @@ test("returns the correct Query when two conditions are given", () => { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: "eq", - logicalNOT: false + logicalNot: false }, { key: "_id", stringValue: "DZwLHk4EAzitRni8F", relationalOperator: "eq", - logicalNOT: false + logicalNot: false } ] } @@ -150,13 +150,13 @@ test("returns the correct Query when multiple conditions are given", () => { key: "handle", stringValue: "mens-waterproof-outdoor-rain-jacket", relationalOperator: "eq", - logicalNOT: false + logicalNot: false }, { key: "title", stringValue: "men", relationalOperator: "beginsWith", - logicalNOT: false, + logicalNot: false, caseSensitive: false } ] @@ -167,19 +167,19 @@ test("returns the correct Query when multiple conditions are given", () => { key: "_id", stringArrayValue: ["DZwLHk4EAzitRni8F", "Hn4BRaBvLkYffMq36"], relationalOperator: "in", - logicalNOT: false + logicalNot: false }, { key: "isDeleted", boolValue: false, relationalOperator: "eq", - logicalNOT: false + logicalNot: false }, { key: "workflow.status", stringValue: "new", relationalOperator: "eq", - logicalNOT: false + logicalNot: false } ] }, @@ -189,13 +189,13 @@ test("returns the correct Query when multiple conditions are given", () => { key: "price.min", floatValue: 19.99, relationalOperator: "gte", - logicalNOT: false + logicalNot: false }, { key: "type", stringValue: "simple", relationalOperator: "eq", - logicalNOT: false + logicalNot: false } ] } From 8c72369f2c8a7284c3199246705f90db4a9d4757 Mon Sep 17 00:00:00 2001 From: Sujith Date: Wed, 14 Dec 2022 11:46:02 +0530 Subject: [PATCH 08/11] fix: fixed 1 missed entry in test case Signed-off-by: Sujith --- packages/api-utils/lib/generateFilterQuery.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api-utils/lib/generateFilterQuery.test.js b/packages/api-utils/lib/generateFilterQuery.test.js index b725c611bd9..dbda4c44d7d 100644 --- a/packages/api-utils/lib/generateFilterQuery.test.js +++ b/packages/api-utils/lib/generateFilterQuery.test.js @@ -171,7 +171,7 @@ test("returns the correct Query when multiple conditions are given", () => { }, { key: "isDeleted", - boolValue: false, + booleanValue: false, relationalOperator: "eq", logicalNot: false }, From 0bc6279baabd2cea3882ab6b91010f28b9239bab Mon Sep 17 00:00:00 2001 From: Sujith Date: Thu, 15 Dec 2022 11:19:27 +0530 Subject: [PATCH 09/11] fix: upgrade version of api-util and simp-schema Signed-off-by: Sujith --- packages/api-plugin-accounts/package.json | 2 +- packages/api-plugin-orders/package.json | 2 +- packages/api-plugin-products/package.json | 2 +- packages/api-utils/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api-plugin-accounts/package.json b/packages/api-plugin-accounts/package.json index 4a0869bc977..17c945056a7 100644 --- a/packages/api-plugin-accounts/package.json +++ b/packages/api-plugin-accounts/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "dependencies": { - "@reactioncommerce/api-utils": "^1.16.9", + "@reactioncommerce/api-utils": "^1.17.0", "@reactioncommerce/db-version-check": "^1.0.0", "@reactioncommerce/logger": "^1.1.3", "@reactioncommerce/random": "^1.0.2", diff --git a/packages/api-plugin-orders/package.json b/packages/api-plugin-orders/package.json index 940f4b8b9a2..3d8caecbcfa 100644 --- a/packages/api-plugin-orders/package.json +++ b/packages/api-plugin-orders/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "dependencies": { - "@reactioncommerce/api-utils": "^1.16.5", + "@reactioncommerce/api-utils": "^1.17.0", "@reactioncommerce/logger": "^1.1.4", "@reactioncommerce/random": "^1.0.2", "@reactioncommerce/reaction-error": "^1.0.1", diff --git a/packages/api-plugin-products/package.json b/packages/api-plugin-products/package.json index e61f74406ad..1076ae30119 100644 --- a/packages/api-plugin-products/package.json +++ b/packages/api-plugin-products/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "dependencies": { - "@reactioncommerce/api-utils": "^1.16.5", + "@reactioncommerce/api-utils": "^1.17.0", "@reactioncommerce/logger": "^1.1.3", "@reactioncommerce/random": "^1.0.2", "@reactioncommerce/reaction-error": "^1.0.1", diff --git a/packages/api-utils/package.json b/packages/api-utils/package.json index d4ba026db29..2672ebb5c8f 100644 --- a/packages/api-utils/package.json +++ b/packages/api-utils/package.json @@ -52,7 +52,7 @@ "lodash": "^4.17.15", "ramda": "^0.28.0", "transliteration": "^2.1.9", - "simpl-schema": "^1.12.0" + "simpl-schema": "^3.4.0" }, "devDependencies": { "@babel/core": "^7.9.6", From f16ad9221e28605df9c6bdc164569a8b40cd61fe Mon Sep 17 00:00:00 2001 From: Sujith Date: Thu, 15 Dec 2022 23:19:08 +0530 Subject: [PATCH 10/11] fix: update api-util and pnpm-lock Signed-off-by: Sujith --- packages/api-plugin-accounts/package.json | 2 +- packages/api-plugin-orders/package.json | 2 +- packages/api-plugin-products/package.json | 2 +- pnpm-lock.yaml | 23 ++++++++++++++++++----- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/api-plugin-accounts/package.json b/packages/api-plugin-accounts/package.json index 17c945056a7..71d0bd35c3f 100644 --- a/packages/api-plugin-accounts/package.json +++ b/packages/api-plugin-accounts/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "dependencies": { - "@reactioncommerce/api-utils": "^1.17.0", + "@reactioncommerce/api-utils": "~1.17.1", "@reactioncommerce/db-version-check": "^1.0.0", "@reactioncommerce/logger": "^1.1.3", "@reactioncommerce/random": "^1.0.2", diff --git a/packages/api-plugin-orders/package.json b/packages/api-plugin-orders/package.json index 3d8caecbcfa..a5f0da740ea 100644 --- a/packages/api-plugin-orders/package.json +++ b/packages/api-plugin-orders/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "dependencies": { - "@reactioncommerce/api-utils": "^1.17.0", + "@reactioncommerce/api-utils": "~1.17.1", "@reactioncommerce/logger": "^1.1.4", "@reactioncommerce/random": "^1.0.2", "@reactioncommerce/reaction-error": "^1.0.1", diff --git a/packages/api-plugin-products/package.json b/packages/api-plugin-products/package.json index 1076ae30119..25952ec9191 100644 --- a/packages/api-plugin-products/package.json +++ b/packages/api-plugin-products/package.json @@ -26,7 +26,7 @@ }, "sideEffects": false, "dependencies": { - "@reactioncommerce/api-utils": "^1.17.0", + "@reactioncommerce/api-utils": "~1.17.1", "@reactioncommerce/logger": "^1.1.3", "@reactioncommerce/random": "^1.0.2", "@reactioncommerce/reaction-error": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6e3ac324a6..2d1cd0c444f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,7 +332,7 @@ importers: specifiers: '@babel/core': ^7.7.7 '@babel/preset-env': ^7.7.7 - '@reactioncommerce/api-utils': ^1.16.9 + '@reactioncommerce/api-utils': ~1.17.1 '@reactioncommerce/babel-remove-es-create-require': ~1.0.0 '@reactioncommerce/data-factory': ~1.0.1 '@reactioncommerce/db-version-check': ^1.0.0 @@ -817,7 +817,7 @@ importers: '@babel/preset-env': ^7.7.7 '@reactioncommerce/api-plugin-catalogs': ^1.0.0 '@reactioncommerce/api-plugin-shops': ^1.0.0 - '@reactioncommerce/api-utils': ^1.16.5 + '@reactioncommerce/api-utils': ~1.17.1 '@reactioncommerce/babel-remove-es-create-require': ~1.0.0 '@reactioncommerce/data-factory': ^1.0.1 '@reactioncommerce/logger': ^1.1.4 @@ -968,7 +968,7 @@ importers: specifiers: '@babel/core': ^7.7.7 '@babel/preset-env': ^7.7.7 - '@reactioncommerce/api-utils': ^1.16.5 + '@reactioncommerce/api-utils': ~1.17.1 '@reactioncommerce/babel-remove-es-create-require': ~1.0.0 '@reactioncommerce/data-factory': ~1.0.1 '@reactioncommerce/logger': ^1.1.3 @@ -1331,7 +1331,7 @@ importers: graphql-relay: ^0.9.0 lodash: ^4.17.15 ramda: ^0.28.0 - simpl-schema: ^1.12.0 + simpl-schema: ^3.4.0 transliteration: ^2.1.9 dependencies: '@jest/globals': 26.6.2 @@ -1345,7 +1345,7 @@ importers: graphql-relay: 0.9.0_graphql@14.7.0 lodash: 4.17.21 ramda: 0.28.0 - simpl-schema: 1.12.3 + simpl-schema: 3.4.0 transliteration: 2.3.5 devDependencies: '@babel/core': 7.19.0 @@ -11336,6 +11336,11 @@ packages: /mongo-object/0.1.4: resolution: {integrity: sha512-QtYk0gupWEn2+iB+DDRt1L+WbcNYvJRaHdih/dcqthOa1DbnREUGSs2WGcW478GNYpElflo/yybZXu0sTiRXHg==} + /mongo-object/3.0.0: + resolution: {integrity: sha512-dDF7V0k+55s6YOjrF294GrE5s81z6RHR/YNkXj9mKcAK9hlL0Os0FRHtpinDHCqhEqImdLJUosIJ5lbYsCwbfA==} + engines: {node: '>=14.16'} + dev: false + /mongodb-connection-string-url/2.5.3: resolution: {integrity: sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==} dependencies: @@ -13037,6 +13042,14 @@ packages: message-box: 0.2.7 mongo-object: 0.1.4 + /simpl-schema/3.4.0: + resolution: {integrity: sha512-TUwGiWaRR6SnUYM/ULmVmBIsLsdWMe8QzJqsvhu8ztr+HWZFTY+VVQy03BjRhJ8o4vL9CSazxWrMiARJ4vtYMQ==} + engines: {node: '>=14.16', npm: '>=8'} + dependencies: + clone: 2.1.2 + mongo-object: 3.0.0 + dev: false + /simple-concat/1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} dev: false From 5c86ead53fa001cffe2481ce78fb2702c271e300 Mon Sep 17 00:00:00 2001 From: Sujith Date: Fri, 16 Dec 2022 12:39:33 +0530 Subject: [PATCH 11/11] fix: revert simpl-schema version to 1.12 Signed-off-by: Sujith --- packages/api-utils/package.json | 2 +- pnpm-lock.yaml | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/api-utils/package.json b/packages/api-utils/package.json index 775f6c9047f..8968d847782 100644 --- a/packages/api-utils/package.json +++ b/packages/api-utils/package.json @@ -52,7 +52,7 @@ "lodash": "^4.17.15", "ramda": "^0.28.0", "transliteration": "^2.1.9", - "simpl-schema": "^3.4.0" + "simpl-schema": "^1.12.0" }, "devDependencies": { "@babel/core": "^7.9.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d1cd0c444f..9c19e3ff727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1331,7 +1331,7 @@ importers: graphql-relay: ^0.9.0 lodash: ^4.17.15 ramda: ^0.28.0 - simpl-schema: ^3.4.0 + simpl-schema: ^1.12.0 transliteration: ^2.1.9 dependencies: '@jest/globals': 26.6.2 @@ -1345,7 +1345,7 @@ importers: graphql-relay: 0.9.0_graphql@14.7.0 lodash: 4.17.21 ramda: 0.28.0 - simpl-schema: 3.4.0 + simpl-schema: 1.12.3 transliteration: 2.3.5 devDependencies: '@babel/core': 7.19.0 @@ -11336,11 +11336,6 @@ packages: /mongo-object/0.1.4: resolution: {integrity: sha512-QtYk0gupWEn2+iB+DDRt1L+WbcNYvJRaHdih/dcqthOa1DbnREUGSs2WGcW478GNYpElflo/yybZXu0sTiRXHg==} - /mongo-object/3.0.0: - resolution: {integrity: sha512-dDF7V0k+55s6YOjrF294GrE5s81z6RHR/YNkXj9mKcAK9hlL0Os0FRHtpinDHCqhEqImdLJUosIJ5lbYsCwbfA==} - engines: {node: '>=14.16'} - dev: false - /mongodb-connection-string-url/2.5.3: resolution: {integrity: sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==} dependencies: @@ -13042,14 +13037,6 @@ packages: message-box: 0.2.7 mongo-object: 0.1.4 - /simpl-schema/3.4.0: - resolution: {integrity: sha512-TUwGiWaRR6SnUYM/ULmVmBIsLsdWMe8QzJqsvhu8ztr+HWZFTY+VVQy03BjRhJ8o4vL9CSazxWrMiARJ4vtYMQ==} - engines: {node: '>=14.16', npm: '>=8'} - dependencies: - clone: 2.1.2 - mongo-object: 3.0.0 - dev: false - /simple-concat/1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} dev: false