web-dev-qa-db-fra.com

Typeorm Builder Dynamic Dynamic à 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 à la façon dont Prisma fonctionne:

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

À l'intérieur de mon résolveur, je nourris les arguments à travers une fonction pour analyser la structure et utiliser le constructeur de requêtes de Typeorm pour le convertir en sql approprié. 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 (un peu) mais n'envère pas les requêtes et/ou les requêtes comme je m'attendais (et avait déjà eu du travail à Knex). La fonction ci-dessus génère le 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'attendrais à voir quelque chose de plus comme:

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

Pardonnez la désensibilité, une requête répétitive. J'essaie juste d'illustrer les déclarations imbriquées attendues.

Comment puis-je forcer les et/ou les branches de la fonction de mon constructeur de requêtes pour devenir correctement niché comme prévu?

** Points bonus Si quelqu'un peut m'aider à comprendre les dactylographies dossées réelles ici **

11
Jake Lowen

Basé sur la réponse de Ben, j'ai modifié un peu les fonctions pour permettre un objet plus polyvalent "filter" 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 résultante serait:

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

Je dois faire une recherche plus complexe. Cela couvre-t-il aussi bien ce cas d'utilisation?

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