{"id":21069613,"url":"https://github.com/mtarnovan/manole","last_synced_at":"2026-05-19T17:42:05.979Z","repository":{"id":139552289,"uuid":"160805456","full_name":"mtarnovan/manole","owner":"mtarnovan","description":"Manole is an query builder for Ecto (Elixir)","archived":false,"fork":false,"pushed_at":"2018-12-07T10:02:39.000Z","size":13,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-20T21:48:02.821Z","etag":null,"topics":["ecto","elixir","query-builder"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mtarnovan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-07T09:57:10.000Z","updated_at":"2018-12-07T10:02:40.000Z","dependencies_parsed_at":null,"dependency_job_id":"5144999b-e830-41df-b336-62616ac0a87a","html_url":"https://github.com/mtarnovan/manole","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtarnovan%2Fmanole","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtarnovan%2Fmanole/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtarnovan%2Fmanole/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mtarnovan%2Fmanole/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mtarnovan","download_url":"https://codeload.github.com/mtarnovan/manole/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243511012,"owners_count":20302485,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ecto","elixir","query-builder"],"created_at":"2024-11-19T18:36:21.175Z","updated_at":"2025-12-28T17:22:28.256Z","avatar_url":"https://github.com/mtarnovan.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Manole\n\nManole is a query builder for Ecto.\n\n**N.B. This is a toy project, if you're looking for something viable take a\nlook at [Flop](https://github.com/woylie/flop).**\n\n\u003c!-- MDOC --\u003e\n\nAllows parsing of a filter and appending it to the given queryable.\n\nA filter definition looks like this:\n\n```elixir\nfilter = %{\n  combinator: :or,\n  rules: [\n    %{field: \"name\", operator: \"=\", value: \"Alice\"},\n    %{\n      combinator: :or,\n      rules: [\n        %{field: \"name\", operator: \"=\", value: \"Bob\"},\n        %{field: \"age\", operator: \"\u003e\", value: \"30\"},\n        %{combinator: :and,\n          rules: [\n            %{field: \"name\", operator: \"=\", value: \"Carol\"},\n            %{field: \"age\", operator: \"\u003c\", value: \"27\"},\n            %{field: \"income\", operator: \"\u003e\", value: \"100000\"},\n          ]\n        }\n      ]\n    }\n  ]\n}\n```\n\nGiven this filter and the following data:\n\n- Name: Alice, Age: 30, Income: 50000\n- Name: Bob, Age: 35, Income: 60000\n- Name: Carol, Age: 25, Income: 40000\n\n```elixir\nalias Manole.{Repo, Person}\nRepo.insert!(%Person{name: \"Alice\", age: 30, income: 50000})\nRepo.insert!(%Person{name: \"Bob\", age: 35, income: 60000})\nRepo.insert!(%Manole.Person{name: \"Carol\", age: 25, income: 40000})\n```\n\nWe can build an Ecto query from it:\n\n```elixir\niex\u003e {:ok, query} = Manole.build_query(Person, filter)\n{:ok,\n #Ecto.Query\u003cfrom p0 in Manole.Person,\n  where: p0.name == ^\"Alice\" or\n  (p0.name == ^\"Bob\" or p0.age \u003e ^\"30\" or\n     (p0.name == ^\"Carol\" and p0.age \u003c ^\"27\" and p0.income \u003e ^\"100000\"))\u003e}\niex\u003e Repo.all(query) |\u003e Enum.map(\u0026 \u00261.name)\n[\"Alice\", \"Bob\"]\n```\n\n`Carol` is excluded because she does not match \"Alice\", \"Bob\", or \"Age \u003e 30\",\nand fails the income requirement (\u003e 100000) of the nested AND block.\n\n### Association Support\n\nIf a rule contains a field with dots, it is interpreted as an association. The\nqueryable is inspected to check if it already has a named binding for that join,\nand if it doesn't, it is added automatically.\n\n```elixir\nfilter = %{\n  rules: [%{value: \"one\", operator: :contains, field: \"dogs.toys.name\"}],\n  combinator: :or\n}\n\nalias Manole.{Repo, Person, Factory}\n\nFactory.insert_person_with_dog_and_toy(\n  %{name: \"Alice\", age: 30},\n  %{name: \"Gigi\"},\n  %{name: \"Bone\", color: \"blue\"}\n)\nFactory.insert_person_with_dog_and_toy(\n  %{name: \"Bob\", age: 30},\n  %{name: \"Jimbo\"},\n  %{name: \"Trombone\", color: \"pink\"}\n)\nFactory.insert_person_with_dog_and_toy(\n  %{name: \"Carol\", age: 30},\n  %{name: \"Rex\"},\n  %{name: \"Ball\", color: \"red\"}\n)\n{:ok, query} = Manole.build_query(Person, filter)\n```\n\nwould result in something like this:\n\n```elixir\nEcto.Query\u003cfrom p0 in Manole.Person, join: d1 in assoc(p0, :dogs), as: :dogs,\n  join: t2 in assoc(d1, :toys), as: :dogs_toys,\n  where: ilike(as(:dogs_toys).name, ^\"%one%\")\u003e\n```\n\n### Allowlisting (Security)\n\nAn allowlist is a list of fields to allow on the input queryable and the\nassociations. By default (if no allowlist is provided), all fields are allowed.\nIf an allowlist is provided, only fields in the list are accessible.\n\n#### Example:\n\nAssuming the input queryable is a `Post`, an allowlist given as:\n\n```elixir\nopts = [\n  allowlist: [\n    :title,\n    comments: [:inserted_at, tags: [:name]]\n  ]\n]\nManole.build_query(Post, filter, opts)\n```\n\nthis would allow filtering on `post.title`, `post.comments.inserted_at` and\n`post.comments.tags.name`.\n\nIf a field in the filter is not found in the allowlist, `{:error, \"Field '...'\nis not in allowlist\"}` is returned.\n\n### Supported Operators\n\n- `=`: Equal (`==`, `eq`)\n- `!=`: Not Equal (`neq`)\n- `\u003e`: Greater Than (`gt`)\n- `\u003e=`: Greater Than or Equal (`gte`)\n- `\u003c`: Less Than (`lt`)\n- `\u003c=`: Less Than or Equal (`lte`)\n- `contains`: Case-insensitive substring match (`ilike %value%`). Wildcards `%`\n  and `_` in the value are escaped.\n\n\u003c!-- MDOC --\u003e\n\n# TODOs\n\n- [x] implement allowlisting\n- [x] add support for joins and querying on association\n- [x] remove dependency on libgraph\n- [ ] CI/CD Pipeline (GitHub Actions)\n- [ ] Expanded Operator Support (`in`, `is_nil`)\n- [ ] Test Coverage \u0026 Docs (`mix coveralls`, `ExDoc`)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmtarnovan%2Fmanole","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmtarnovan%2Fmanole","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmtarnovan%2Fmanole/lists"}