Compare commits

...

5 Commits

Author SHA1 Message Date
Dave Richer
a87473c392 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-29 21:31:10 -05:00
Dave Richer
989f020e30 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-29 21:22:51 -05:00
Dave Richer
42cf072f6b - Documentation
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-28 21:13:51 -05:00
Dave Richer
58b11fde76 - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-28 18:51:13 -05:00
Dave Richer
32bba8060a - Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-28 17:17:17 -05:00
8 changed files with 401 additions and 221 deletions

View File

@@ -3,6 +3,12 @@
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports. modify the graphQL query and provide the user more power over their reports.
For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
# Special Notes # Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected - When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
@@ -67,6 +73,8 @@ The following cases are available
- `special.employees` - This will reflect the employees `bodyshop.employees` - `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees`
- `special.last_names` - This will reflect the last names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees`
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources
- `special.class`- This will reflect the class `bodyshop.md_classes`
- -
### Path without brackets, multi level ### Path without brackets, multi level
@@ -142,8 +150,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release. - The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The type object must be 'string' or 'number' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used. - The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be - Do not add the ability to filter things that are already filtered as part of the original query, this would be
redundant and could cause issues. redundant and could cause issues.

View File

@@ -6,6 +6,7 @@ import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils"; import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
@@ -71,8 +72,9 @@ function FiltersSection({filters, form, bodyshop}) {
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item <Form.Item
dependencies={[['filters', field.name, "field"]]}> dependencies={[['filters', field.name, "field"]]}
>
{ {
() => { () => {
const name = form.getFieldValue(['filters', field.name, "field"]); const name = form.getFieldValue(['filters', field.name, "field"]);
@@ -82,7 +84,6 @@ function FiltersSection({filters, form, bodyshop}) {
key={`${index}operator`} key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')} label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]} name={[field.name, "operator"]}
dependencies={[]}
rules={[ rules={[
{ {
required: true, required: true,
@@ -92,7 +93,13 @@ function FiltersSection({filters, form, bodyshop}) {
> >
<Select <Select
getPopupContainer={trigger => trigger.parentNode} getPopupContainer={trigger => trigger.parentNode}
options={getWhereOperatorsByType(type)}/> options={getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], null);
}}
/>
</Form.Item> </Form.Item>
} }
} }
@@ -100,12 +107,19 @@ function FiltersSection({filters, form, bodyshop}) {
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item <Form.Item
dependencies={[['filters', field.name, "field"]]}> dependencies={[
['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{ {
() => { () => {
const name = form.getFieldValue(['filters', field.name, "field"]); // Because it looks cleaner than inlining.
const type = filters.find(f => f.name === name)?.type; const name = form.getFieldValue(['filters', field.name, "field"]);
const reflector = filters.find(f => f.name === name)?.reflector; const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector;
const operator = form.getFieldValue(['filters', field.name, "operator"]);
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
return <Form.Item return <Form.Item
key={`${index}value`} key={`${index}value`}
@@ -138,7 +152,24 @@ function FiltersSection({filters, form, bodyshop}) {
const reflections = reflector ? generateReflections(reflector) : []; const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]]; const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) { if (reflections.length > 0) {
// We have reflections and the operator type is array, so we will use a select box with multiple options
console.log(`operatorType: ${operatorType}`)
console.log(`operator: ${operator}`)
if (operatorType === "array") {
return (
<Select
disabled={!operator}
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return ( return (
<Select <Select
options={reflections} options={reflections}
@@ -150,16 +181,49 @@ function FiltersSection({filters, form, bodyshop}) {
); );
} }
// We have a type of number, so we will use a number input
if (type === "number") { if (type === "number") {
return ( return (
<InputNumber <InputNumber
onChange={(value) => form.setFieldValue(fieldPath, value)}/> disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
); );
} }
// We have a type of date, so we will use a date picker
if (type === "date") {
return ( return (
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>
);
}
// we have a type of boolean, so we will use a select box with a true or false option.
if (type === "boolean" || type === "bool") {
return (
<Select
disabled={!operator}
getPopupContainer={trigger => trigger.parentNode}
options={[
{
label: t('reportcenter.labels.advanced_filters_true'),
value: true
},
{
label: t('reportcenter.labels.advanced_filters_false'),
value: false
}
]}
onChange={(value) => form.setFieldValue(fieldPath, value)}
/>
);
}
return (
<Input <Input
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/> disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/>
); );
})() })()
} }

View File

@@ -38,6 +38,14 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
})), 'value'); })), 'value');
} }
const generateOptionsFromArray = (bodyshop, path) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(options.map((value) => ({
label: value,
value: value,
})), 'value');
}
/** /**
* Generate special reflections * Generate special reflections
* @param bodyshop * @param bodyshop
@@ -46,15 +54,15 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
*/ */
const generateSpecialReflections = (bodyshop, finalPath) => { const generateSpecialReflections = (bodyshop, finalPath) => {
switch (finalPath) { switch (finalPath) {
// Special case because Referral Sources is an Array, not an Object.
case 'referral_source':
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
case 'class':
return generateOptionsFromArray(bodyshop, 'md_classes');
case 'cost_centers': case 'cost_centers':
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name'); return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
// Special case because Categories is an Array, not an Object.
case 'categories': case 'categories':
const catOptions = getValueFromPath(bodyshop, 'md_categories'); return generateOptionsFromArray(bodyshop, 'md_categories');
return uniqBy(catOptions.map((value) => ({
label: value,
value: value,
})), 'value');
case 'insurance_companies': case 'insurance_companies':
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
case 'employee_teams': case 'employee_teams':

View File

@@ -2708,7 +2708,9 @@
"advanced_filters_filters": "Filters", "advanced_filters_filters": "Filters",
"advanced_filters_hide": "Hide", "advanced_filters_hide": "Hide",
"advanced_filters_show": "Show", "advanced_filters_show": "Show",
"advanced_filters_sorter_direction": "Direction", "advanced_filters_true": "True",
"advanced_filters_false": "False",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_sorter_field": "Field", "advanced_filters_sorter_field": "Field",
"advanced_filters_sorters": "Sorters", "advanced_filters_sorters": "Sorters",
"dates": "Dates", "dates": "Dates",

View File

@@ -2708,6 +2708,8 @@
"advanced_filters_filters": "", "advanced_filters_filters": "",
"advanced_filters_hide": "", "advanced_filters_hide": "",
"advanced_filters_show": "", "advanced_filters_show": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "", "advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "", "advanced_filters_sorter_field": "",
"advanced_filters_sorters": "", "advanced_filters_sorters": "",

View File

@@ -2708,6 +2708,8 @@
"advanced_filters_filters": "", "advanced_filters_filters": "",
"advanced_filters_hide": "", "advanced_filters_hide": "",
"advanced_filters_show": "", "advanced_filters_show": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "", "advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "", "advanced_filters_sorter_field": "",
"advanced_filters_sorters": "", "advanced_filters_sorters": "",

View File

@@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient"; import client from "./GraphQLClient";
import {gql} from "@apollo/client"; 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 = [ const STRING_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"}, {value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"}, {value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"}, {value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain 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 = [ const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"}, {value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"}, {value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"}, {value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less 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 = [ const ORDER_BY_OPERATORS = [
{value: "asc", label: "ascending"}, {value: "asc", label: "ascending"},
{value: "desc", label: "descending"} {value: "desc", label: "descending"}
@@ -31,7 +75,6 @@ export function getOrderOperatorsByType() {
return ORDER_BY_OPERATORS; return ORDER_BY_OPERATORS;
} }
/** /**
* Get the available operators for filtering * Get the available operators for filtering
* @param type * @param type
@@ -39,14 +82,15 @@ export function getOrderOperatorsByType() {
*/ */
export function getWhereOperatorsByType(type = 'string') { export function getWhereOperatorsByType(type = 'string') {
const operators = { const operators = {
string: STRING_OPERATORS, string: STRING_OPERATORS,
number: NUMBER_OPERATORS number: NUMBER_OPERATORS,
boolean: BOOLEAN_OPERATORS,
bool: BOOLEAN_OPERATORS,
date: DATE_OPERATORS
}; };
return operators[type]; return operators[type];
} }
/* eslint-disable no-loop-func */
/** /**
* Parse a GraphQL query into an AST * Parse a GraphQL query into an AST
* @param query * @param query
@@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
// Parse the query and apply the filters and sorters // Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute); const ast = parseQuery(templateQueryToExecute);
let filterFields = [];
if (templateObject?.filters && templateObject?.filters?.length) { if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields); applyFilters(ast, templateObject.filters);
wrapFiltersInAnd(ast, filterFields);
} }
if (templateObject?.sorters && templateObject?.sorters?.length) { if (templateObject?.sorters && templateObject?.sorters?.length) {
@@ -94,8 +136,8 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
const finalQuery = printQuery(ast); const finalQuery = printQuery(ast);
// commented out for future revision debugging // commented out for future revision debugging
// console.log('Modified Query'); console.log('Modified Query');
// console.log(finalQuery); console.log(finalQuery);
let contextData = {}; let contextData = {};
if (templateQueryToExecute) { if (templateQueryToExecute) {
@@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
return {contextData, useShopSpecificTemplate}; return {contextData, useShopSpecificTemplate};
} }
/** /**
* Apply sorters to the AST * Apply sorters to the AST
* @param ast * @param ast
@@ -170,161 +211,239 @@ export function applySorters(ast, sorters) {
}); });
} }
/**
* 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 * Apply filters to the AST
* @param ast * @param ast
* @param filters * @param filters
* @returns {ASTNode}
*/ */
export function applyFilters(ast, filters) { export function applyFilters(ast, filters) {
return visit(ast, { return visit(ast, {
OperationDefinition: { OperationDefinition: {
enter(node) { enter(node) {
filters.forEach(filter => { filters.forEach(filter => {
const fieldPath = filter.field.split('.'); const fieldPath = filter.field.split('.');
let topLevel = false; let topLevel = false;
let topLevelSub = false;
// Determine if the filter should be applied at the top level // Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { if (fieldPath.length === 2) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets topLevel = true;
topLevel = true; }
}
if (topLevel) { if ( fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
// Construct the filter for a top-level application fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
const targetFieldName = fieldPath[fieldPath.length - 1]; topLevelSub = true;
const filterValue = { }
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = { // Construct the filter for a top-level application
kind: Kind.OBJECT_FIELD, const targetFieldName = fieldPath[fieldPath.length - 1];
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 filterValue = createFilterValue(filter);
let whereArg = node.selectionSet.selections let filterField = createFilterField(targetFieldName, filter, filterValue);
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) { if (topLevel) {
whereArg = { applyTopLevelFilter(node, fieldPath, filterField);
kind: Kind.ARGUMENT, } else if (topLevelSub) {
name: {kind: Kind.NAME, value: 'where'}, applyTopLevelSub(node, fieldPath, filterField);
value: {kind: Kind.OBJECT, fields: []}, } else {
}; applyNestedFilter(node, fieldPath, filterField);
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 * Create a filter value based on the filter
let currentField = whereArg.value; * @param filter
fieldPath.slice(1, -1).forEach((path, index) => { * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
let existingField = currentField.fields.find(f => f.name.value === path); */
if (!existingField) { function createFilterValue(filter) {
existingField = { if (Array.isArray(filter.value)) {
kind: Kind.OBJECT_FIELD, // If it's an array, create a list value with the array items
name: {kind: Kind.NAME, value: path}, return {
value: {kind: Kind.OBJECT, fields: []} kind: Kind.LIST,
}; values: filter.value.map(item => ({
currentField.fields.push(existingField); kind: getGraphQLKind(item),
} value: item,
currentField = existingField.value; })),
}); };
currentField.fields.push(nestedFilter); } else {
} else { // Directly under the top level // If it's not an array, use the existing logic
whereArg.value.fields.push(nestedFilter); return {
} kind: getGraphQLKind(filter.value),
} else { value: filter.value,
// 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++) { * Create a filter field based on the target field and filter
const fieldName = fieldPath[i]; * @param targetFieldName
let fieldFound = false; * @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,
}],
},
};
}
// Check if the current selection has a selectionSet and selections /**
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { * Apply a top-level filter to the AST
// Look for the field in the current selection's selections * @param node
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); * @param fieldPath
if (selection) { * @param filterField
// Move down the AST to the found selection */
currentSelection = selection; function applyTopLevelFilter(node, fieldPath, filterField) {
fieldFound = true; // 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 the field was not found in the current path, it's an issue if (!whereArg) {
if (!fieldFound) { whereArg = {
console.error(`Field ${fieldName} not found in the current selection.`); kind: Kind.ARGUMENT,
return; // Exit the loop and function due to error 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);
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied // Correctly position the nested filter without an extra 'where'
// Check if the 'where' argument already exists in the current selection if (fieldPath.length > 2) { // More than one level deep
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); let currentField = whereArg.value;
if (whereArg) { fieldPath.slice(1, -1).forEach((path, index) => {
whereArgFound = true; let existingField = currentField.fields.find(f => f.name.value === path);
} else { if (!existingField) {
// If not found, create a new 'where' argument for the current selection existingField = {
currentSelection.arguments.push({ kind: Kind.OBJECT_FIELD,
kind: Kind.ARGUMENT, name: {kind: Kind.NAME, value: path},
name: {kind: Kind.NAME, value: 'where'}, value: {kind: Kind.OBJECT, fields: []}
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter };
}); currentField.fields.push(existingField);
} }
currentField = existingField.value;
// 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);
}
}
});
}
}
}); });
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);
} }
/** /**
@@ -333,41 +452,17 @@ export function applyFilters(ast, filters) {
* @returns {Kind|Kind.INT} * @returns {Kind|Kind.INT}
*/ */
function getGraphQLKind(value) { function getGraphQLKind(value) {
if (typeof value === 'number') { if (Array.isArray(value)) {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT; return Kind.LIST;
} else if (typeof value === 'boolean') { } else if (typeof value === 'number') {
return Kind.BOOLEAN; return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'string') { } else if (typeof value === 'boolean') {
return Kind.STRING; return Kind.BOOLEAN;
} } else if (typeof value === 'string') {
// Extend with more types as needed 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
/** }
* 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 */ /* eslint-enable no-loop-func */

View File

@@ -160,7 +160,7 @@ exports.sendNotification = async (req, res) => {
.send({ .send({
topic: "PRD_PATRICK-messaging", topic: "PRD_PATRICK-messaging",
notification: { notification: {
title: `ImEX Online Message - , title: `ImEX Online Message -` ,
body: "Test Noti.", body: "Test Noti.",
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //imageUrl: "https://thinkimex.com/img/io-fcm.png",
}, },