mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-02-12 03:26:08 +00:00
* Introduce `logic_operator` field to combine filters with OR logic.
* Implement grouping for consecutive OR filters to ensure proper SQL precedence.
* Add support for custom SQL operators in filter conditions.
* Enhance `fetch_row_number` functionality to return specific record with its position.
* Update tests to cover new filter logic and grouping behavior.
Features Implemented:
1. OR Logic Filter Support (SearchOr)
- Added to resolvespec, restheadspec, and websocketspec
- Consecutive OR filters are automatically grouped with parentheses
- Prevents SQL logic errors: (A OR B OR C) AND D instead of A OR B OR C AND D
2. CustomOperators
- Allows arbitrary SQL conditions in resolvespec
- Properly integrated with filter logic
3. FetchRowNumber
- Uses SQL window functions: ROW_NUMBER() OVER (ORDER BY ...)
- Returns only the specific record (not all records)
- Available in resolvespec and restheadspec
- Perfect for "What's my rank?" queries
4. RowNumber Field Auto-Population
- Now available in all three packages: resolvespec, restheadspec, and websocketspec
- Uses simple offset-based math: offset + index + 1
- Automatically populates RowNumber int64 field if it exists on models
- Perfect for displaying paginated lists with sequential numbering
573 lines
12 KiB
Markdown
573 lines
12 KiB
Markdown
# ResolveSpec Query Features Examples
|
|
|
|
This document provides examples of using the advanced query features in ResolveSpec, including OR logic filters, Custom Operators, and FetchRowNumber.
|
|
|
|
## OR Logic in Filters (SearchOr)
|
|
|
|
### Basic OR Filter Example
|
|
|
|
Find all users with status "active" OR "pending":
|
|
|
|
```json
|
|
POST /users
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "active"
|
|
},
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "pending",
|
|
"logic_operator": "OR"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Combined AND/OR Filters
|
|
|
|
Find users with (status="active" OR status="pending") AND age >= 18:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "active"
|
|
},
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "pending",
|
|
"logic_operator": "OR"
|
|
},
|
|
{
|
|
"column": "age",
|
|
"operator": "gte",
|
|
"value": 18
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**SQL Generated:** `WHERE (status = 'active' OR status = 'pending') AND age >= 18`
|
|
|
|
**Important Notes:**
|
|
- By default, filters use AND logic
|
|
- Consecutive filters with `"logic_operator": "OR"` are automatically grouped with parentheses
|
|
- This grouping ensures OR conditions don't interfere with AND conditions
|
|
- You don't need to specify `"logic_operator": "AND"` as it's the default
|
|
|
|
### Multiple OR Groups
|
|
|
|
You can have multiple separate OR groups:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "active"
|
|
},
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "pending",
|
|
"logic_operator": "OR"
|
|
},
|
|
{
|
|
"column": "priority",
|
|
"operator": "eq",
|
|
"value": "high"
|
|
},
|
|
{
|
|
"column": "priority",
|
|
"operator": "eq",
|
|
"value": "urgent",
|
|
"logic_operator": "OR"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**SQL Generated:** `WHERE (status = 'active' OR status = 'pending') AND (priority = 'high' OR priority = 'urgent')`
|
|
|
|
## Custom Operators
|
|
|
|
### Simple Custom SQL Condition
|
|
|
|
Filter by email domain using custom SQL:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"customOperators": [
|
|
{
|
|
"name": "company_emails",
|
|
"sql": "email LIKE '%@company.com'"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Multiple Custom Operators
|
|
|
|
Combine multiple custom SQL conditions:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"customOperators": [
|
|
{
|
|
"name": "recent_active",
|
|
"sql": "last_login > NOW() - INTERVAL '30 days'"
|
|
},
|
|
{
|
|
"name": "high_score",
|
|
"sql": "score > 1000"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Complex Custom Operator
|
|
|
|
Use complex SQL expressions:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"customOperators": [
|
|
{
|
|
"name": "priority_users",
|
|
"sql": "(subscription = 'premium' AND points > 500) OR (subscription = 'enterprise')"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Combining Custom Operators with Regular Filters
|
|
|
|
Mix custom operators with standard filters:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "country",
|
|
"operator": "eq",
|
|
"value": "USA"
|
|
}
|
|
],
|
|
"customOperators": [
|
|
{
|
|
"name": "active_last_month",
|
|
"sql": "last_activity > NOW() - INTERVAL '1 month'"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Row Numbers
|
|
|
|
### Two Ways to Get Row Numbers
|
|
|
|
There are two different features for row numbers:
|
|
|
|
1. **`fetch_row_number`** - Get the position of ONE specific record in a sorted/filtered set
|
|
2. **`RowNumber` field in models** - Automatically number all records in the response
|
|
|
|
### 1. FetchRowNumber - Get Position of Specific Record
|
|
|
|
Get the rank/position of a specific user in a leaderboard. **Important:** When `fetch_row_number` is specified, the response contains **ONLY that specific record**, not all records.
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"sort": [
|
|
{
|
|
"column": "score",
|
|
"direction": "desc"
|
|
}
|
|
],
|
|
"fetch_row_number": "12345"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response - Contains ONLY the specified user:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"id": 12345,
|
|
"name": "Alice Smith",
|
|
"score": 9850,
|
|
"level": 42
|
|
},
|
|
"metadata": {
|
|
"total": 10000,
|
|
"count": 1,
|
|
"filtered": 10000,
|
|
"row_number": 42
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:** User "12345" is ranked #42 out of 10,000 users. The response includes only Alice's data, not the other 9,999 users.
|
|
|
|
### Row Number with Filters
|
|
|
|
Find position within a filtered subset (e.g., "What's my rank in my country?"):
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "country",
|
|
"operator": "eq",
|
|
"value": "USA"
|
|
},
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "active"
|
|
}
|
|
],
|
|
"sort": [
|
|
{
|
|
"column": "score",
|
|
"direction": "desc"
|
|
}
|
|
],
|
|
"fetch_row_number": "12345"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"id": 12345,
|
|
"name": "Bob Johnson",
|
|
"country": "USA",
|
|
"score": 7200,
|
|
"status": "active"
|
|
},
|
|
"metadata": {
|
|
"total": 2500,
|
|
"count": 1,
|
|
"filtered": 2500,
|
|
"row_number": 156
|
|
}
|
|
}
|
|
```
|
|
|
|
**Result:** Bob is ranked #156 out of 2,500 active USA users. Only Bob's record is returned.
|
|
|
|
### 2. RowNumber Field - Auto-Number All Records
|
|
|
|
If your model has a `RowNumber int64` field, restheadspec will automatically populate it for paginated results.
|
|
|
|
**Model Definition:**
|
|
```go
|
|
type Player struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Score int64 `json:"score"`
|
|
RowNumber int64 `json:"row_number"` // Will be auto-populated
|
|
}
|
|
```
|
|
|
|
**Request (with pagination):**
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"sort": [{"column": "score", "direction": "desc"}],
|
|
"limit": 10,
|
|
"offset": 20
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response - RowNumber automatically set:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": [
|
|
{
|
|
"id": 456,
|
|
"name": "Player21",
|
|
"score": 8900,
|
|
"row_number": 21
|
|
},
|
|
{
|
|
"id": 789,
|
|
"name": "Player22",
|
|
"score": 8850,
|
|
"row_number": 22
|
|
},
|
|
{
|
|
"id": 123,
|
|
"name": "Player23",
|
|
"score": 8800,
|
|
"row_number": 23
|
|
}
|
|
// ... records 24-30 ...
|
|
]
|
|
}
|
|
```
|
|
|
|
**How It Works:**
|
|
- `row_number = offset + index + 1` (1-based)
|
|
- With offset=20, first record gets row_number=21
|
|
- With offset=20, second record gets row_number=22
|
|
- Perfect for displaying "Rank" in paginated tables
|
|
|
|
**Use Case:** Displaying leaderboards with rank numbers:
|
|
```
|
|
Rank | Player | Score
|
|
-----|-----------|-------
|
|
21 | Player21 | 8900
|
|
22 | Player22 | 8850
|
|
23 | Player23 | 8800
|
|
```
|
|
|
|
**Note:** This feature is available in all three packages: resolvespec, restheadspec, and websocketspec.
|
|
|
|
### When to Use Each Feature
|
|
|
|
| Feature | Use Case | Returns | Performance |
|
|
|---------|----------|---------|-------------|
|
|
| `fetch_row_number` | "What's my rank?" | 1 record with position | Fast - 1 record |
|
|
| `RowNumber` field | "Show top 10 with ranks" | Many records numbered | Fast - simple math |
|
|
|
|
**Combined Example - Full Leaderboard UI:**
|
|
|
|
```javascript
|
|
// Request 1: Get current user's rank
|
|
const userRank = await api.read({
|
|
fetch_row_number: currentUserId,
|
|
sort: [{column: "score", direction: "desc"}]
|
|
});
|
|
// Returns: {id: 123, name: "You", score: 7500, row_number: 156}
|
|
|
|
// Request 2: Get top 10 with rank numbers
|
|
const top10 = await api.read({
|
|
sort: [{column: "score", direction: "desc"}],
|
|
limit: 10,
|
|
offset: 0
|
|
});
|
|
// Returns: [{row_number: 1, ...}, {row_number: 2, ...}, ...]
|
|
|
|
// Display:
|
|
// "Your Rank: #156"
|
|
// "Top Players:"
|
|
// "#1 - Alice - 9999"
|
|
// "#2 - Bob - 9876"
|
|
// ...
|
|
```
|
|
|
|
## Complete Example: Advanced Query
|
|
|
|
Combine all features for a complex query:
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"columns": ["id", "name", "email", "score", "status"],
|
|
"filters": [
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "active"
|
|
},
|
|
{
|
|
"column": "status",
|
|
"operator": "eq",
|
|
"value": "trial",
|
|
"logic_operator": "OR"
|
|
},
|
|
{
|
|
"column": "score",
|
|
"operator": "gte",
|
|
"value": 100
|
|
}
|
|
],
|
|
"customOperators": [
|
|
{
|
|
"name": "recent_activity",
|
|
"sql": "last_login > NOW() - INTERVAL '7 days'"
|
|
},
|
|
{
|
|
"name": "verified_email",
|
|
"sql": "email_verified = true"
|
|
}
|
|
],
|
|
"sort": [
|
|
{
|
|
"column": "score",
|
|
"direction": "desc"
|
|
},
|
|
{
|
|
"column": "created_at",
|
|
"direction": "asc"
|
|
}
|
|
],
|
|
"fetch_row_number": "12345",
|
|
"limit": 50,
|
|
"offset": 0
|
|
}
|
|
}
|
|
```
|
|
|
|
This query:
|
|
- Selects specific columns
|
|
- Filters for users with status "active" OR "trial"
|
|
- AND score >= 100
|
|
- Applies custom SQL conditions for recent activity and verified emails
|
|
- Sorts by score (descending) then creation date (ascending)
|
|
- Returns the row number of user "12345" in this filtered/sorted set
|
|
- Returns 50 records starting from the first one
|
|
|
|
## Use Cases
|
|
|
|
### 1. Leaderboards - Get Current User's Rank
|
|
|
|
Get the current user's position and data (returns only their record):
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "game_id",
|
|
"operator": "eq",
|
|
"value": "game123"
|
|
}
|
|
],
|
|
"sort": [
|
|
{
|
|
"column": "score",
|
|
"direction": "desc"
|
|
}
|
|
],
|
|
"fetch_row_number": "current_user_id"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Tip:** For full leaderboards, make two requests:
|
|
1. One with `fetch_row_number` to get user's rank
|
|
2. One with `limit` and `offset` to get top players list
|
|
|
|
### 2. Multi-Status Search
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"filters": [
|
|
{
|
|
"column": "order_status",
|
|
"operator": "eq",
|
|
"value": "pending"
|
|
},
|
|
{
|
|
"column": "order_status",
|
|
"operator": "eq",
|
|
"value": "processing",
|
|
"logic_operator": "OR"
|
|
},
|
|
{
|
|
"column": "order_status",
|
|
"operator": "eq",
|
|
"value": "shipped",
|
|
"logic_operator": "OR"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Advanced Date Filtering
|
|
|
|
```json
|
|
{
|
|
"operation": "read",
|
|
"options": {
|
|
"customOperators": [
|
|
{
|
|
"name": "this_month",
|
|
"sql": "created_at >= DATE_TRUNC('month', CURRENT_DATE)"
|
|
},
|
|
{
|
|
"name": "business_hours",
|
|
"sql": "EXTRACT(HOUR FROM created_at) BETWEEN 9 AND 17"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
**Warning:** Custom operators allow raw SQL, which can be a security risk if not properly handled:
|
|
|
|
1. **Never** directly interpolate user input into custom operator SQL
|
|
2. Always validate and sanitize custom operator SQL on the backend
|
|
3. Consider using a whitelist of allowed custom operators
|
|
4. Use prepared statements or parameterized queries when possible
|
|
5. Implement proper authorization checks before executing queries
|
|
|
|
Example of safe custom operator handling in Go:
|
|
|
|
```go
|
|
// Whitelist of allowed custom operators
|
|
allowedOperators := map[string]string{
|
|
"recent_week": "created_at > NOW() - INTERVAL '7 days'",
|
|
"active_users": "status = 'active' AND last_login > NOW() - INTERVAL '30 days'",
|
|
"premium_only": "subscription_level = 'premium'",
|
|
}
|
|
|
|
// Validate custom operators from request
|
|
for _, op := range req.Options.CustomOperators {
|
|
if sql, ok := allowedOperators[op.Name]; ok {
|
|
op.SQL = sql // Use whitelisted SQL
|
|
} else {
|
|
return errors.New("custom operator not allowed: " + op.Name)
|
|
}
|
|
}
|
|
```
|