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.
@@ -172,4 +179,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
"direction": "asc" "direction": "asc"
} }
} }
``` ```

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)}/>
); );
})() })()
} }
@@ -370,4 +434,4 @@ function RenderFilters({templateId, form, bodyshop}) {
)} )}
</div> </div>
); );
} }

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':
@@ -118,4 +126,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
} }
}; };
export {generateInternalReflections,} export {generateInternalReflections,}

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 ( fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevelSub = true;
}
if (topLevel) { // Construct the filter for a top-level application
// Construct the filter for a top-level application const targetFieldName = fieldPath[fieldPath.length - 1];
const targetFieldName = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = { let filterValue = createFilterValue(filter);
kind: Kind.OBJECT_FIELD, let filterField = createFilterField(targetFieldName, filter, filterValue);
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 if (topLevel) {
let whereArg = node.selectionSet.selections applyTopLevelFilter(node, fieldPath, filterField);
.find(selection => selection.name.value === fieldPath[0]) } else if (topLevelSub) {
?.arguments.find(arg => arg.name.value === 'where'); applyTopLevelSub(node, fieldPath, filterField);
} else {
applyNestedFilter(node, fieldPath, filterField);
}
});
}
}
});
}
if (!whereArg) { /**
whereArg = { * Create a filter value based on the filter
kind: Kind.ARGUMENT, * @param filter
name: {kind: Kind.NAME, value: 'where'}, * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
value: {kind: Kind.OBJECT, fields: []}, */
}; function createFilterValue(filter) {
const topLevelSelection = node.selectionSet.selections.find(selection => if (Array.isArray(filter.value)) {
selection.name.value === fieldPath[0] // If it's an array, create a list value with the array items
); return {
if (topLevelSelection) { kind: Kind.LIST,
topLevelSelection.arguments = topLevelSelection.arguments || []; values: filter.value.map(item => ({
topLevelSelection.arguments.push(whereArg); kind: getGraphQLKind(item),
} value: item,
} })),
};
} else {
// If it's not an array, use the existing logic
return {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
}
}
// Correctly position the nested filter without an extra 'where' /**
if (fieldPath.length > 2) { // More than one level deep * Create a filter field based on the target field and filter
let currentField = whereArg.value; * @param targetFieldName
fieldPath.slice(1, -1).forEach((path, index) => { * @param filter
let existingField = currentField.fields.find(f => f.name.value === path); * @param filterValue
if (!existingField) { * @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}]}}}
existingField = { */
kind: Kind.OBJECT_FIELD, function createFilterField(targetFieldName, filter, filterValue) {
name: {kind: Kind.NAME, value: path}, return {
value: {kind: Kind.OBJECT, fields: []} kind: Kind.OBJECT_FIELD,
}; name: {kind: Kind.NAME, value: targetFieldName},
currentField.fields.push(existingField); value: {
} kind: Kind.OBJECT,
currentField = existingField.value; fields: [{
}); kind: Kind.OBJECT_FIELD,
currentField.fields.push(nestedFilter); name: {kind: Kind.NAME, value: filter.operator},
} else { // Directly under the top level value: filterValue,
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++) { * Apply a top-level filter to the AST
const fieldName = fieldPath[i]; * @param node
let fieldFound = false; * @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');
// Check if the current selection has a selectionSet and selections if (!whereArg) {
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { whereArg = {
// Look for the field in the current selection's selections kind: Kind.ARGUMENT,
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); name: {kind: Kind.NAME, value: 'where'},
if (selection) { value: {kind: Kind.OBJECT, fields: []},
// Move down the AST to the found selection };
currentSelection = selection; const topLevelSelection = node.selectionSet.selections.find(selection =>
fieldFound = true; selection.name.value === fieldPath[0]
} );
} if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// If the field was not found in the current path, it's an issue // Correctly position the nested filter without an extra 'where'
if (!fieldFound) { if (fieldPath.length > 2) { // More than one level deep
console.error(`Field ${fieldName} not found in the current selection.`); let currentField = whereArg.value;
return; // Exit the loop and function due to error fieldPath.slice(1, -1).forEach((path, index) => {
} let existingField = currentField.fields.find(f => f.name.value === path);
} if (!existingField) {
existingField = {
// At this point, currentSelection should be the parent field where the filter needs to be applied kind: Kind.OBJECT_FIELD,
// Check if the 'where' argument already exists in the current selection name: {kind: Kind.NAME, value: path},
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); value: {kind: Kind.OBJECT, fields: []}
if (whereArg) { };
whereArgFound = true; currentField.fields.push(existingField);
} else { }
// If not found, create a new 'where' argument for the current selection currentField = existingField.value;
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);
}
}
});
}
}
}); });
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
}
} }
/** /* eslint-enable no-loop-func */
* 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 */

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",
}, },