import { Kind, parse, print, visit } from "graphql"; import client from "./GraphQLClient"; import { gql } from "@apollo/client"; /* eslint-disable no-loop-func */ /** * The available operators for filtering (string) * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} */ const STRING_OPERATORS = [ { value: "_eq", label: "equals" }, { value: "_neq", label: "does not equal" }, { value: "_like", label: "contains" }, { value: "_nlike", label: "does not contain" }, { value: "_ilike", label: "contains case-insensitive" }, { value: "_nilike", label: "does not contain case-insensitive" }, { value: "_in", label: "in", type: "array" }, { value: "_nin", label: "not in", type: "array" } ]; /** * The available operators for filtering (dates) * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} */ const DATE_OPERATORS = [ { value: "_eq", label: "equals" }, { value: "_neq", label: "does not equal" }, { value: "_gt", label: "greater than" }, { value: "_lt", label: "less than" }, { value: "_gte", label: "greater than or equal" }, { value: "_lte", label: "less than or equal" }, { value: "_in", label: "in", type: "array" }, { value: "_nin", label: "not in", type: "array" } ]; /** * The available operators for filtering (booleans) * @type {[{label: string, value: string},{label: string, value: string}]} */ const BOOLEAN_OPERATORS = [ { value: "_eq", label: "equals" }, { value: "_neq", label: "does not equal" } ]; /** * The available operators for filtering (numbers) * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]} */ const NUMBER_OPERATORS = [ { value: "_eq", label: "equals" }, { value: "_neq", label: "does not equal" }, { value: "_gt", label: "greater than" }, { value: "_lt", label: "less than" }, { value: "_gte", label: "greater than or equal" }, { value: "_lte", label: "less than or equal" }, { value: "_in", label: "in", type: "array" }, { value: "_nin", label: "not in", type: "array" } ]; /** * The available operators for sorting * @type {[{label: string, value: string},{label: string, value: string}]} */ const ORDER_BY_OPERATORS = [ { value: "asc", label: "ascending" }, { value: "desc", label: "descending" } ]; /** * Get the available operators for filtering * @returns {[{label: string, value: string},{label: string, value: string}]} */ export function getOrderOperatorsByType() { return ORDER_BY_OPERATORS; } /** * Get the available operators for filtering * @param type * @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]} */ export function getWhereOperatorsByType(type = "string") { const operators = { string: STRING_OPERATORS, number: NUMBER_OPERATORS, boolean: BOOLEAN_OPERATORS, bool: BOOLEAN_OPERATORS, date: DATE_OPERATORS }; return operators[type]; } /** * Parse a GraphQL query into an AST * @param query * @returns {DocumentNode} */ export function parseQuery(query) { return parse(query); } /** * Print an AST back into a GraphQL query * @param query * @returns {string} */ export function printQuery(query) { return print(query); } /** * Generate a template based on the query and object * @param templateQueryToExecute * @param templateObject * @param useShopSpecificTemplate * @returns {Promise<{contextData: {}, useShopSpecificTemplate}>} */ export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) { // Advanced Filtering and Sorting modifications start here // Parse the query and apply the filters and sorters const ast = parseQuery(templateQueryToExecute); if (templateObject?.filters && templateObject?.filters?.length) { applyFilters(ast, templateObject.filters); } if (templateObject?.sorters && templateObject?.sorters?.length) { applySorters(ast, templateObject.sorters); } else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) { applySorters(ast, templateObject.defaultSorters); } const finalQuery = printQuery(ast); // commented out for future revision debugging // console.log('Modified Query'); // console.log(finalQuery); let contextData = {}; if (templateQueryToExecute) { const { data } = await client.query({ query: gql(finalQuery), variables: { ...templateObject.variables } }); contextData = data; } return { contextData, useShopSpecificTemplate }; } /** * Apply sorters to the AST * @param ast * @param sorters */ export function applySorters(ast, sorters) { sorters.forEach((sorter) => { const fieldPath = sorter.field.split("."); visit(ast, { OperationDefinition: { enter(node) { // Loop through each sorter to apply it // noinspection DuplicatedCode let currentSelection = node; // Start with the root operation // Navigate down the field path to the correct location for (let i = 0; i < fieldPath.length - 1; i++) { let found = false; visit(currentSelection, { Field: { enter(node) { if (node.name.value === fieldPath[i]) { currentSelection = node; // Move down to the next level found = true; } } } }); if (!found) break; // Stop if we can't find the next field in the path } // Apply the sorter at the correct level if (currentSelection) { const targetFieldName = fieldPath[fieldPath.length - 1]; let orderByArg = currentSelection.arguments.find((arg) => arg.name.value === "order_by"); if (!orderByArg) { orderByArg = { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: "order_by" }, value: { kind: Kind.OBJECT, fields: [] } }; currentSelection.arguments.push(orderByArg); } const sorterField = { kind: Kind.OBJECT_FIELD, name: { kind: Kind.NAME, value: targetFieldName }, value: { kind: Kind.ENUM, value: sorter.direction } // Adjust if your schema uses a different type for sorting directions }; // Add the new sorter condition orderByArg.value.fields.push(sorterField); } } } }); }); } /** * Apply Top Level Sub to the AST * @param node * @param fieldPath * @param filterField */ function applyTopLevelSub(node, fieldPath, filterField) { // Find or create the where argument for the top-level subfield let whereArg = node.selectionSet.selections .find((selection) => selection.name.value === fieldPath[0]) ?.arguments.find((arg) => arg.name.value === "where"); if (!whereArg) { whereArg = { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: "where" }, value: { kind: Kind.OBJECT, fields: [] } }; const topLevelSubSelection = node.selectionSet.selections.find( (selection) => selection.name.value === fieldPath[0] ); if (topLevelSubSelection) { topLevelSubSelection.arguments = topLevelSubSelection.arguments || []; topLevelSubSelection.arguments.push(whereArg); } } // Correctly position the nested filter without an extra 'where' if (fieldPath.length > 2) { // More than one level deep let currentField = whereArg.value; fieldPath.slice(1, -1).forEach((path, index) => { let existingField = currentField.fields.find((f) => f.name.value === path); if (!existingField) { existingField = { kind: Kind.OBJECT_FIELD, name: { kind: Kind.NAME, value: path }, value: { kind: Kind.OBJECT, fields: [] } }; currentField.fields.push(existingField); } currentField = existingField.value; }); currentField.fields.push(filterField); } else { // Directly under the top level whereArg.value.fields.push(filterField); } } /** * Apply filters to the AST * @param ast * @param filters * @returns {ASTNode} */ export function applyFilters(ast, filters) { return visit(ast, { OperationDefinition: { enter(node) { filters.forEach((filter) => { const fieldPath = filter.field.split("."); let topLevel = false; let topLevelSub = false; // Determine if the filter should be applied at the top level if (fieldPath.length === 2) { topLevel = true; } if (fieldPath.length > 2 && fieldPath[0].startsWith("[") && fieldPath[0].endsWith("]")) { fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets topLevelSub = true; } // Construct the filter for a top-level application const targetFieldName = fieldPath[fieldPath.length - 1]; let filterValue = createFilterValue(filter); let filterField = createFilterField(targetFieldName, filter, filterValue); if (topLevel) { applyTopLevelFilter(node, fieldPath, filterField); } else if (topLevelSub) { applyTopLevelSub(node, fieldPath, filterField); } else { applyNestedFilter(node, fieldPath, filterField); } }); } } }); } /** * Create a filter value based on the filter * @param filter * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}} */ function createFilterValue(filter) { if (Array.isArray(filter.value)) { // If it's an array, create a list value with the array items return { kind: Kind.LIST, values: filter.value.map((item) => ({ kind: getGraphQLKind(item), value: item })) }; } else { // If it's not an array, use the existing logic return { kind: getGraphQLKind(filter.value), value: filter.value }; } } /** * Create a filter field based on the target field and filter * @param targetFieldName * @param filter * @param filterValue * @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}} */ function createFilterField(targetFieldName, filter, filterValue) { return { kind: Kind.OBJECT_FIELD, name: { kind: Kind.NAME, value: targetFieldName }, value: { kind: Kind.OBJECT, fields: [ { kind: Kind.OBJECT_FIELD, name: { kind: Kind.NAME, value: filter.operator }, value: filterValue } ] } }; } /** * Apply a top-level filter to the AST * @param node * @param fieldPath * @param filterField */ function applyTopLevelFilter(node, fieldPath, filterField) { // Find or create the where argument for the top-level field let whereArg = node.selectionSet.selections .find((selection) => selection.name.value === fieldPath[0]) ?.arguments.find((arg) => arg.name.value === "where"); if (!whereArg) { whereArg = { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: "where" }, value: { kind: Kind.OBJECT, fields: [] } }; const topLevelSelection = node.selectionSet.selections.find((selection) => selection.name.value === fieldPath[0]); if (topLevelSelection) { topLevelSelection.arguments = topLevelSelection.arguments || []; topLevelSelection.arguments.push(whereArg); } } // Correctly position the nested filter without an extra 'where' if (fieldPath.length > 2) { // More than one level deep let currentField = whereArg.value; fieldPath.slice(1, -1).forEach((path, index) => { let existingField = currentField.fields.find((f) => f.name.value === path); if (!existingField) { existingField = { kind: Kind.OBJECT_FIELD, name: { kind: Kind.NAME, value: path }, value: { kind: Kind.OBJECT, fields: [] } }; currentField.fields.push(existingField); } currentField = existingField.value; }); currentField.fields.push(filterField); } else { // Directly under the top level whereArg.value.fields.push(filterField); } } /** * Apply a nested filter to the AST * @param node * @param fieldPath * @param filterField */ function applyNestedFilter(node, fieldPath, filterField) { // Initialize a reference to the current selection to traverse down the AST let currentSelection = node; // Iterate over the fieldPath, except for the last entry, to navigate the structure for (let i = 0; i < fieldPath.length - 1; i++) { const fieldName = fieldPath[i]; let fieldFound = false; // Check if the current selection has a selectionSet and selections if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { // Look for the field in the current selection's selections const selection = currentSelection.selectionSet.selections.find((sel) => sel.name.value === fieldName); if (selection) { // Move down the AST to the found selection currentSelection = selection; fieldFound = true; } } // If the field was not found in the current path, it's an issue if (!fieldFound) { console.error(`Field ${fieldName} not found in the current selection.`); return; // Exit the loop and function due to error } } // At this point, currentSelection should be the parent field where the filter needs to be applied // Check if the 'where' argument already exists in the current selection const whereArg = currentSelection.arguments.find((arg) => arg.name.value === "where"); if (!whereArg) { // If not found, create a new 'where' argument for the current selection currentSelection.arguments.push({ kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: "where" }, value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter }); } // Add the filter field to the 'where' clause of the current selection currentSelection.arguments.find((arg) => arg.name.value === "where").value.fields.push(filterField); } /** * Get the GraphQL kind for a value * @param value * @returns {Kind|Kind.INT} */ function getGraphQLKind(value) { if (Array.isArray(value)) { return Kind.LIST; } else if (typeof value === "number") { return value % 1 === 0 ? Kind.INT : Kind.FLOAT; } else if (typeof value === "boolean") { return Kind.BOOLEAN; } else if (typeof value === "string") { return Kind.STRING; } else if (value instanceof Date) { return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string } } /* eslint-enable no-loop-func */