ExOdata4
An OData v4 query parser and Ecto query builder for Elixir. Parse OData query strings or full URIs and get back Ecto queries ready to execute against your own repo. Designed for exposing internal Elixir services to BI tools like Power BI and Excel.
Installation
Add ex_odata4 to your dependencies in mix.exs:
def deps do
[
{:ex_odata4, "~> 0.2.0"}
]
endConfiguration
Register your Ecto schemas in config/config.exs:
config :ex_odata4, schemas: %{
"Orders" => MyApp.Orders,
"Products" => MyApp.Products
}
OData field names are automatically derived from your schema — Ecto's snake_case atoms become PascalCase OData names. So :first_name is exposed as FirstName, :amount as Amount, and so on. No additional mapping is required.
Usage
Parsing a full OData URI
ExOdata4.parse_uri("/Orders?$filter=Amount gt 1000&$top=25&$skip=0")
|> MyApp.Repo.all()Parsing a query string directly
ExOdata4.get("Orders", "$filter=Status eq 'active'&$orderby=Amount desc&$top=10")
|> MyApp.Repo.all()
Both functions return an %Ecto.Query{} ready to pipe into your repo.
$metadata
For Power BI and Excel, serve the $metadata document from your router:
# In your Phoenix router or Plug
get "/$metadata", fn conn, _ ->
xml = ExOdata4.Metadata.generate(MyApp.Orders, namespace: "MyApp")
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, xml)
endgenerate/2 accepts an optional :namespace (defaults to "Default").
Supported OData query options
| Option | Example |
|---|---|
$filter | $filter=Name eq 'John' |
$top | $top=25 |
$skip | $skip=50 |
$orderby | $orderby=Amount desc,Name asc |
$metadata | GET /$metadata |
Filter operators
| Operator | Meaning |
|---|---|
eq | Equal |
ne | Not equal |
gt | Greater than |
ge | Greater than or equal |
lt | Less than |
le | Less than or equal |
and | Logical and |
or | Logical or |
Filter functions
| Function | Example | SQL |
|---|---|---|
contains | contains(Name, 'Jo') | LIKE '%Jo%' |
startswith | startswith(Name, 'Jo') | LIKE 'Jo%' |
endswith | endswith(Email, '.com') | LIKE '%.com' |
tolower | tolower(Name) eq 'john' | lower(name) = 'john' |
toupper | toupper(Name) eq 'JOHN' | upper(name) = 'JOHN' |
year | year(Date) eq 2024 | extract(year from date) = 2024 |
month | month(Date) eq 1 | extract(month from date) = 1 |
day | day(Date) eq 15 | extract(day from date) = 15 |
hour | hour(Timestamp) gt 8 | extract(hour from timestamp) > 8 |
Functions can be combined with logical operators:
$filter=contains(Name, 'John') and Amount gt 100
$filter=tolower(Status) eq 'active' or year(Date) gt 2023Supported literal types
-
Strings:
'hello' -
Integers:
42,-7 -
Decimals:
3.14 -
Booleans:
true,false -
Null:
null -
Dates:
2024-01-01 -
DateTimeOffset:
2024-01-01T00:00:00Z -
GUIDs:
00000000-0000-0000-0000-000000000000
EDM type mapping
$metadata automatically maps Ecto types to OData EDM types:
| Ecto type | EDM type |
|---|---|
:string | Edm.String |
:integer | Edm.Int32 |
:float | Edm.Double |
:decimal | Edm.Decimal |
:boolean | Edm.Boolean |
:date | Edm.Date |
:utc_datetime / :naive_datetime | Edm.DateTimeOffset |
:binary_id | Edm.Guid |
Not yet supported
$select— field projection$expand— loading related entities$count— total result countnot— logical negation-
Math functions:
round(),floor(),ceiling() -
Lambda operators:
any(),all() - Navigation properties
Contributions are welcome. Open an issue or PR on GitHub.
License
MIT