Merge branch 'feautre/IO-2647-Reporting-V3-From-Master' into release/2024-03-01
# Conflicts: # _reference/reportFiltersAndSorters.md
This commit is contained in:
@@ -3,8 +3,13 @@
|
|||||||
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.
|
||||||
|
|
||||||
## Special Notes
|
For filters and sorters, valid types include (`type` key in the schema):
|
||||||
|
- string (default)
|
||||||
|
- number
|
||||||
|
- bool or boolean
|
||||||
|
- date
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
## High level Schema Overview
|
## High level Schema Overview
|
||||||
@@ -69,7 +74,9 @@ 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
|
||||||
|
|
||||||
`"name": "jobs.joblines.mod_lb_hrs",`
|
`"name": "jobs.joblines.mod_lb_hrs",`
|
||||||
@@ -142,17 +149,21 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
|||||||
|
|
||||||
## Known Caveats
|
## Known Caveats
|
||||||
|
|
||||||
- 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.
|
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
|
||||||
- The `dates` object is not yet implemented and will be added in a future release.
|
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
|
||||||
- The type object must be 'string' or 'number' and is case-sensitive.
|
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' 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 redundant and could cause issues.
|
- 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.
|
||||||
- Do not add the ability to filter on things like FK constraints, must like the above example.
|
- Do not add the ability to filter on things like FK constraints, must like the above example.
|
||||||
|
|
||||||
## Sorters
|
## Sorters
|
||||||
|
|
||||||
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
|
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
|
||||||
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters.
|
a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
|
||||||
|
would be added at the `joblines` level.
|
||||||
|
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
|
||||||
|
using the sorters.
|
||||||
|
|
||||||
### Default Sorters
|
### Default Sorters
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +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}) {
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +33,7 @@ function FiltersSection({filters, form, bodyshop}) {
|
|||||||
return (
|
return (
|
||||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
|
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
|
||||||
<Form.List name={["filters"]}>
|
<Form.List name={["filters"]}>
|
||||||
{(fields, {add, remove, move}) => {
|
{(fields, {add, remove}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
@@ -70,7 +70,9 @@ function FiltersSection({filters, form, bodyshop}) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<Form.Item dependencies={[['filters', field.name, "field"]]}>
|
<Form.Item
|
||||||
|
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
() => {
|
() => {
|
||||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||||
@@ -80,7 +82,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,
|
||||||
@@ -90,19 +91,32 @@ 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'], undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<Form.Item dependencies={[['filters', field.name, "field"]]}>
|
<Form.Item dependencies={[
|
||||||
|
['filters', field.name, "field"],
|
||||||
|
['filters', field.name, "operator"]
|
||||||
|
]}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
() => {
|
() => {
|
||||||
|
// Because it looks cleaner than inlining.
|
||||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||||
const type = filters.find(f => f.name === name)?.type;
|
const type = filters.find(f => f.name === name)?.type;
|
||||||
const reflector = filters.find(f => f.name === name)?.reflector;
|
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`}
|
||||||
@@ -134,8 +148,22 @@ 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
|
||||||
|
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}
|
||||||
@@ -147,16 +175,50 @@ 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
|
||||||
|
disabled={!operator}
|
||||||
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
|
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We have a type of date, so we will use a date picker
|
||||||
|
if (type === "date") {
|
||||||
|
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 (
|
return (
|
||||||
<Input
|
<Input
|
||||||
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/>
|
disabled={!operator}
|
||||||
|
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -203,12 +265,12 @@ function FiltersSection({filters, form, bodyshop}) {
|
|||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function SortersSection({sorters, form}) {
|
function SortersSection({sorters}) {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
|
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
|
||||||
<Form.List name={["sorters"]}>
|
<Form.List name={["sorters"]}>
|
||||||
{(fields, {add, remove, move}) => {
|
{(fields, {add, remove}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Sorters
|
Sorters
|
||||||
|
|||||||
@@ -8,6 +8,21 @@ import {uniqBy} from "lodash";
|
|||||||
*/
|
*/
|
||||||
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
|
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate options from array
|
||||||
|
* @param bodyshop
|
||||||
|
* @param path
|
||||||
|
* @returns {unknown[]}
|
||||||
|
*/
|
||||||
|
const generateOptionsFromArray = (bodyshop, path) => {
|
||||||
|
const options = getValueFromPath(bodyshop, path);
|
||||||
|
return uniqBy(options.map((value) => ({
|
||||||
|
label: value,
|
||||||
|
value: value,
|
||||||
|
})), 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid internal reflections
|
* Valid internal reflections
|
||||||
* Note: This is intended for future functionality
|
* Note: This is intended for future functionality
|
||||||
@@ -46,15 +61,16 @@ 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.
|
// 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 +134,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export {generateInternalReflections,}
|
export {generateInternalReflections}
|
||||||
@@ -2589,6 +2589,8 @@
|
|||||||
"advanced_filters_sorters": "Sorters",
|
"advanced_filters_sorters": "Sorters",
|
||||||
"advanced_filters_filter_field": "Field",
|
"advanced_filters_filter_field": "Field",
|
||||||
"advanced_filters_sorter_field": "Field",
|
"advanced_filters_sorter_field": "Field",
|
||||||
|
"advanced_filters_true": "True",
|
||||||
|
"advanced_filters_false": "False",
|
||||||
"advanced_filters_sorter_direction": "Direction",
|
"advanced_filters_sorter_direction": "Direction",
|
||||||
"advanced_filters_filter_operator": "Operator",
|
"advanced_filters_filter_operator": "Operator",
|
||||||
"advanced_filters_filter_value": "Value",
|
"advanced_filters_filter_value": "Value",
|
||||||
|
|||||||
@@ -2589,6 +2589,8 @@
|
|||||||
"advanced_filters_sorters": "",
|
"advanced_filters_sorters": "",
|
||||||
"advanced_filters_filter_field": "",
|
"advanced_filters_filter_field": "",
|
||||||
"advanced_filters_sorter_field": "",
|
"advanced_filters_sorter_field": "",
|
||||||
|
"advanced_filters_true": "",
|
||||||
|
"advanced_filters_false": "",
|
||||||
"advanced_filters_sorter_direction": "",
|
"advanced_filters_sorter_direction": "",
|
||||||
"advanced_filters_filter_operator": "",
|
"advanced_filters_filter_operator": "",
|
||||||
"advanced_filters_filter_value": "",
|
"advanced_filters_filter_value": "",
|
||||||
|
|||||||
@@ -2589,6 +2589,8 @@
|
|||||||
"advanced_filters_sorters": "",
|
"advanced_filters_sorters": "",
|
||||||
"advanced_filters_filter_field": "",
|
"advanced_filters_filter_field": "",
|
||||||
"advanced_filters_sorter_field": "",
|
"advanced_filters_sorter_field": "",
|
||||||
|
"advanced_filters_true": "",
|
||||||
|
"advanced_filters_false": "",
|
||||||
"advanced_filters_sorter_direction": "",
|
"advanced_filters_sorter_direction": "",
|
||||||
"advanced_filters_filter_operator": "",
|
"advanced_filters_filter_operator": "",
|
||||||
"advanced_filters_filter_value": "",
|
"advanced_filters_filter_value": "",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -40,13 +83,14 @@ 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) {
|
||||||
@@ -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
|
||||||
@@ -149,16 +190,16 @@ export function applySorters(ast, sorters) {
|
|||||||
if (!orderByArg) {
|
if (!orderByArg) {
|
||||||
orderByArg = {
|
orderByArg = {
|
||||||
kind: Kind.ARGUMENT,
|
kind: Kind.ARGUMENT,
|
||||||
name: { kind: Kind.NAME, value: 'order_by' },
|
name: {kind: Kind.NAME, value: 'order_by'},
|
||||||
value: { kind: Kind.OBJECT, fields: [] },
|
value: {kind: Kind.OBJECT, fields: []},
|
||||||
};
|
};
|
||||||
currentSelection.arguments.push(orderByArg);
|
currentSelection.arguments.push(orderByArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorterField = {
|
const sorterField = {
|
||||||
kind: Kind.OBJECT_FIELD,
|
kind: Kind.OBJECT_FIELD,
|
||||||
name: { kind: Kind.NAME, value: targetFieldName },
|
name: {kind: Kind.NAME, value: targetFieldName},
|
||||||
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions
|
value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new sorter condition
|
// Add the new sorter condition
|
||||||
@@ -170,10 +211,59 @@ 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, {
|
||||||
@@ -182,192 +272,197 @@ export function applyFilters(ast, filters) {
|
|||||||
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 = {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
* Get the GraphQL kind for a value
|
||||||
* @param value
|
* @param value
|
||||||
* @returns {Kind|Kind.INT}
|
* @returns {Kind|Kind.INT}
|
||||||
*/
|
*/
|
||||||
function getGraphQLKind(value) {
|
function getGraphQLKind(value) {
|
||||||
if (typeof value === 'number') {
|
if (Array.isArray(value)) {
|
||||||
|
return Kind.LIST;
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
|
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
|
||||||
} else if (typeof value === 'boolean') {
|
} else if (typeof value === 'boolean') {
|
||||||
return Kind.BOOLEAN;
|
return Kind.BOOLEAN;
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
return Kind.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
|
||||||
}
|
}
|
||||||
// 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 */
|
/* eslint-enable no-loop-func */
|
||||||
Reference in New Issue
Block a user