Построитель динамических запросов Typeorm из структурированного объекта

Для использования на сервере graphql я определил тип структурированного ввода, в котором вы можете указать ряд условий фильтрации, очень похожих на то, как работает prisma:

Построитель динамических запросов Typeorm из структурированного объекта

Это позволяет мне отправлять структурированные фильтры в таких запросах, как:

{
  users(
    where: {
      OR: [{ email: { starts_with: "ja" } }, { email: { ends_with: ".com" } }],
      AND: [{ email: { starts_with: "ja" } }, { email: { ends_with: ".com" } }],
      email: {contains: "lowe"}
    }
  ) {
    id
    email
  }
}

Внутри моего преобразователя я загружаю args.where через функцию для анализа структуры и использую построитель запросов TypeOrm для преобразования ее в правильный sql. Полная функция:

import { Brackets } from "typeorm";

export const filterQuery = (query: any, where: any) => {
  if (!where) {
    return query;
  }

  Object.keys(where).forEach(key => {
    if (key === "OR") {
      where[key].map((queryArray: any) => {
        query.orWhere(new Brackets(qb => filterQuery(qb, queryArray)));
      });
    } else if (key === "AND") {
      where[key].map((queryArray: any) => {
        query.andWhere(new Brackets(qb => filterQuery(qb, queryArray)));
      });
    } else {
      const whereArgs = Object.entries(where);

      whereArgs.map(whereArg => {
        const [fieldName, filters] = whereArg;
        const ops = Object.entries(filters);

        ops.map(parameters => {
          const [operation, value] = parameters;

          switch (operation) {
            case "is": {
              query.andWhere(`${fieldName} = :isvalue`, { isvalue: value });
              break;
            }
            case "not": {
              query.andWhere(`${fieldName} != :notvalue`, { notvalue: value });
              break;
            }
            case "in": {
              query.andWhere(`${fieldName} IN :invalue`, { invalue: value });
              break;
            }
            case "not_in": {
              query.andWhere(`${fieldName} NOT IN :notinvalue`, {
                notinvalue: value
              });
              break;
            }
            case "lt": {
              query.andWhere(`${fieldName} < :ltvalue`, { ltvalue: value });
              break;
            }
            case "lte": {
              query.andWhere(`${fieldName} <= :ltevalue`, { ltevalue: value });
              break;
            }
            case "gt": {
              query.andWhere(`${fieldName} > :gtvalue`, { gtvalue: value });
              break;
            }
            case "gte": {
              query.andWhere(`${fieldName} >= :gtevalue`, { gtevalue: value });
              break;
            }
            case "contains": {
              query.andWhere(`${fieldName} ILIKE :convalue`, {
                convalue: `%${value}%`
              });
              break;
            }
            case "not_contains": {
              query.andWhere(`${fieldName} NOT ILIKE :notconvalue`, {
                notconvalue: `%${value}%`
              });
              break;
            }
            case "starts_with": {
              query
                .andWhere(`${fieldName} ILIKE :swvalue`)
                .setParameter("swvalue", `${value}%`);
              break;
            }
            case "not_starts_with": {
              query
                .andWhere(`${fieldName} NOT ILIKE :nswvalue`)
                .setParameter("nswvalue", `${value}%`);
              break;
            }
            case "ends_with": {
              query.andWhere(`${fieldName} ILIKE :ewvalue`, {
                ewvalue: `%${value}`
              });
              break;
            }
            case "not_ends_with": {
              query.andWhere(`${fieldName} ILIKE :newvalue`, {
                newvalue: `%${value}`
              });
              break;
            }
            default: {
              break;
            }
          }
        });
      });
    }
  });

  return query;
};

Что работает (вроде), но не вкладывает запросы AND / OR, как я ожидал (и ранее работал в KNEX). Вышеупомянутая функция генерирует SQL:

SELECT
  "user"."id" AS "user_id",
  "user"."name" AS "user_name",
  "user"."email" AS "user_email",
  "user"."loginToken" AS "user_loginToken",
  "user"."loginTokenExpiry" AS "user_loginTokenExpiry",
  "user"."active" AS "user_active",
  "user"."visible" AS "user_visible",
  "user"."isStaff" AS "user_isStaff",
  "user"."isBilling" AS "user_isBilling",
  "user"."createdAt" AS "user_createdAt",
  "user"."updatedAt" AS "user_updatedAt",
  "user"."version" AS "user_version"
FROM "user" "user"
WHERE (email ILIKE $1)
  AND (email ILIKE $2)
  OR (email ILIKE $3)
  OR (email ILIKE $4)
  AND email ILIKE $5
-- PARAMETERS: ["ja%","%.com","ja%","%.com","%lowe%"]

Но я ожидал увидеть нечто большее:

..... 
WHERE email ILIKE '%low%' 
AND (
    email ILIKE 'ja%' AND email ILIKE '%.com'
) AND (
    email ILIKE 'ja%' OR email ILIKE '%.com'
)

Простите чепуху, повторяющийся вопрос. Я просто пытаюсь проиллюстрировать ожидаемые утверждения NESTED.

Как я могу заставить ветви И / ИЛИ моей функции построителя запросов правильно размещаться, как ожидалось?

** Бонусные баллы, если кто-то может помочь мне разобраться, как именно здесь набирается машинописный текст **

в вашем примере запроса есть {id email}, вы уверены, что получаете его, потому что это похоже на json, json должен иметь пару <key, value>. И что такое запрос? вы вызываете query.orWhere

Shadab Faiz 15.01.2019 11:36

Спасибо, Шадаб. Это не json, это стандартный запрос Graphql. Идентификатор и адрес электронной почты представляют собой поля возврата, которые я хочу вернуть из запроса.

Jake Lowen 15.01.2019 14:47
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
15
2
16 439
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий
  1. Разделите его на 2 функции, чтобы упростить добавление типов
  2. В вашем случае операторы нужно делать orWhere или andWhere
  3. Вместо того, чтобы отображать скобки, поднимите его на один уровень выше.
import { Brackets, WhereExpression, SelectQueryBuilder } from "typeorm";

interface FieldOptions {
  starts_with?: string;
  ends_with?: string;
  contains?: string;
}

interface Fields {
  email?: FieldOptions;
}

interface Where extends Fields {
  OR?: Fields[];
  AND?: Fields[];
}

const handleArgs = (
  query: WhereExpression,
  where: Where,
  andOr: "andWhere" | "orWhere"
) => {
  const whereArgs = Object.entries(where);

  whereArgs.map(whereArg => {
    const [fieldName, filters] = whereArg;
    const ops = Object.entries(filters);

    ops.map(parameters => {
      const [operation, value] = parameters;

      switch (operation) {
        case "is": {
          query[andOr](`${fieldName} = :isvalue`, { isvalue: value });
          break;
        }
        case "not": {
          query[andOr](`${fieldName} != :notvalue`, { notvalue: value });
          break;
        }
        case "in": {
          query[andOr](`${fieldName} IN :invalue`, { invalue: value });
          break;
        }
        case "not_in": {
          query[andOr](`${fieldName} NOT IN :notinvalue`, {
            notinvalue: value
          });
          break;
        }
        case "lt": {
          query[andOr](`${fieldName} < :ltvalue`, { ltvalue: value });
          break;
        }
        case "lte": {
          query[andOr](`${fieldName} <= :ltevalue`, { ltevalue: value });
          break;
        }
        case "gt": {
          query[andOr](`${fieldName} > :gtvalue`, { gtvalue: value });
          break;
        }
        case "gte": {
          query[andOr](`${fieldName} >= :gtevalue`, { gtevalue: value });
          break;
        }
        case "contains": {
          query[andOr](`${fieldName} ILIKE :convalue`, {
            convalue: `%${value}%`
          });
          break;
        }
        case "not_contains": {
          query[andOr](`${fieldName} NOT ILIKE :notconvalue`, {
            notconvalue: `%${value}%`
          });
          break;
        }
        case "starts_with": {
          query[andOr](`${fieldName} ILIKE :swvalue`, {
            swvalue: `${value}%`
          });
          break;
        }
        case "not_starts_with": {
          query[andOr](`${fieldName} NOT ILIKE :nswvalue`, {
            nswvalue: `${value}%`
          });
          break;
        }
        case "ends_with": {
          query[andOr](`${fieldName} ILIKE :ewvalue`, {
            ewvalue: `%${value}`
          });
          break;
        }
        case "not_ends_with": {
          query[andOr](`${fieldName} ILIKE :newvalue`, {
            newvalue: `%${value}`
          });
          break;
        }
        default: {
          break;
        }
      }
    });
  });

  return query;
};

export const filterQuery = <T>(query: SelectQueryBuilder<T>, where: Where) => {
  if (!where) {
    return query;
  }

  Object.keys(where).forEach(key => {
    if (key === "OR") {
      query.andWhere(
        new Brackets(qb =>
          where[key]!.map(queryArray => {
            handleArgs(qb, queryArray, "orWhere");
          })
        )
      );
    } else if (key === "AND") {
      query.andWhere(
        new Brackets(qb =>
          where[key]!.map(queryArray => {
            handleArgs(qb, queryArray, "andWhere");
          })
        )
      );
    }
  });

  return query;
};

Это фантастика. Спасибо, Бен. Единственный случай, который это не распространяется, - это операторы корневого уровня, не вложенные в AND или WHERE .. В случае моего примера запроса выше email: {contains: "lowe"} игнорируется. По вашему мнению, следует ли мне принимать операторы корневого уровня, или делать, я должен требовать, чтобы все операторы были вложены в ИЛИ или И? `` ''

Jake Lowen 15.01.2019 15:37

Теперь, когда я больше возился, становится более разумным иметь только И / ИЛИ в качестве элементов корневого уровня. Это гораздо более явный способ и не доставляет особых неудобств.

Jake Lowen 15.01.2019 16:27

и если вы действительно хотите добавить его на корневой уровень, вы можете добавить еще, где вы вызываете функцию else { handleArgs(query, where, "andWhere") }

benawad 15.01.2019 17:48

Это предполагает, что ни одно из полей не имеет повторяющегося имени, однако, как можно решить случай, когда две таблицы объединены, и функция должна различать id в первом объекте и id во втором объекте без получения ошибки: Error: ER_NON_UNIQ_ERROR: Column 'id' in where clause is ambiguous?

ProGrammer 08.12.2019 08:09

@benawad Простите за это, но вы Бен Авад?

johannchopin 03.10.2020 11:50

Это открывает код для SQL-инъекций, если он используется при вводе пользователем.

Heniker 30.05.2021 21:22

Основываясь на ответе Бена, я немного изменил функции, чтобы сделать объект «фильтр» более универсальным:

// enum
export enum Operator {
  AND = 'AND',
  OR = 'OR',
}

// interfaces
interface FieldOptions {
  is?: string;
  not?: string;
  in?: string;
  not_in?: string;
  lt?: string;
  lte?: string;
  gt?: string;
  gte?: string;
  contains?: string;
  not_contains?: string;
  starts_with?: string;
  not_starts_with?: string;
  ends_with?: string;
  not_ends_with?: string;
}

export interface Field {
  [key: string]: FieldOptions;
}

export type Where = {
  [K in Operator]?: (Where | Field)[];
};

// functions
export const filterQuery = <T>(query: SelectQueryBuilder<T>, where: Where) => {
  if (!where) {
    return query;
  } else {
    return traverseTree(query, where) as SelectQueryBuilder<T>;
  }
};

const traverseTree = (query: WhereExpression, where: Where, upperOperator = Operator.AND) => {
  Object.keys(where).forEach((key) => {
    if (key === Operator.OR) {
      query = query.orWhere(buildNewBrackets(where, Operator.OR));
    } else if (key === Operator.AND) {
      query = query.andWhere(buildNewBrackets(where, Operator.AND));
    } else {
      // Field
      query = handleArgs(query, where as Field, upperOperator === Operator.AND ? 'andWhere' : 'orWhere');
    }
  });

  return query;
};

const buildNewBrackets = (where: Where, operator: Operator) => {
  return new Brackets((qb) =>
    where[operator].map((queryArray) => {
      traverseTree(qb, queryArray, operator);
    }),
  );
};

const handleArgs = (query: WhereExpression, field: Field, andOr: 'andWhere' | 'orWhere') => {
  ...
};

Таким образом, теперь мы можем иметь такой объект в качестве параметра запроса:

{
  AND: [
    {
      OR: [
        {
          name: {
            is: 'John'
          },
        },
        {
          surname: {
            is: 'Doe'
          },
        }
      ]
    },
    {
      AND: [
        {
          age: {
            gt: 30
          },
        },
        {
          type: {
            not: 'Employee'
          }
        }
      ]
    },
    {
      registered_date: {
        gte: '2000-01-01'
      }
    }
  ]
}

В результате будет получен следующий запрос:

SELECT *
FROM users U 
WHERE (U.name = 'John' OR U.surname = 'Doe') AND (U.age > 30 AND U.type != 'Employee') AND U.registered_date >= '2000-01-01';

Другие вопросы по теме