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 */