{"id":26964459,"url":"https://github.com/nicklayb/loupe","last_synced_at":"2025-04-03T06:32:28.208Z","repository":{"id":142796472,"uuid":"613093624","full_name":"nicklayb/loupe","owner":"nicklayb","description":"Loupe is a querying syntax","archived":false,"fork":false,"pushed_at":"2024-01-26T03:47:39.000Z","size":117,"stargazers_count":6,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-26T17:42:04.024Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nicklayb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","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":"2023-03-12T21:19:01.000Z","updated_at":"2024-03-15T18:46:41.000Z","dependencies_parsed_at":"2024-09-06T19:29:47.475Z","dependency_job_id":"37db02f3-5b2b-4c76-be77-d77d4b524370","html_url":"https://github.com/nicklayb/loupe","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/nicklayb%2Floupe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Floupe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Floupe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nicklayb%2Floupe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nicklayb","download_url":"https://codeload.github.com/nicklayb/loupe/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246948042,"owners_count":20859366,"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":[],"created_at":"2025-04-03T06:31:36.455Z","updated_at":"2025-04-03T06:32:28.192Z","avatar_url":"https://github.com/nicklayb.png","language":"Elixir","funding_links":["https://ko-fi.com/D1D2YX9OU"],"categories":[],"sub_categories":[],"readme":"# Loupe\n\n[![Coverage Status](https://coveralls.io/repos/github/nicklayb/loupe/badge.svg?branch=main)](https://coveralls.io/github/nicklayb/loupe?branch=main)\n[![Elixir CI](https://github.com/nicklayb/loupe/actions/workflows/elixir.yml/badge.svg)](https://github.com/nicklayb/loupe/actions/workflows/elixir.yml)\n\nLoupe is query language for Ecto schema inspection in a safe and configurable manner.\n\nYou can see [this example app](https://github.com/nicklayb/loupe_example) to understand how it applies with Ecto.\n\n## Important\n\nUntil Loupe reaches `1.x.x`, it's considered experimental. The syntax will change, APIs will change and structure will too. We'll do our best to respect semantic versioning and avoid big breaking changes but they can happen.\n\n## Installation\n\nLoupe is [available in Hex](https://hex.pm/docs/publish), the package can be installed\nby adding `loupe` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:loupe, \"~\u003e 0.11.0\"}\n  ]\nend\n```\n\nThhe documentation can be found at \u003chttps://hexdocs.pm/loupe\u003e.\n\n## Syntax\n\nThe basic syntax has the following format\n\n```\nget [quantifier?] [schema][parameters?] where [predicates]\n```\n\n- `quantifier` is how many records you want. You can provide a positive integer (`1`, `2`, `3` ...), a range (`1..10`, `10..20`, `50..100`) or `all`.\n- `schema` can be an alphanumeric indentifier that you registered in the Definition (See [Ecto Usage](#ecto-usage) for exmaple). The schema is required only for Ecto usage.\n- `parameters` is a json inspired map. It takes the format of `{key: \"value\"}`. Key is an identifier, but value can be any literal type (another object, string, int, float, boolean, list)\n- `predicates` needs to be a combinaison or operators and boolean operators.\n\n### Cool stuff\n\nYou can use `k` and `m` quantifiers for numbers. Writing `get all User where money \u003e 100k` translates to `get all User where money \u003e 100000`.\n\n### Operators\n\nThe are a couple of basic operators like `\u003c`, `\u003e`, `\u003c=`, `\u003e=`, `=`, `!=`.\n\nBut also some textual operators:\n\n- `in` is used with lists, like `age in [18, 21]`\n- `like` is used with strings and automatically wraps in `%`.\n\nYou can also use the keyword `:empty` as a null checker like `age :empty`.\n\nTextual operators and `:empty` can be prefixed with `not` to negate the expression: `not like`, `not in`, `age not :empty`.\n\nFor boolean, the binding can be provided as is and prefixed by `not` for false. Example `where active` or `where not enabled`.\n\n### Boolean Operators\n\nSo far, the syntax supprts `and` and `or` and use parenthese to scope the expressions. Multiple `or` and `and` operations can be grouped together using single ampersand `\u0026` and single pipe `|`, example:\n\n```\nget User where name | email | username | alternate_name like \"John\"\n```\n\nThis is equivalent to\n\n```\nget User where name like \"Jonh\" or email like \"John\" or username like \"John\" or altername_name like \"John\"\n```\n\n### Field variant\n\nRecently, support for \"field variant\" has been added. It's a syntax that allows to \"customize\" a field. The Ecto implementation uses the variant to query composite fields. Assume you have a composite Postgres field that is Money (like the [Money.Ecto.Composite.Type](https://hexdocs.pm/money/Money.Ecto.Composite.Type.html) type from the Money lib), you can now do the following to query the amount:\n\n```\nget User where bank_account:amount \u003e= 1k\n```\n\n### Path binding\n\nLoupe now supports \"Path binding\", being able to specify a path (like a json path) on a field. This is used by te Ecto implementation to query json field like below:\n\n```\nget User where role.permissions[posts, access] = \"write\"\n# or\nget User where role.permissions[\"posts\", \"access\"] = \"write\"\n```\n\n### Variables and external identifiers\n\n#### Query variable\n\nThe library allows you to provide external data to you query. Any identifier (unquote alphanumerical and underscore values) provided on the right side of an operator will be output as such. Taking for instance the Ecto implementation, it allows you to provide external parameter to the query. \n\nA good usecase example could be to automatically provide a `user_id` based from the authenticated user. So you can use it like:\n\n```\nget Posts where author_id = user_id\n```\n\nThen when evaluating the query you make sure to provide the user id by doing\n\n```\nLoupe.Ecto.build_query(query, EctoDefinition, %{}, %{\"user_id\" =\u003e current_user.id})\n```\n\n*Note*: Variables in query are *required*. When evaluating if the query uses a variable that is not provided, an error will be raise.\n\n#### Parameters\n\nThis variables can also be used in parameters. Suppose your implementation supports an `order_by` parameter, you can use it like\n\n```\nget Posts{order_by: {direction: direction, field: field}}\n```\n\nUnlike variables, they don't need to be provided, they are simply extract as such and it's up to you to manipulate them the way you want. For the case of the Ecto implementation, however, they do need to be implemented so they can be extracted in the returning context.\n\n## Ecto usage\n\n### Create a Definition module\n\nThe Definition module is necessary for Loupe to work with your Ecto schema. In this module you define the schemas that are allowed to be queried and the fields that are permitted for querying.\n\nAll callbacks accepts a last argument called \"assigns\". The assigns are provided to you when evaluating the query allowing you to alter the defition. You could, for instance, add a user's role to the assign and use that role to filter out the allowed schemas so that only admins can query Users.\n\n```elixir\ndefmodule MyApp.Loupe.Definition do\n    @moduledoc \"\"\"\n    Example Ecto definition for the modules defined above.\n    \"\"\"\n    @behaviour Loupe.Ecto.Definition\n\n    @schemas %{\n      \"Post\" =\u003e Post,\n      \"User\" =\u003e User,\n      \"Role\" =\u003e Role\n    }\n\n    @impl Loupe.Ecto.Definition\n    def schemas(%{role: \"admin\"}), do: @schemas\n    def schemas(_), do: Map.take(@schemas, [\"Post\", \"User\"])\n\n    @impl Loupe.Ecto.Definition\n    def schema_fields(_, %{role: \"admin\"}), do: :all\n    def schema_fields(Post, _), do: {:only, [:title, :body]}\n    def schema_fields(User, _), do: {:only, [:email, :posts]}\n    def schema_fields(_, _), do: :all\n\n    @impl Loupe.Ecto.Definition\n    def scope_schema(schema, _), do: schema\nend\n```\n\nOnce you have this definition, you can try some queries\n\n```elixir\n{:ok, ast} = Loupe.Language.compile(~s|get all User where age \u003e 18|)\n{:ok, ecto_query} = Loupe.Ecto.build_query(ast, MyApp.Loupe.Definition, %{role: \"admin\"})\nRepo.all(ecto_query)\n```\n\n## Stream / Enumerable\n\nSupport has been added to filter streams or enumerable.\n\nThe same features applies and some more extra;\n\n- You can use a quantifier to limit the stream (`get 3 ...`)\n- You can override the whole comparison logic\n- You can use field variant as \"modifier\" through a custom `Loupe.Stream.Comparator` implementation.\n- You can use sigil for more complex comparison\n\n### Example\n\n```elixir\nposts = [\n  %{title: \"My post\", comments: [%{comment: \"Boring!\", author: \"Homer Simpsons\"}]},\n  %{title: \"My second post\", comments: [%{comment: \"Respect my authorita!\", author: \"Eric Cartman\"}]},\n]\n{:ok, stream} = Loupe.Stream.query(~s|get where comments.author like \"Eric\"|, posts)\n[%{title: \"My second posts\"}] = Enum.to_list(stream)\n```\n\n## Todo\n\nHere are some things that I would like Loupe to support:\n\n- ~Sorting a query, current ideas involves~\n  - This can be achieve with a parameter like `get User{order_by: \"age\"} where ...` and be handled manually by your application\n- ~Support some more complex fields prefixed by ~ (or whatever syntax, inspired by elixir's sigils) like the examples below~\n  - This has been implemented. Field variants can be used for composite fields and sigil can be used for expresions.\n- Implement a LiveView UI lib that shows the strucutres as expandable. Being able to click on a User's `posts` to automatically preload all of its nested Posts.\n  - Also have \"block\" UI module where you can simply create a query from dropdowns in a form for non-power user.\n- Make lexer and parser swappable. Right now, you are stuck with the internal structure that I came up with. The idea would be to allow some to swap the syntax for anything they want. For instance, a french team could implement a french query language to give to their normal user.\n\n## Contributing\n\nYou can see the `CONTRIBUTING.md` file to know more about the contributing guidelines.\n\nPull requests are welcome!\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D2YX9OU)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicklayb%2Floupe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnicklayb%2Floupe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicklayb%2Floupe/lists"}