odata-sequelize
Version:
Advanced OData v4 parser for Sequelize.JS with support for $expand, lambda expressions, navigation properties, and complex filters
807 lines (681 loc) • 18.6 kB
Markdown
# odata-sequelize
[](https://opensource.org/licenses/MIT)

[](https://travis-ci.org/Vicnovais/odata-sequelize)

[](https://nodei.co/npm/odata-sequelize/)
## Objective
This library is intended to take an OData query string as a parameter and transform it on a
sequelize-compliant query.
## 🚀 **Latest Features (v2.0)**
- **Navigation Properties**: Filter on related entity properties (`Customer/CompanyName eq 'Acme'`)
- **Lambda Expressions**: Query child tables with `any`/`all` operators (`Orders/any(o: o/Amount gt 100)`)
- **Complex Mixed Logic**: Advanced AND/OR combinations with deep parentheses nesting
- **Smart Include Merging**: Automatic merging of `$expand` and navigation filters
- **0 deps**: Now the library has no dependencies!
## Requirements
- Node.JS
- NPM
- Sequelize.JS
## Installing
Simply run a npm command to install it in your project:
npm install odata-sequelize
## How does it work?
The OData query string is parsed using a custom PEG.js grammar that handles the complete OData specification. The resulting abstract syntax tree (AST) is then transformed using a visitor pattern to build a sequelize-compliant query object.
## Roadmap
### Completed Features
All planned features have been implemented!
### Boolean Operators
- [x] AND
- [x] OR
- [x] NOT
### Comparison Operators
- [x] Equal (eq)
- [x] Not Equal (ne)
- [x] Greater Than (gt)
- [x] Greater Than or Equal (ge)
- [x] Less Than (lt)
- [x] Less Than or Equal (le)
### Functions
1. String Functions
- [x] substringof
- [x] endswith
- [x] startswith
- [x] tolower
- [x] toupper
- [x] trim
- [x] concat
- [x] substring
- [x] replace
- [x] indexof
2. Date Functions
- [x] day
- [x] hour
- [x] minute
- [x] month
- [x] second
- [x] year
### Advanced Features
- [x] **$expand** - Eager loading associations with nested support
- [x] **Lambda expressions** - `any`/`all` operators for child table queries
- [x] **Navigation properties** - Filter on related entity properties (e.g., `Customer/CompanyName`)
- [x] **Mixed logical operators** - Complex AND/OR combinations with parentheses
- [x] **Function integration** - Functions combined with navigation and lambda expressions
- [x] **Include merging** - Smart merging of $expand and navigation filters
- [x] **Precedence handling** - Proper parentheses and operator precedence support
### Core OData Query Options
- [x] **$filter** - Complex filtering with all operators and functions
- [x] **$select** - Choose specific fields to return
- [x] **$expand** - Eager load related entities
- [x] **$top** - Limit number of results
- [x] **$skip** - Pagination offset
- [x] **$orderby** - Sorting with multiple fields
### Development & Quality
- [x] Test (Jest) - Thanks to [@remcohaszing](https://github.com/remcohaszing)
- [x] Lint & Prettier - Thanks to [@remcohaszing](https://github.com/remcohaszing)
- [x] **86 comprehensive tests** - Including complex integration scenarios
- [x] **76% code coverage** - High-quality test coverage
## How to Use
You just need to pass an OData query string as parameter with your sequelize object instance, and
automagically it is converted to a sequelize query.
**Usage Example:**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData(
"$top=5&$skip=1&$select=Foo,Bar&$filter=Foo eq 'Test' or Bar eq 'Test'&$orderby=Foo desc",
sequelize
);
// Supposing you have your sequelize model
Model.findAll(query);
```
See the examples below to checkout what's created under the hood:
**1) Simple Query with Top, Skip, Select, Filter and OrderBy**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData(
"$top=5&$skip=1&$select=Foo,Bar&$filter=Foo eq 'Test' or Bar eq 'Test'&$orderby=Foo desc",
sequelize
);
```
query becomes...
```javascript
{
attributes: ['Foo', 'Bar'],
limit: 5,
offset: 1,
order: [
['Foo', 'DESC']
],
where: {
[Op.or]: [
{
Foo: {
[Op.eq]: "Test"
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**2) Complex Query with Precedence**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData(
"$filter=(Foo eq 'Test' or Bar eq 'Test') and ((Foo ne 'Lorem' or Bar ne 'Ipsum') and (Year gt 2017))",
sequelize
);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
[Op.or]: [
{
Foo: {
[Op.eq]: "Test"
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
},
{
[Op.and]: [
{
[Op.or]: [
{
Foo: {
[Op.ne]: "Lorem"
},
},
{
Bar: {
[Op.ne]: "Ipsum"
}
}
]
},
{
Year: {
[Op.gt]: 2017
}
}
]
}
]
}
}
```
**3) Using Date**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData(
"$filter=Foo eq 'Test' and Date gt datetime'2012-09-27T21:12:59'",
sequelize
);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Foo: {
[Op.eq]: "Test"
}
},
{
Date: {
[Op.gt]: new Date("2012-09-27T21:12:59")
}
}
]
}
}
```
**4) startswith function**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=startswith('lorem', Foo) and Bar eq 'Test'", sequelize);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Foo: {
[Op.like]: "lorem%"
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**5) substringof function**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=substringof('lorem', Foo) and Bar eq 'Test'", sequelize);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Foo: {
[Op.like]: "%lorem%"
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**6) startswith function**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=startswith('Foo', Name) and Bar eq 'Test'", sequelize);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Name: {
[Op.like]: "Foo%"
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**7) trim function**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=trim(Name) eq 'Foo' and Bar eq 'Test'", sequelize);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Name: {
comparator: [Op.eq],
logic: "Foo",
attribute: {
fn: "trim",
args: [
{
col: "Name"
}
]
}
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**8) tolower function**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=tolower(Name) eq 'foobaz' and Name eq 'bar'", sequelize);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Name: {
comparator: [Op.eq],
logic: "foobaz",
attribute: {
fn: "lower",
args: [
{
col: "Name"
}
]
}
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**9) toupper function**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=toupper(Name) eq 'FOOBAZ' and Name eq 'bar'", sequelize);
```
query becomes...
```javascript
{
where: {
[Op.and]: [
{
Name: {
comparator: [Op.eq],
logic: "FOOBAZ",
attribute: {
fn: "upper",
args: [
{
col: "Name"
}
]
}
}
},
{
Bar: {
[Op.eq]: "Test"
}
}
]
}
}
```
**10) year, month, day, hour, minute, second function**
- The same logic applies to all 6 date functions. The only difference resides in attribute object,
whose "fn" property reflects the called function.
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=year(StartDate) gt 2017", sequelize);
```
becomes...
```javascript
{
where: {
{
StartDate: {
comparator: [Op.gt],
logic: 2017,
attribute: {
fn: "year",
args: [
{
col: "StartDate"
}
]
}
}
}
}
}
```
**11) $expand for eager loading associations**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$expand=Orders", sequelize);
```
becomes...
```javascript
{
include: [
{
association: "Orders"
}
]
}
```
**12) Multiple $expand**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$expand=Orders,Customer", sequelize);
```
becomes...
```javascript
{
include: [
{
association: "Orders"
},
{
association: "Customer"
}
]
}
```
**13) Nested $expand**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$expand=Orders/OrderItems", sequelize);
```
becomes...
```javascript
{
include: [
{
association: "Orders",
include: [
{
association: "OrderItems"
}
]
}
]
}
```
**14) Complex query with $expand**
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData(
"$select=Name,Id&$expand=Orders&$top=10&$filter=Active eq true",
sequelize
);
```
becomes...
```javascript
{
attributes: ["Name", "Id"],
limit: 10,
include: [
{
association: "Orders"
}
],
where: {
Active: {
[Op.eq]: true
}
}
}
```
**15) Query in children tables (Lambda expressions)**
The library fully supports OData lambda expressions (`any` and `all` operators) for filtering parent entities based on child entity properties. This powerful feature allows you to query related data with complex conditions.
```javascript
// Filter customers who have orders with amount > 100
var query = parseOData("$filter=Orders/any(o: o/Amount gt 100)", sequelize);
// Filter customers where all orders are shipped
var query = parseOData("$filter=Orders/all(o: o/Status eq 'Shipped')", sequelize);
```
Expected output format:
```javascript
{
include: [
{
association: "Orders",
where: {
Amount: {
[Op.gt]: 100
}
},
required: true // 'any' uses INNER JOIN, 'all' uses different logic
}
]
}
```
**Key Features of Lambda Expressions:**
- **`any` operator**: Returns parent records if at least one child matches the condition (uses `required: true` for INNER JOIN)
- **`all` operator**: Returns parent records if all children match the condition
- **Variable scoping**: Supports variable names in lambda expressions (e.g., `o: o/Amount`)
- **Complex conditions**: Supports nested property access and multiple comparison operators
- **Sequelize integration**: Converts to appropriate `include` structures with `where` clauses
**16) Navigation Properties - Filtering on Related Entity Properties**
You can filter parent entities based on properties of related entities using navigation syntax:
```javascript
var parseOData = require("odata-sequelize");
var sequelize = require("sequelize");
var query = parseOData("$filter=Customer/CompanyName eq 'Acme Corp'", sequelize);
```
becomes...
```javascript
{
include: [
{
association: "Customer",
where: {
CompanyName: {
[Op.eq]: "Acme Corp"
}
},
required: true
}
]
}
```
**17) Navigation Properties with $expand**
Navigation filters automatically merge with $expand when targeting the same association:
```javascript
var query = parseOData(
"$filter=Customer/Country ne 'USA'&$expand=Customer",
sequelize
);
```
becomes...
```javascript
{
include: [
{
association: "Customer",
where: {
Country: {
[Op.ne]: "USA"
}
},
required: true
}
]
}
```
**18) Complex Mixed AND/OR with Parentheses**
The parser handles deeply nested logical expressions with proper precedence:
```javascript
var query = parseOData(
"$filter=((Type eq 'A' or Type eq 'B') and Status eq 'Active') or (Category eq 'Premium' and Year gt 2020)",
sequelize
);
```
becomes...
```javascript
{
where: {
[Op.or]: [
{
[Op.and]: [
{
[Op.or]: [
{
Type: {
[Op.eq]: "A"
}
},
{
Type: {
[Op.eq]: "B"
}
}
]
},
{
Status: {
[Op.eq]: "Active"
}
}
]
},
{
[Op.and]: [
{
Category: {
[Op.eq]: "Premium"
}
},
{
Year: {
[Op.gt]: 2020
}
}
]
}
]
}
}
```
**19) Mixed Function and Navigation Filters**
Complex queries combining function calls, navigation properties, and $expand:
```javascript
var query = parseOData(
"$filter=tolower(CompanyName) eq 'acme corp' and Customer/Country ne 'USA'&$expand=Customer&$orderby=OrderDate desc",
sequelize
);
```
becomes...
```javascript
{
order: [["OrderDate", "DESC"]],
where: {
[Op.and]: [
{
CompanyName: {
attribute: {
fn: "tolower",
args: [{ col: "CompanyName" }]
},
comparator: [Op.eq],
logic: "acme corp"
}
}
]
},
include: [
{
association: "Customer",
where: {
Country: {
[Op.ne]: "USA"
}
},
required: true
}
]
}
```
**20) Complete Integration Example**
Real-world complex query combining all features:
```javascript
var query = parseOData(
"$select=Name,Status,Priority&$expand=Customer,Orders&$top=20&$skip=10&$orderby=Priority desc,Name asc&$filter=((Status eq 'Active' and Priority ge 3) or (Customer/Country eq 'USA' and Orders/any(o: o/Amount gt 500))) and year(CreatedDate) ge 2023",
sequelize
);
```
This generates a comprehensive Sequelize query with:
- **Attributes selection** (`$select`)
- **Eager loading** (`$expand`)
- **Pagination** (`$top`, `$skip`)
- **Ordering** (`$orderby`)
- **Complex filtering** with nested logic, navigation properties, lambda expressions, and function calls