12 votes

Constructeur de requêtes dynamiques Typeorm à partir d'un objet structuré

Pour une utilisation dans un serveur graphql, j'ai défini un type d'entrée structuré dans lequel vous pouvez spécifier un certain nombre de conditions de filtrage très similaires au fonctionnement de prisma :

enter image description here

Ce qui me permet de soumettre des filtres structurés dans une requête comme :

{
  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
  }
}

Dans mon résolveur, j'envoie le fichier args.where à travers une fonction pour analyser la structure et utiliser le constructeur de requêtes de TypeOrm pour la convertir en SQL correct. L'intégralité de la fonction est :

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;
};

Ce qui fonctionne (en quelque sorte) mais n'imbrique pas les requêtes AND/OR comme je l'aurais souhaité (et que j'avais déjà réussi à faire fonctionner dans KNEX). La fonction ci-dessus génère le code 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%"]

Mais je m'attendais à voir quelque chose de plus proche :

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

Pardonnez cette question absurde et répétitive. J'essaie simplement d'illustrer les déclarations NESTED attendues.

Comment puis-je forcer les branches AND/OR de ma fonction de construction de requête à s'imbriquer correctement comme prévu ?

** Points bonus si quelqu'un peut m'aider à trouver les caractères typographiques réels ici **

12voto

benawad Points 115
  1. Le diviser en 2 fonctions pour faciliter l'ajout des types
  2. Dans vos déclarations, vous devez faire orWhere ou andWhere
  3. Au lieu de cartographier les supports, soulevez-les d'un niveau.

    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; };

0voto

manuelnucci Points 83

En me basant sur la réponse de Ben, j'ai légèrement modifié les fonctions pour permettre une utilisation plus polyvalente de " filtre " objet :

// 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') => {
  ...
};

De cette façon, nous pouvons maintenant avoir ce type d'objet en tant que paramètre de requête :

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

La requête qui en résulterait serait la suivante :

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';

-1voto

StS Points 183

Je dois effectuer une recherche plus complexe. Cela couvre-t-il également ce cas d'utilisation ?

{
  "AND":[
    {"name":{"contains":"Peter"}},
    {"OR": [
      {"phone_001":{"contains":"123455621"}},
      {"phone_002":{"contains":"123455621"}}
    ]}
  ]
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X