- clear stage

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-02-16 12:50:51 -05:00
13 changed files with 977 additions and 22 deletions

View File

@@ -9,7 +9,7 @@ import {store} from "../redux/store";
import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
import {TemplateList} from "./TemplateConstants";
import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server;
@@ -292,7 +292,7 @@ export const GenerateDocument = async (
})
);
} else if (sendType === "x") {
console.log("excel");
await RenderTemplate(template, bodyshop, false, true);
} else if (sendType === "text") {
await RenderTemplate(template, bodyshop, false, false, true);
@@ -306,6 +306,58 @@ export const GenerateDocuments = async (templates) => {
await RenderTemplates(templates, bodyshop);
};
export const fetchFilterData = async ({name}) => {
try {
const bodyshop = store.getState().user.bodyshop;
const jsrAuth = (await axios.post("/utils/jsr")).data;
jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: {Authorization: jsrAuth},
});
const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid
);
const jsReportFilters = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${name}.filters'`,
{headers: {Authorization: jsrAuth}}
);
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
let parsedFilterData;
let useShopSpecificTemplate = false;
// let shopSpecificTemplate;
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportFilters.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
parsedFilterData = atob(shopSpecificTemplate.content);
}
}
if (!parsedFilterData) {
const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
}
const data = JSON.parse(parsedFilterData);
return {
data,
useShopSpecificTemplate,
success: true,
}
} catch {
return {
success: false,
}
}
};
const fetchContextData = async (templateObject, jsrAuth) => {
const bodyshop = store.getState().user.bodyshop;
@@ -344,7 +396,16 @@ const fetchContextData = async (templateObject, jsrAuth) => {
templateQueryToExecute = atob(generalTemplate.content);
}
let contextData = {};
// Commented out for future revision debugging
// console.log('Template Object');
// console.dir(templateObject);
// console.log('Unmodified Query');
// console.dir(templateQueryToExecute);
// We have no template filters or sorters, so we can just execute the query and return the data
if ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) {
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
query: gql(templateQueryToExecute),
@@ -352,6 +413,37 @@ const fetchContextData = async (templateObject, jsrAuth) => {
});
contextData = data;
}
return {contextData, useShopSpecificTemplate};
}
// Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute);
let filterFields = [];
if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields);
wrapFiltersInAnd(ast, filterFields);
}
if (templateObject?.sorters && templateObject?.sorters?.length) {
applySorters(ast, templateObject.sorters);
}
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};
};
@@ -406,4 +498,4 @@ function extend(o1, o2, o3) {
}
}
return result;
}
}

View File

@@ -0,0 +1,309 @@
import {Kind, parse, print, visit} from "graphql";
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"}
];
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"}
];
export function getOperatorsByType(type = 'string') {
const operators = {
string: STRING_OPERATORS,
number: NUMBER_OPERATORS
};
return operators[type];
}
/* eslint-disable no-loop-func */
/**
* 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);
}
/**
* 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 filters to the AST
* @param ast
* @param filters
*/
export function applyFilters(ast, filters) {
return visit(ast, {
OperationDefinition: {
enter(node) {
filters.forEach(filter => {
const fieldPath = filter.field.split('.');
let topLevel = false;
// Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true;
}
if (topLevel) {
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = {
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,
}],
},
};
// 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(nestedFilter);
} else { // Directly under the top level
whereArg.value.fields.push(nestedFilter);
}
} else {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
let whereArgFound = false;
// 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) {
whereArgFound = true;
} else {
// 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
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetField},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
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 (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;
}
// Extend with more types as needed
}
/**
* Wrap filters in an 'and' object
* @param ast
* @param filterFields
*/
export function wrapFiltersInAnd(ast, filterFields) {
visit(ast, {
OperationDefinition: {
enter(node) {
node.selectionSet.selections.forEach((selection) => {
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
if (filterFields.length > 1) {
const andFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: '_and'},
value: {kind: Kind.LIST, values: filterFields}
};
whereArg.value.fields.push(andFilter);
} else if (filterFields.length === 1) {
whereArg.value.fields.push(filterFields[0].fields[0]);
}
});
}
}
});
}
/* eslint-enable no-loop-func */