Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: filterSearch initial version
Signed-off-by: Sujith <[email protected]>
  • Loading branch information
sujithvn committed Dec 2, 2022
commit e5a287a321c9cb790d854fc2855a62a71de546a3
3 changes: 2 additions & 1 deletion apps/reaction/plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
115 changes: 115 additions & 0 deletions packages/api-core/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions packages/api-plugin-accounts/src/queries/filterSearchAccounts.js
Original file line number Diff line number Diff line change
@@ -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<Object>} 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);
}
28 changes: 28 additions & 0 deletions packages/api-plugin-accounts/src/queries/filterSearchCustomers.js
Original file line number Diff line number Diff line change
@@ -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<Object>} 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that this is a completely correct way to retrieve only customers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing query endpoint uses the same approach. Hence I re-used it
packages/api-plugin-accounts/src/queries/customers.js

return Accounts.find(filterQuery);
}
4 changes: 4 additions & 0 deletions packages/api-plugin-accounts/src/queries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this has to be a separate endpoint rather than functionality added on to the existing queries?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing query endpoint is using a fixed input structure like so
const { groupIds, notInAnyGroups } = input;

The new filter has an entirely different input format to handle multiple conditions. So I assume it will break any existing implementation of the query if we change it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not extend it while not breaking the existing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that we keep the implementation of the new 'filter' feature as a standard/similar implementation across all the selected collections. Something like a common format including the name of the query endpoint.

All the existing end-points (for accounts/customers/orders/products) accept input in different formats and we would have to make different changes in each of these to accept both the old & new format of input.

Note: I have not checked, but we may again hit the earlier problem of graphql not flexible in allowing different input formats.

Please let me know if we want to make these changes for all the collections or selected ones.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see the logic of having consistent naming of the endpoint across collections but I also think it's confusing to have two endpoints that do basically the same thing but differently. What do you suggest to resolve this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be okay to use 'filterXYZ' as the go-forward endpoint across all collections and mark the old ones as 'deprecated'? If so, we can add the new filter to all collections where there is something similar currently in place.

Else, as you mentioned initially, we have to update the existing query end-points. We can start & test with a simple one like accounts (to confirm graphql does not create issues) and then implement that for the remaining 3 (customers/orders/products). In this case also, we may have to implement this update to other collections where there is something similar currently in place.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm fine with the "create new and deprecate" old approach

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I shall find other collections that are using 'filter' like query and add new filter along with the promotions-filter ticket

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, let's just stick with the three for now

* @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<Object>} 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)
});
}
Original file line number Diff line number Diff line change
@@ -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<Object>} 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)
});
}
4 changes: 4 additions & 0 deletions packages/api-plugin-accounts/src/resolvers/Query/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
78 changes: 78 additions & 0 deletions packages/api-plugin-accounts/src/schemas/account.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading