From a7651489415c48e5330fa68a6968d35f7e98814b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:01:47 +0000 Subject: [PATCH 1/6] Initial plan From d22ccaf910b2c49222a6c0f8ea2c421eb06f40f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:08:27 +0000 Subject: [PATCH 2/6] Update filter documentation with comprehensive examples and migration guide Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../docs/references/data/FilterCondition.mdx | 425 +++++++++++++++++- .../docs/references/data/NormalizedFilter.mdx | 338 +++++++++++++- content/docs/references/data/QueryFilter.mdx | 246 +++++++++- .../docs/references/data/logic/FilterNode.mdx | 330 +++++++++++++- .../references/data/logic/FilterOperator.mdx | 420 ++++++++++++++++- 5 files changed, 1733 insertions(+), 26 deletions(-) diff --git a/content/docs/references/data/FilterCondition.mdx b/content/docs/references/data/FilterCondition.mdx index 622d19051..c206cf996 100644 --- a/content/docs/references/data/FilterCondition.mdx +++ b/content/docs/references/data/FilterCondition.mdx @@ -1,5 +1,428 @@ --- title: FilterCondition -description: FilterCondition Schema Reference +description: Recursive filter structure supporting implicit equality, explicit operators, and logical combinations --- +# FilterCondition + +FilterCondition is the recursive data structure that represents filter criteria in ObjectStack. It supports three syntactic patterns: implicit equality, explicit operators, and logical combinations. + +## Overview + +FilterCondition is the core building block for all filter operations. It provides a flexible, MongoDB-inspired syntax that can express simple to complex query logic. + +## Type Definition + +```typescript +type FilterCondition = { + [key: string]: + | any // Implicit equality + | FieldOperators // Explicit operators + | FilterCondition; // Nested relation +} & { + $and?: FilterCondition[]; // Logical AND + $or?: FilterCondition[]; // Logical OR + $not?: FilterCondition; // Logical NOT +}; +``` + +## Syntax Patterns + +### 1. Implicit Equality (Simplest) + +The most common pattern - just assign a value to a field: + +```typescript +{ + status: "active", + verified: true, + age: 18 +} +``` + +**Equivalent to:** +```typescript +{ + status: { $eq: "active" }, + verified: { $eq: true }, + age: { $eq: 18 } +} +``` + +### 2. Explicit Operators + +Use operator objects when you need more than equality: + +```typescript +{ + age: { $gte: 18, $lte: 65 }, + email: { $contains: "@company.com" }, + role: { $in: ["admin", "editor"] }, + deletedAt: { $null: true } +} +``` + +### 3. Logical Combinations + +Combine multiple conditions with `$and`, `$or`, `$not`: + +```typescript +{ + $and: [ + { status: "active" }, + { age: { $gte: 18 } } + ], + $or: [ + { role: "admin" }, + { permissions: { $contains: "write" } } + ], + $not: { + status: "deleted" + } +} +``` + +### 4. Nested Relations + +Filter on related objects using dot notation or nested structures: + +```typescript +{ + department: { + name: "Engineering", + company: { + country: "USA" + } + } +} +``` + +## Available Operators + +### Equality Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$eq` | Equal to | `{ status: { $eq: "active" } }` | +| `$ne` | Not equal to | `{ status: { $ne: "deleted" } }` | + +### Comparison Operators + +| Operator | Types | Description | Example | +| :--- | :--- | :--- | :--- | +| `$gt` | number, Date | Greater than | `{ age: { $gt: 18 } }` | +| `$gte` | number, Date | Greater than or equal | `{ age: { $gte: 18 } }` | +| `$lt` | number, Date | Less than | `{ score: { $lt: 100 } }` | +| `$lte` | number, Date | Less than or equal | `{ age: { $lte: 65 } }` | + +### Set Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$in` | Value in array | `{ role: { $in: ["admin", "editor"] } }` | +| `$nin` | Value not in array | `{ status: { $nin: ["spam", "deleted"] } }` | +| `$between` | Between range (inclusive) | `{ age: { $between: [18, 65] } }` | + +### String Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$contains` | Contains substring | `{ email: { $contains: "@example.com" } }` | +| `$startsWith` | Starts with prefix | `{ username: { $startsWith: "admin_" } }` | +| `$endsWith` | Ends with suffix | `{ filename: { $endsWith: ".pdf" } }` | + +### Special Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$null` | Is null (true) or not null (false) | `{ deletedAt: { $null: true } }` | +| `$exist` | Field exists | `{ metadata: { $exist: true } }` | + +### Logical Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$and` | All conditions must be true | `{ $and: [{ a: 1 }, { b: 2 }] }` | +| `$or` | At least one condition must be true | `{ $or: [{ a: 1 }, { b: 2 }] }` | +| `$not` | Negates the condition | `{ $not: { status: "deleted" } }` | + +## Usage Examples + +### Basic Filtering + +```typescript +// Simple equality +const filter: FilterCondition = { + status: "active", + verified: true +}; + +// With operators +const filter: FilterCondition = { + age: { $gte: 18 }, + score: { $lt: 100 }, + email: { $contains: "@example.com" } +}; + +// Multiple operators on same field +const filter: FilterCondition = { + age: { $gte: 18, $lte: 65 } +}; +``` + +### Logical Combinations + +```typescript +// Implicit AND (fields at same level) +const filter: FilterCondition = { + status: "active", + age: { $gte: 18 }, + verified: true +}; + +// Explicit OR +const filter: FilterCondition = { + $or: [ + { role: "admin" }, + { role: "editor" }, + { role: "moderator" } + ] +}; + +// Complex nested logic +const filter: FilterCondition = { + status: "active", + $and: [ + { + $or: [ + { priority: "high" }, + { urgent: true } + ] + }, + { assignee: { $null: false } } + ] +}; +``` + +### Nested Relations + +```typescript +// Single level nesting +const filter: FilterCondition = { + status: "active", + owner: { + department: "Engineering" + } +}; + +// Multi-level nesting +const filter: FilterCondition = { + status: "active", + owner: { + department: { + company: { + country: "USA" + } + } + } +}; + +// Nested with operators +const filter: FilterCondition = { + task: { + status: { $in: ["todo", "in_progress"] }, + assignee: { + role: "developer", + team: { + name: { $startsWith: "Backend" } + } + } + } +}; +``` + +### Real-World Examples + +#### User Search Query +```typescript +const filter: FilterCondition = { + $or: [ + { name: { $contains: "john" } }, + { email: { $contains: "john" } }, + { username: { $contains: "john" } } + ], + status: "active", + role: { $in: ["user", "admin"] }, + createdAt: { $gte: new Date("2024-01-01") } +}; +``` + +#### Task Management Filter +```typescript +const filter: FilterCondition = { + $and: [ + { + $or: [ + { priority: "high" }, + { dueDate: { $lt: new Date() } } + ] + }, + { status: { $ne: "completed" } }, + { assignee: { $null: false } } + ], + project: { + status: "active", + team: { + department: "Engineering" + } + } +}; +``` + +#### E-commerce Product Filter +```typescript +const filter: FilterCondition = { + category: { $in: ["electronics", "computers"] }, + price: { $between: [100, 1000] }, + inStock: true, + $or: [ + { rating: { $gte: 4.5 } }, + { reviews: { $gt: 100 } } + ], + brand: { + verified: true, + country: { $in: ["USA", "Germany", "Japan"] } + } +}; +``` + +#### Content Management Filter +```typescript +const filter: FilterCondition = { + $and: [ + { published: true }, + { deletedAt: { $null: true } } + ], + $or: [ + { title: { $contains: "tutorial" } }, + { content: { $contains: "guide" } }, + { tags: { name: { $in: ["tutorial", "guide", "howto"] } } } + ], + author: { + verified: true, + role: { $in: ["editor", "admin"] }, + posts: { count: { $gt: 10 } } + }, + publishedAt: { + $gte: new Date("2024-01-01") + } +}; +``` + +## TypeScript Type Safety + +Use the generic `Filter` type for type-safe filters: + +```typescript +interface User { + id: number; + name: string; + age: number; + email: string; + verified: boolean; + profile: { + bio: string; + avatar?: string; + }; +} + +// Type-safe filter +const filter: Filter = { + age: { $gte: 18 }, // ✓ Valid + email: { $contains: "@" }, // ✓ Valid + verified: true, // ✓ Valid + profile: { + bio: { $contains: "developer" } // ✓ Valid + }, + // status: "active" // ✗ Error: 'status' doesn't exist on User +}; +``` + +## Combining with Other Query Features + +FilterCondition is typically used as the `where` clause in a `Query`: + +```typescript +const query: Query = { + object: "user", + fields: ["name", "email", "age"], + where: { // FilterCondition here + status: "active", + age: { $gte: 18 }, + $or: [ + { role: "admin" }, + { verified: true } + ] + }, + sort: [{ field: "createdAt", order: "desc" }], + top: 10 +}; +``` + +## Design Principles + +1. **Declarative**: Describe what you want, not how to get it +2. **Composable**: Build complex filters from simple conditions +3. **Type-safe**: Full TypeScript support with inference +4. **Database-agnostic**: Works with SQL, NoSQL, and other backends +5. **Human-readable**: Easy to write and understand + +## Best Practices + +### Do ✓ + +```typescript +// Use implicit equality for simple cases +{ status: "active", verified: true } + +// Combine operators on same field when needed +{ age: { $gte: 18, $lte: 65 } } + +// Use $or at appropriate level +{ + status: "active", + $or: [{ role: "admin" }, { role: "editor" }] +} +``` + +### Don't ✗ + +```typescript +// Don't use $eq unnecessarily +{ status: { $eq: "active" } } // Just use: { status: "active" } + +// Don't create redundant $and +{ + $and: [{ status: "active" }] // Just use: { status: "active" } +} + +// Don't mix patterns unnecessarily +{ + status: { $eq: "active" }, // Inconsistent + verified: true +} +``` + +## Performance Considerations + +1. **Index Fields**: Ensure frequently filtered fields have database indexes +2. **Avoid Complex Nesting**: Keep filter depth reasonable for query optimization +3. **Use Specific Operators**: `$eq` is faster than `$contains` for exact matches +4. **Limit OR Branches**: Too many `$or` branches can impact performance + +## See Also + +- [QueryFilter](./QueryFilter.mdx) - Top-level query filter wrapper +- [NormalizedFilter](./NormalizedFilter.mdx) - Internal AST representation +- [Query](./types/Query.mdx) - Complete query specification +- [Field Operators](./logic/FilterOperator.mdx) - Detailed operator reference \ No newline at end of file diff --git a/content/docs/references/data/NormalizedFilter.mdx b/content/docs/references/data/NormalizedFilter.mdx index babf352d3..20462a1f1 100644 --- a/content/docs/references/data/NormalizedFilter.mdx +++ b/content/docs/references/data/NormalizedFilter.mdx @@ -1,12 +1,342 @@ --- title: NormalizedFilter -description: NormalizedFilter Schema Reference +description: Internal AST representation of filter conditions after normalization --- +# NormalizedFilter + +NormalizedFilter is the internal Abstract Syntax Tree (AST) representation of filter conditions after converting all syntactic sugar to explicit operators. This simplified structure makes it easier for driver implementations to process filters consistently. + +## Overview + +During the normalization pass, implicit syntax is converted to explicit operator-based conditions: + +**Input (User-friendly):** +```typescript +{ age: 18, role: "admin" } +``` + +**Output (Normalized):** +```typescript +{ + $and: [ + { age: { $eq: 18 } }, + { role: { $eq: "admin" } } + ] +} +``` + +## Schema + +```typescript +{ + $and?: Array, + $or?: Array, + $not?: FieldCondition | NormalizedFilter +} + +type FieldCondition = Record +``` + ## Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **$and** | `Record \| any[]` | optional | | -| **$or** | `Record \| any[]` | optional | | -| **$not** | `Record \| any` | optional | | +| **$and** | `Array` | optional | All conditions must be true (logical AND) | +| **$or** | `Array` | optional | At least one condition must be true (logical OR) | +| **$not** | `FieldCondition \| NormalizedFilter` | optional | Negates the condition (logical NOT) | + +## Normalization Process + +### Stage 1: Implicit Equality → Explicit $eq + +**Before:** +```typescript +{ status: "active", verified: true } +``` + +**After:** +```typescript +{ + $and: [ + { status: { $eq: "active" } }, + { verified: { $eq: true } } + ] +} +``` + +### Stage 2: Flatten Top-level AND + +**Before:** +```typescript +{ + status: "active", + age: { $gte: 18 }, + role: "admin" +} +``` + +**After:** +```typescript +{ + $and: [ + { status: { $eq: "active" } }, + { age: { $gte: 18 } }, + { role: { $eq: "admin" } } + ] +} +``` + +### Stage 3: Preserve Logical Operators + +**Before:** +```typescript +{ + status: "active", + $or: [ + { role: "admin" }, + { permissions: { $contains: "write" } } + ] +} +``` + +**After:** +```typescript +{ + $and: [ + { status: { $eq: "active" } }, + { + $or: [ + { role: { $eq: "admin" } }, + { permissions: { $contains: "write" } } + ] + } + ] +} +``` + +## Examples + +### Simple AND Condition + +**Input:** +```typescript +const filter: QueryFilter = { + where: { + status: "active", + age: { $gte: 18 } + } +}; +``` + +**Normalized:** +```typescript +{ + $and: [ + { status: { $eq: "active" } }, + { age: { $gte: 18 } } + ] +} +``` + +### OR with Nested AND + +**Input:** +```typescript +const filter: QueryFilter = { + where: { + $or: [ + { role: "admin" }, + { + $and: [ + { verified: true }, + { score: { $gt: 80 } } + ] + } + ] + } +}; +``` + +**Normalized:** +```typescript +{ + $or: [ + { role: { $eq: "admin" } }, + { + $and: [ + { verified: { $eq: true } }, + { score: { $gt: 80 } } + ] + } + ] +} +``` + +### NOT Condition + +**Input:** +```typescript +const filter: QueryFilter = { + where: { + $not: { + status: "deleted", + archived: true + } + } +}; +``` + +**Normalized:** +```typescript +{ + $not: { + $and: [ + { status: { $eq: "deleted" } }, + { archived: { $eq: true } } + ] + } +} +``` + +### Complex Nested Structure + +**Input:** +```typescript +const filter: QueryFilter = { + where: { + status: "active", + $and: [ + { age: { $gte: 18 } }, + { + $or: [ + { role: "admin" }, + { permissions: { $contains: "edit" } } + ] + } + ], + department: { + name: "Engineering" + } + } +}; +``` + +**Normalized:** +```typescript +{ + $and: [ + { status: { $eq: "active" } }, + { age: { $gte: 18 } }, + { + $or: [ + { role: { $eq: "admin" } }, + { permissions: { $contains: "edit" } } + ] + }, + { "department.name": { $eq: "Engineering" } } // Flattened path + ] +} +``` + +## Benefits for Driver Implementation + +### 1. Consistent Structure +Every filter is guaranteed to have explicit operators, eliminating ambiguity. + +### 2. Simplified Traversal +Drivers can use a simple recursive pattern: +```typescript +function processFilter(filter: NormalizedFilter) { + if (filter.$and) return processAnd(filter.$and); + if (filter.$or) return processOr(filter.$or); + if (filter.$not) return processNot(filter.$not); + // Process field conditions +} +``` + +### 3. SQL Generation Example +```typescript +function toSQL(filter: NormalizedFilter): string { + if (filter.$and) { + return filter.$and.map(toSQL).join(' AND '); + } + if (filter.$or) { + return filter.$or.map(toSQL).join(' OR '); + } + if (filter.$not) { + return `NOT (${toSQL(filter.$not)})`; + } + // Handle field conditions +} +``` + +### 4. MongoDB Query Example +```typescript +function toMongo(filter: NormalizedFilter) { + if (filter.$and) { + return { $and: filter.$and.map(toMongo) }; + } + if (filter.$or) { + return { $or: filter.$or.map(toMongo) }; + } + if (filter.$not) { + return { $nor: [toMongo(filter.$not)] }; + } + // Handle field conditions +} +``` + +## Field Operators in Normalized Filters + +Each field condition uses explicit operators: + +```typescript +{ + $eq?: any, // Equal to + $ne?: any, // Not equal to + $gt?: number | Date, // Greater than + $gte?: number | Date,// Greater than or equal + $lt?: number | Date, // Less than + $lte?: number | Date,// Less than or equal + $in?: any[], // In array + $nin?: any[], // Not in array + $between?: [number | Date, number | Date], // Between range + $contains?: string, // Contains substring + $startsWith?: string,// Starts with prefix + $endsWith?: string, // Ends with suffix + $null?: boolean, // Is/isn't null + $exist?: boolean // Field exists +} +``` + +## Usage in Driver Implementation + +Drivers should normalize user input before processing: + +```typescript +import { normalizeFilter } from '@objectstack/spec'; + +class MyDriver implements Driver { + async find(object: string, query: Query) { + // Normalize filter + const normalized = normalizeFilter(query.where); + + // Convert to database-specific query + const dbQuery = this.toNativeQuery(normalized); + + // Execute + return this.execute(dbQuery); + } + + private toNativeQuery(filter: NormalizedFilter) { + // Driver-specific implementation + } +} +``` + +## See Also + +- [QueryFilter](./QueryFilter.mdx) - User-friendly filter syntax +- [FilterCondition](./FilterCondition.mdx) - Recursive filter structure +- [Driver Implementation Guide](/docs/guides/custom-driver.mdx) - Building custom drivers diff --git a/content/docs/references/data/QueryFilter.mdx b/content/docs/references/data/QueryFilter.mdx index ca1b37363..dc1ac28e3 100644 --- a/content/docs/references/data/QueryFilter.mdx +++ b/content/docs/references/data/QueryFilter.mdx @@ -1,10 +1,252 @@ --- title: QueryFilter -description: QueryFilter Schema Reference +description: MongoDB-style query filter for data retrieval operations --- +# QueryFilter + +QueryFilter provides a MongoDB-style, database-agnostic query DSL for filtering data. It supports implicit equality, explicit operators, logical combinations, and nested relation queries. + +## Overview + +QueryFilter is the top-level wrapper for filter conditions, typically used as the `where` clause in queries. + +## Schema + +```typescript +{ + where?: FilterCondition // Optional filter criteria +} +``` + ## Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **where** | `any` | optional | | +| **where** | `FilterCondition` | optional | Filter criteria for the query | + +## Filter Operators + +### Equality Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$eq` | Equal to | `{ status: { $eq: "active" } }` | +| `$ne` | Not equal to | `{ status: { $ne: "deleted" } }` | + +### Comparison Operators (Numeric/Date) + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$gt` | Greater than | `{ age: { $gt: 18 } }` | +| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | +| `$lt` | Less than | `{ score: { $lt: 100 } }` | +| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | + +### Set & Range Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$in` | Value in array | `{ role: { $in: ["admin", "editor"] } }` | +| `$nin` | Value not in array | `{ status: { $nin: ["spam", "deleted"] } }` | +| `$between` | Between range (inclusive) | `{ age: { $between: [18, 65] } }` | + +### String Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$contains` | Contains substring | `{ email: { $contains: "@example.com" } }` | +| `$startsWith` | Starts with prefix | `{ username: { $startsWith: "admin_" } }` | +| `$endsWith` | Ends with suffix | `{ filename: { $endsWith: ".pdf" } }` | + +### Special Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$null` | Is null (true) or not null (false) | `{ deletedAt: { $null: true } }` | +| `$exist` | Field exists (true) or doesn't exist (false) | `{ metadata: { $exist: true } }` | + +### Logical Operators + +| Operator | Description | Example | +| :--- | :--- | :--- | +| `$and` | All conditions must be true | `{ $and: [{ status: "active" }, { age: { $gte: 18 } }] }` | +| `$or` | At least one condition must be true | `{ $or: [{ role: "admin" }, { role: "editor" }] }` | +| `$not` | Negates the condition | `{ $not: { status: "deleted" } }` | + +## Usage Examples + +### Basic Filtering + +#### Implicit Equality +```typescript +const filter: QueryFilter = { + where: { + status: "active", // Implicit equality + verified: true + } +}; +``` + +#### Explicit Operators +```typescript +const filter: QueryFilter = { + where: { + age: { $gte: 18 }, // Comparison + email: { $contains: "@company.com" }, // String match + role: { $in: ["admin", "editor"] } // Set membership + } +}; +``` + +### Logical Combinations + +#### AND Conditions (Implicit) +```typescript +const filter: QueryFilter = { + where: { + status: "active", // All fields at same level are AND'ed + age: { $gte: 18 }, + verified: true + } +}; +``` + +#### OR Conditions +```typescript +const filter: QueryFilter = { + where: { + $or: [ + { role: "admin" }, + { permissions: { $contains: "write" } } + ] + } +}; +``` + +#### Complex Nested Logic +```typescript +const filter: QueryFilter = { + where: { + status: "active", + $or: [ + { priority: "high" }, + { + $and: [ + { dueDate: { $lt: new Date() } }, + { assignee: { $null: false } } + ] + } + ] + } +}; +``` + +### Nested Relations + +```typescript +const filter: QueryFilter = { + where: { + status: "active", + department: { // Nested relation + name: "Engineering", + company: { // Deeply nested + country: "USA" + } + } + } +}; +``` + +### Real-World Examples + +#### User Search +```typescript +const userSearch: QueryFilter = { + where: { + $or: [ + { name: { $contains: "john" } }, + { email: { $contains: "john" } }, + { username: { $contains: "john" } } + ], + status: "active", + role: { $in: ["user", "admin"] } + } +}; +``` + +#### E-commerce Order Filter +```typescript +const orderFilter: QueryFilter = { + where: { + status: { $in: ["pending", "processing", "shipped"] }, + totalAmount: { $gte: 100 }, + createdAt: { + $between: [ + new Date("2024-01-01"), + new Date("2024-12-31") + ] + }, + customer: { + tier: "premium", + country: { $in: ["US", "CA", "UK"] } + } + } +}; +``` + +#### Content Management +```typescript +const contentFilter: QueryFilter = { + where: { + $and: [ + { published: true }, + { deletedAt: { $null: true } } + ], + $or: [ + { title: { $contains: "tutorial" } }, + { tags: { name: { $in: ["tutorial", "guide", "howto"] } } } + ], + author: { + verified: true, + role: { $in: ["editor", "admin"] } + } + } +}; +``` + +## TypeScript Type Safety + +```typescript +interface User { + id: number; + name: string; + age: number; + email: string; + profile: { + verified: boolean; + }; +} + +// Type-safe filter +const filter: Filter = { + age: { $gte: 18 }, + email: { $contains: "@example.com" }, + profile: { + verified: true + } +}; +``` + +## Design Principles + +1. **Declarative**: Frontend describes "what data to get", not "how to query" +2. **Database Agnostic**: Syntax contains no database-specific directives +3. **Type Safe**: Structure can be statically inferred by TypeScript +4. **Convention over Configuration**: Implicit syntax for common queries + +## See Also + +- [FilterCondition](./FilterCondition.mdx) - Recursive filter structure details +- [NormalizedFilter](./NormalizedFilter.mdx) - Internal AST representation +- [Query](./types/Query.mdx) - Complete query specification diff --git a/content/docs/references/data/logic/FilterNode.mdx b/content/docs/references/data/logic/FilterNode.mdx index d554091bc..3b0cf8519 100644 --- a/content/docs/references/data/logic/FilterNode.mdx +++ b/content/docs/references/data/logic/FilterNode.mdx @@ -1,5 +1,333 @@ --- title: FilterNode -description: FilterNode Schema Reference +description: Array-based filter syntax for query conditions (Legacy) --- +# FilterNode + +FilterNode represents the legacy array-based filter syntax used in ObjectStack queries. This syntax is being deprecated in favor of the MongoDB-style `FilterCondition` syntax. + +## Overview + +FilterNode uses a simple array structure to represent filter conditions: +- **Leaf Node**: `[field, operator, value]` for simple conditions +- **Logic Node**: `[expression, "and"|"or", expression]` for logical combinations + +## Type Definition + +```typescript +type FilterNode = + | [string, FilterOperator, any] // Leaf: Simple condition + | Array; // Logic: Logical combination +``` + +## Syntax Patterns + +### 1. Simple Condition (Leaf Node) + +```typescript +[field, operator, value] +``` + +**Examples:** +```typescript +['status', '=', 'active'] +['age', '>=', 18] +['email', 'contains', '@example.com'] +['role', 'in', ['admin', 'editor']] +['deletedAt', 'is_null', true] +``` + +### 2. Logical Combinations + +```typescript +[condition1, 'and'|'or', condition2] +``` + +**Examples:** +```typescript +// AND +[ + ['status', '=', 'active'], + 'and', + ['age', '>=', 18] +] + +// OR +[ + ['role', '=', 'admin'], + 'or', + ['role', '=', 'editor'] +] + +// Nested logic +[ + ['status', '=', 'active'], + 'and', + [ + ['role', '=', 'admin'], + 'or', + ['verified', '=', true] + ] +] +``` + +## Available Operators + +See [FilterOperator](./FilterOperator.mdx) for complete operator documentation. + +**Common operators:** +- Equality: `=`, `!=`, `<>` +- Comparison: `>`, `>=`, `<`, `<=` +- String: `contains`, `startswith`, `notcontains` +- Set: `in`, `notin`, `between` +- Special: `is_null`, `is_not_null` + +## Usage Examples + +### Basic Filtering + +```typescript +// Equality +const filter: FilterNode = ['status', '=', 'active']; + +// Comparison +const filter: FilterNode = ['age', '>=', 18]; + +// String matching +const filter: FilterNode = ['email', 'contains', '@company.com']; + +// Set membership +const filter: FilterNode = ['role', 'in', ['admin', 'editor', 'viewer']]; +``` + +### Logical AND + +```typescript +const filter: FilterNode = [ + ['status', '=', 'active'], + 'and', + ['verified', '=', true] +]; +``` + +### Logical OR + +```typescript +const filter: FilterNode = [ + ['role', '=', 'admin'], + 'or', + ['permissions', 'contains', 'write'] +]; +``` + +### Complex Nested Logic + +```typescript +const filter: FilterNode = [ + ['status', '=', 'active'], + 'and', + [ + [ + ['priority', '=', 'high'], + 'or', + ['urgent', '=', true] + ], + 'and', + ['assignee', 'is_not_null', true] + ] +]; +``` + +### Real-World Examples + +#### Task Filter +```typescript +const taskFilter: FilterNode = [ + [ + ['status', '!=', 'completed'], + 'and', + ['assignee', 'is_not_null', true] + ], + 'and', + [ + ['priority', '=', 'high'], + 'or', + ['dueDate', '<', new Date()] + ] +]; +``` + +#### User Search +```typescript +const userSearch: FilterNode = [ + [ + ['name', 'contains', 'john'], + 'or', + ['email', 'contains', 'john'] + ], + 'and', + ['status', '=', 'active'] +]; +``` + +## Usage in Query AST + +FilterNode is used in the `filters` field of the Query AST: + +```typescript +const query: Query = { + object: 'user', + fields: ['name', 'email', 'age'], + filters: [ + ['status', '=', 'active'], + 'and', + ['age', '>=', 18] + ], + sort: [{ field: 'createdAt', order: 'desc' }], + top: 10 +}; +``` + +## Migration to Modern Syntax + +⚠️ **Deprecation Notice**: This array-based syntax is being replaced by the MongoDB-style `FilterCondition` syntax, which is more expressive and type-safe. + +### Simple Condition + +**Before (FilterNode):** +```typescript +['status', '=', 'active'] +``` + +**After (FilterCondition):** +```typescript +{ status: 'active' } +// or explicit +{ status: { $eq: 'active' } } +``` + +### Comparison + +**Before (FilterNode):** +```typescript +['age', '>=', 18] +``` + +**After (FilterCondition):** +```typescript +{ age: { $gte: 18 } } +``` + +### String Matching + +**Before (FilterNode):** +```typescript +['email', 'contains', '@example.com'] +['username', 'startswith', 'admin_'] +``` + +**After (FilterCondition):** +```typescript +{ email: { $contains: '@example.com' } } +{ username: { $startsWith: 'admin_' } } +``` + +### Set Operations + +**Before (FilterNode):** +```typescript +['role', 'in', ['admin', 'editor']] +``` + +**After (FilterCondition):** +```typescript +{ role: { $in: ['admin', 'editor'] } } +``` + +### Logical AND + +**Before (FilterNode):** +```typescript +[ + ['status', '=', 'active'], + 'and', + ['age', '>=', 18] +] +``` + +**After (FilterCondition):** +```typescript +{ + status: 'active', + age: { $gte: 18 } +} +``` + +### Logical OR + +**Before (FilterNode):** +```typescript +[ + ['role', '=', 'admin'], + 'or', + ['role', '=', 'editor'] +] +``` + +**After (FilterCondition):** +```typescript +{ + $or: [ + { role: 'admin' }, + { role: 'editor' } + ] +} +``` + +### Complex Nested Logic + +**Before (FilterNode):** +```typescript +[ + ['status', '=', 'active'], + 'and', + [ + ['priority', '=', 'high'], + 'or', + ['urgent', '=', true] + ] +] +``` + +**After (FilterCondition):** +```typescript +{ + status: 'active', + $or: [ + { priority: 'high' }, + { urgent: true } + ] +} +``` + +## Limitations + +1. **Less Type-Safe**: Array structure is harder for TypeScript to validate +2. **Less Readable**: Nested arrays become hard to read +3. **Limited Operators**: Not all operators are available +4. **No Nested Relations**: Cannot easily query nested object properties + +## Benefits of Modern Syntax + +1. **Type Safety**: Full TypeScript support with inference +2. **Readability**: Object syntax is more natural and clear +3. **Flexibility**: Supports nested relations and complex queries +4. **Industry Standard**: MongoDB-style syntax is widely recognized + +## See Also + +- [FilterCondition](../FilterCondition.mdx) - Modern MongoDB-style filter syntax (Recommended) +- [QueryFilter](../QueryFilter.mdx) - Top-level query filter wrapper +- [FilterOperator](./FilterOperator.mdx) - Operator reference and migration guide +- [Query](../types/Query.mdx) - Complete query specification \ No newline at end of file diff --git a/content/docs/references/data/logic/FilterOperator.mdx b/content/docs/references/data/logic/FilterOperator.mdx index ca63cccc7..8b482f821 100644 --- a/content/docs/references/data/logic/FilterOperator.mdx +++ b/content/docs/references/data/logic/FilterOperator.mdx @@ -1,22 +1,406 @@ --- title: FilterOperator -description: FilterOperator Schema Reference +description: Supported filter operators for query conditions --- -## Allowed Values - -* `=` -* `!=` -* `<>` -* `>` -* `>=` -* `<` -* `<=` -* `startswith` -* `contains` -* `notcontains` -* `between` -* `in` -* `notin` -* `is_null` -* `is_not_null` \ No newline at end of file +# FilterOperator + +Filter operators define the comparison and logical operations available for filtering data in ObjectStack queries. + +## Overview + +ObjectStack supports two filter operator systems: + +1. **MongoDB-style operators** (Recommended): Modern `$` prefix operators (`$eq`, `$gte`, `$contains`) +2. **Array-based operators** (Legacy): String operators used in array-based filter syntax + +## MongoDB-Style Operators (Recommended) + +These operators are used with the `FilterCondition` and `QueryFilter` types. + +### Equality Operators + +| Operator | SQL Equivalent | MongoDB Equivalent | Description | +| :--- | :--- | :--- | :--- | +| `$eq` | `=` | `$eq` | Equal to | +| `$ne` | `!=` or `<>` | `$ne` | Not equal to | + +**Examples:** +```typescript +{ status: { $eq: "active" } } +{ status: { $ne: "deleted" } } +``` + +### Comparison Operators + +| Operator | SQL Equivalent | MongoDB Equivalent | Data Types | Description | +| :--- | :--- | :--- | :--- | :--- | +| `$gt` | `>` | `$gt` | number, Date | Greater than | +| `$gte` | `>=` | `$gte` | number, Date | Greater than or equal | +| `$lt` | `<` | `$lt` | number, Date | Less than | +| `$lte` | `<=` | `$lte` | number, Date | Less than or equal | + +**Examples:** +```typescript +{ age: { $gt: 18 } } +{ age: { $gte: 18 } } +{ score: { $lt: 100 } } +{ age: { $lte: 65 } } +{ createdAt: { $gte: new Date("2024-01-01") } } +``` + +### Set Operators + +| Operator | SQL Equivalent | MongoDB Equivalent | Description | +| :--- | :--- | :--- | :--- | +| `$in` | `IN (?, ?, ?)` | `$in` | Value in array | +| `$nin` | `NOT IN (?, ?, ?)` | `$nin` | Value not in array | +| `$between` | `BETWEEN ? AND ?` | `$gte` + `$lte` | Between range (inclusive) | + +**Examples:** +```typescript +{ role: { $in: ["admin", "editor", "moderator"] } } +{ status: { $nin: ["spam", "deleted", "archived"] } } +{ age: { $between: [18, 65] } } +{ price: { $between: [100, 1000] } } +``` + +### String Operators + +| Operator | SQL Equivalent | MongoDB Equivalent | Description | +| :--- | :--- | :--- | :--- | +| `$contains` | `LIKE '%?%'` | `$regex` | Contains substring (case-sensitive) | +| `$startsWith` | `LIKE '?%'` | `$regex` | Starts with prefix | +| `$endsWith` | `LIKE '%?'` | `$regex` | Ends with suffix | + +**Examples:** +```typescript +{ email: { $contains: "@example.com" } } +{ username: { $startsWith: "admin_" } } +{ filename: { $endsWith: ".pdf" } } +{ title: { $contains: "tutorial" } } +``` + +**Note:** Case sensitivity is handled at the backend/driver level. + +### Special Operators + +| Operator | SQL Equivalent | MongoDB Equivalent | Description | +| :--- | :--- | :--- | :--- | +| `$null` | `IS NULL` / `IS NOT NULL` | `field: null` | Is null (true) or not null (false) | +| `$exist` | N/A | `$exists` | Field exists (primarily for NoSQL) | + +**Examples:** +```typescript +{ deletedAt: { $null: true } } // IS NULL +{ deletedAt: { $null: false } } // IS NOT NULL +{ metadata: { $exist: true } } // Field exists +{ optionalField: { $exist: false } } // Field doesn't exist +``` + +### Logical Operators + +| Operator | SQL Equivalent | MongoDB Equivalent | Description | +| :--- | :--- | :--- | :--- | +| `$and` | `AND` | `$and` | All conditions must be true | +| `$or` | `OR` | `$or` | At least one condition must be true | +| `$not` | `NOT` | `$not` | Negates the condition | + +**Examples:** +```typescript +// AND (implicit at same level) +{ status: "active", verified: true } + +// AND (explicit) +{ $and: [{ status: "active" }, { age: { $gte: 18 } }] } + +// OR +{ $or: [{ role: "admin" }, { role: "editor" }] } + +// NOT +{ $not: { status: "deleted" } } + +// Complex combination +{ + status: "active", + $and: [ + { + $or: [ + { priority: "high" }, + { urgent: true } + ] + }, + { assignee: { $null: false } } + ] +} +``` + +## Array-Based Operators (Legacy) + +These operators are used in the older array-based filter syntax `[field, operator, value]`. + +### Allowed Values + +* `=` - Equal to +* `!=` - Not equal to +* `<>` - Not equal to (alternative) +* `>` - Greater than +* `>=` - Greater than or equal +* `<` - Less than +* `<=` - Less than or equal +* `startswith` - Starts with prefix +* `contains` - Contains substring +* `notcontains` - Does not contain substring +* `between` - Between range +* `in` - In array +* `notin` - Not in array +* `is_null` - Is null +* `is_not_null` - Is not null + +### Examples (Legacy Syntax) + +```typescript +// Simple filter +['status', '=', 'active'] + +// Comparison +['age', '>=', 18] + +// String matching +['email', 'contains', '@example.com'] +['username', 'startswith', 'admin_'] + +// Set membership +['role', 'in', ['admin', 'editor']] + +// Null check +['deletedAt', 'is_null', true] + +// Logical combinations +[ + ['status', '=', 'active'], + 'and', + ['age', '>=', 18] +] +``` + +## Migration Guide: Legacy → Modern + +### Simple Equality + +**Before (Legacy):** +```typescript +['status', '=', 'active'] +``` + +**After (Modern):** +```typescript +{ status: "active" } +// or explicit +{ status: { $eq: "active" } } +``` + +### Comparison + +**Before (Legacy):** +```typescript +['age', '>=', 18] +['score', '<', 100] +``` + +**After (Modern):** +```typescript +{ age: { $gte: 18 } } +{ score: { $lt: 100 } } +``` + +### String Operators + +**Before (Legacy):** +```typescript +['email', 'contains', '@example.com'] +['username', 'startswith', 'admin_'] +``` + +**After (Modern):** +```typescript +{ email: { $contains: "@example.com" } } +{ username: { $startsWith: "admin_" } } +``` + +### Set Operators + +**Before (Legacy):** +```typescript +['role', 'in', ['admin', 'editor']] +['status', 'notin', ['deleted', 'spam']] +``` + +**After (Modern):** +```typescript +{ role: { $in: ["admin", "editor"] } } +{ status: { $nin: ["deleted", "spam"] } } +``` + +### Null Checks + +**Before (Legacy):** +```typescript +['deletedAt', 'is_null', true] +['deletedAt', 'is_not_null', true] +``` + +**After (Modern):** +```typescript +{ deletedAt: { $null: true } } +{ deletedAt: { $null: false } } +``` + +### Logical AND + +**Before (Legacy):** +```typescript +[ + ['status', '=', 'active'], + 'and', + ['age', '>=', 18] +] +``` + +**After (Modern):** +```typescript +// Implicit AND (recommended) +{ + status: "active", + age: { $gte: 18 } +} + +// Explicit AND +{ + $and: [ + { status: "active" }, + { age: { $gte: 18 } } + ] +} +``` + +### Logical OR + +**Before (Legacy):** +```typescript +[ + ['role', '=', 'admin'], + 'or', + ['role', '=', 'editor'] +] +``` + +**After (Modern):** +```typescript +{ + $or: [ + { role: "admin" }, + { role: "editor" } + ] +} + +// Or more concisely +{ role: { $in: ["admin", "editor"] } } +``` + +## Operator Precedence + +When multiple operators are used, they follow this precedence (highest to lowest): + +1. Field operators (`$eq`, `$gt`, `$contains`, etc.) +2. `$not` +3. `$and` +4. `$or` + +**Example:** +```typescript +{ + status: "active", // 1. Field operator (implicit $eq) + $not: { archived: true }, // 2. NOT + $and: [ // 3. AND + { verified: true } + ], + $or: [ // 4. OR (evaluated last) + { role: "admin" } + ] +} +``` + +## Best Practices + +### Use Implicit Equality +```typescript +// ✓ Good +{ status: "active" } + +// ✗ Unnecessary +{ status: { $eq: "active" } } +``` + +### Combine Operators on Same Field +```typescript +// ✓ Good +{ age: { $gte: 18, $lte: 65 } } + +// ✗ Verbose +{ + $and: [ + { age: { $gte: 18 } }, + { age: { $lte: 65 } } + ] +} +``` + +### Use $in for Multiple OR on Same Field +```typescript +// ✓ Good +{ role: { $in: ["admin", "editor", "moderator"] } } + +// ✗ Verbose +{ + $or: [ + { role: "admin" }, + { role: "editor" }, + { role: "moderator" } + ] +} +``` + +### Prefer $null over Explicit Comparison +```typescript +// ✓ Good +{ deletedAt: { $null: true } } + +// ✗ Less clear +{ deletedAt: null } +``` + +## Database-Specific Notes + +### SQL Databases +- `$contains`, `$startsWith`, `$endsWith` map to `LIKE` with wildcards +- `$in` maps to `IN` clause +- `$between` maps to `BETWEEN AND` +- Case sensitivity depends on column collation + +### MongoDB +- Operators map directly to MongoDB query operators +- `$regex` used for string operators +- Native support for `$exists`, `$in`, `$nin` + +### In-Memory Drivers +- All operators implemented in JavaScript +- String operators are case-sensitive by default +- Performance may vary for large datasets + +## See Also + +- [FilterCondition](../FilterCondition.mdx) - Recursive filter structure +- [QueryFilter](../QueryFilter.mdx) - Top-level query filter wrapper +- [Query](../types/Query.mdx) - Complete query specification +- [Driver Guide](/docs/guides/custom-driver.mdx) - Implementing operators in custom drivers \ No newline at end of file From 29e26552702b2b6e04ada7e3231092e21cafc380 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:12:45 +0000 Subject: [PATCH 3/6] Update dashboard schema and tests to use modern MongoDB-style filters Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- examples/todo/src/client-test.ts | 8 +++-- packages/spec/src/ui/dashboard.test.ts | 44 +++++++++++++------------- packages/spec/src/ui/dashboard.zod.ts | 5 +-- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/examples/todo/src/client-test.ts b/examples/todo/src/client-test.ts index e02cdb023..d27c01803 100644 --- a/examples/todo/src/client-test.ts +++ b/examples/todo/src/client-test.ts @@ -55,11 +55,13 @@ async function main() { console.log('✅ Deleted:', deleted); } - // 6. Advanced Query (AST) - console.log('\n🧠 Testing Advanced Query (Select & AST)...'); + // 6. Advanced Query (Modern Filter Syntax) + console.log('\n🧠 Testing Advanced Query (Select & Modern Filter)...'); const advancedResult = await client.data.find('todo_task', { select: ['subject', 'priority'], - filters: ['priority', '>=', 2], + where: { + priority: { $gte: 2 } // Modern MongoDB-style filter syntax + }, sort: ['-priority'] }); console.log(`🎉 Found ${advancedResult.count} high priority tasks:`); diff --git a/packages/spec/src/ui/dashboard.test.ts b/packages/spec/src/ui/dashboard.test.ts index 17ca2ae5b..35ff94103 100644 --- a/packages/spec/src/ui/dashboard.test.ts +++ b/packages/spec/src/ui/dashboard.test.ts @@ -97,7 +97,7 @@ describe('DashboardWidgetSchema', () => { title: 'Top Accounts', type: 'table', object: 'account', - filter: [{ field: 'annual_revenue', operator: '>', value: 1000000 }], + filter: { annual_revenue: { $gt: 1000000 } }, // Modern MongoDB-style filter layout: { x: 0, y: 6, w: 12, h: 4 }, }; @@ -109,7 +109,7 @@ describe('DashboardWidgetSchema', () => { title: 'Active Opportunities', type: 'metric', object: 'opportunity', - filter: { field: 'status', operator: 'equals', value: 'active' }, + filter: { status: 'active' }, aggregate: 'count', layout: { x: 0, y: 0, w: 3, h: 2 }, }; @@ -209,7 +209,7 @@ describe('DashboardSchema', () => { object: 'opportunity', valueField: 'amount', aggregate: 'sum', - filter: { field: 'is_closed', operator: 'equals', value: false }, + filter: { is_closed: false }, layout: { x: 0, y: 0, w: 3, h: 2 }, }, { @@ -217,7 +217,7 @@ describe('DashboardSchema', () => { type: 'metric', object: 'opportunity', aggregate: 'count', - filter: { field: 'is_closed', operator: 'equals', value: false }, + filter: { is_closed: false }, layout: { x: 3, y: 0, w: 3, h: 2 }, }, { @@ -236,7 +236,7 @@ describe('DashboardSchema', () => { object: 'opportunity', valueField: 'amount', aggregate: 'avg', - filter: { field: 'status', operator: 'equals', value: 'won' }, + filter: { status: 'won' }, layout: { x: 9, y: 0, w: 3, h: 2 }, }, { @@ -246,7 +246,7 @@ describe('DashboardSchema', () => { categoryField: 'stage', valueField: 'amount', aggregate: 'sum', - filter: { field: 'is_closed', operator: 'equals', value: false }, + filter: { is_closed: false }, layout: { x: 0, y: 2, w: 8, h: 4 }, options: { horizontal: true, @@ -268,7 +268,7 @@ describe('DashboardSchema', () => { categoryField: 'close_date', valueField: 'amount', aggregate: 'sum', - filter: { field: 'close_date', operator: 'last_n_months', value: 12 }, + filter: { close_date: '{last_12_months}' }, layout: { x: 0, y: 6, w: 12, h: 4 }, options: { smoothCurve: true, @@ -292,7 +292,7 @@ describe('DashboardSchema', () => { type: 'metric', object: 'case', aggregate: 'count', - filter: { field: 'status', operator: 'not_equals', value: 'closed' }, + filter: { status: { $ne: 'closed' } }, layout: { x: 0, y: 0, w: 3, h: 2 }, options: { color: '#FF6384', @@ -303,10 +303,10 @@ describe('DashboardSchema', () => { type: 'metric', object: 'case', aggregate: 'count', - filter: [ - { field: 'status', operator: 'equals', value: 'closed' }, - { field: 'closed_date', operator: 'today' }, - ], + filter: { // Modern MongoDB-style filter + status: 'closed', + closed_date: '{today}' + }, layout: { x: 3, y: 0, w: 3, h: 2 }, }, { @@ -326,7 +326,7 @@ describe('DashboardSchema', () => { object: 'case', valueField: 'satisfaction_rating', aggregate: 'avg', - filter: { field: 'satisfaction_rating', operator: 'not_null' }, + filter: { satisfaction_rating: { $null: false } }, layout: { x: 9, y: 0, w: 3, h: 2 }, options: { max: 5, @@ -339,7 +339,7 @@ describe('DashboardSchema', () => { object: 'case', categoryField: 'priority', aggregate: 'count', - filter: { field: 'status', operator: 'not_equals', value: 'closed' }, + filter: { status: { $ne: 'closed' } }, layout: { x: 0, y: 2, w: 6, h: 4 }, }, { @@ -354,7 +354,7 @@ describe('DashboardSchema', () => { title: 'Recent High Priority Cases', type: 'table', object: 'case', - filter: { field: 'priority', operator: 'equals', value: 'high' }, + filter: { priority: 'high' }, // Modern MongoDB-style filter layout: { x: 0, y: 6, w: 12, h: 4 }, options: { columns: ['case_number', 'subject', 'account', 'owner', 'created_date'], @@ -379,10 +379,10 @@ describe('DashboardSchema', () => { object: 'opportunity', valueField: 'amount', aggregate: 'sum', - filter: [ - { field: 'status', operator: 'equals', value: 'won' }, - { field: 'close_date', operator: 'this_quarter' }, - ], + filter: { // Modern MongoDB-style filter + status: 'won', + close_date: '{this_quarter}' + }, layout: { x: 0, y: 0, w: 4, h: 3 }, options: { prefix: '$', @@ -395,7 +395,7 @@ describe('DashboardSchema', () => { type: 'metric', object: 'account', aggregate: 'count', - filter: { field: 'created_date', operator: 'this_month' }, + filter: { created_date: '{this_month}' }, // Modern MongoDB-style filter layout: { x: 4, y: 0, w: 4, h: 3 }, }, { @@ -403,7 +403,7 @@ describe('DashboardSchema', () => { type: 'metric', object: 'user', aggregate: 'count', - filter: { field: 'is_active', operator: 'equals', value: true }, + filter: { is_active: true }, layout: { x: 8, y: 0, w: 4, h: 3 }, }, { @@ -413,7 +413,7 @@ describe('DashboardSchema', () => { categoryField: 'product_line', valueField: 'amount', aggregate: 'sum', - filter: { field: 'status', operator: 'equals', value: 'won' }, + filter: { status: 'won' }, layout: { x: 0, y: 3, w: 8, h: 4 }, }, { diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 6b399491a..4689b7bef 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { FilterConditionSchema } from '../data/filter.zod'; /** * Chart Type Enum @@ -28,8 +29,8 @@ export const DashboardWidgetSchema = z.object({ /** Data Source Object */ object: z.string().optional().describe('Data source object name'), - /** Data Filter (ObjectQL JSON) */ - filter: z.any().optional().describe('Data filter criteria'), + /** Data Filter (MongoDB-style FilterCondition) */ + filter: FilterConditionSchema.optional().describe('Data filter criteria'), /** Category Field (X-Axis / Group By) */ categoryField: z.string().optional().describe('Field for grouping (X-Axis)'), From 31b6038fbda627a41d0798a48f4518409d15f204 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:18:07 +0000 Subject: [PATCH 4/6] Update CRM examples and Report schema to use modern MongoDB-style filters Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../docs/references/data/FilterCondition.mdx | 425 +----------------- .../docs/references/data/NormalizedFilter.mdx | 338 +------------- content/docs/references/data/QueryFilter.mdx | 246 +--------- .../docs/references/data/logic/FilterNode.mdx | 330 +------------- .../references/data/logic/FilterOperator.mdx | 420 +---------------- examples/crm/src/ui/dashboards.ts | 123 +++-- examples/crm/src/ui/dashboards.ts.bak | 407 +++++++++++++++++ examples/crm/src/ui/reports.ts | 79 +--- packages/spec/json-schema/Dashboard.json | 20 + .../spec/json-schema/DashboardWidget.json | 20 + packages/spec/src/ui/report.zod.ts | 11 +- 11 files changed, 554 insertions(+), 1865 deletions(-) create mode 100644 examples/crm/src/ui/dashboards.ts.bak diff --git a/content/docs/references/data/FilterCondition.mdx b/content/docs/references/data/FilterCondition.mdx index c206cf996..622d19051 100644 --- a/content/docs/references/data/FilterCondition.mdx +++ b/content/docs/references/data/FilterCondition.mdx @@ -1,428 +1,5 @@ --- title: FilterCondition -description: Recursive filter structure supporting implicit equality, explicit operators, and logical combinations +description: FilterCondition Schema Reference --- -# FilterCondition - -FilterCondition is the recursive data structure that represents filter criteria in ObjectStack. It supports three syntactic patterns: implicit equality, explicit operators, and logical combinations. - -## Overview - -FilterCondition is the core building block for all filter operations. It provides a flexible, MongoDB-inspired syntax that can express simple to complex query logic. - -## Type Definition - -```typescript -type FilterCondition = { - [key: string]: - | any // Implicit equality - | FieldOperators // Explicit operators - | FilterCondition; // Nested relation -} & { - $and?: FilterCondition[]; // Logical AND - $or?: FilterCondition[]; // Logical OR - $not?: FilterCondition; // Logical NOT -}; -``` - -## Syntax Patterns - -### 1. Implicit Equality (Simplest) - -The most common pattern - just assign a value to a field: - -```typescript -{ - status: "active", - verified: true, - age: 18 -} -``` - -**Equivalent to:** -```typescript -{ - status: { $eq: "active" }, - verified: { $eq: true }, - age: { $eq: 18 } -} -``` - -### 2. Explicit Operators - -Use operator objects when you need more than equality: - -```typescript -{ - age: { $gte: 18, $lte: 65 }, - email: { $contains: "@company.com" }, - role: { $in: ["admin", "editor"] }, - deletedAt: { $null: true } -} -``` - -### 3. Logical Combinations - -Combine multiple conditions with `$and`, `$or`, `$not`: - -```typescript -{ - $and: [ - { status: "active" }, - { age: { $gte: 18 } } - ], - $or: [ - { role: "admin" }, - { permissions: { $contains: "write" } } - ], - $not: { - status: "deleted" - } -} -``` - -### 4. Nested Relations - -Filter on related objects using dot notation or nested structures: - -```typescript -{ - department: { - name: "Engineering", - company: { - country: "USA" - } - } -} -``` - -## Available Operators - -### Equality Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$eq` | Equal to | `{ status: { $eq: "active" } }` | -| `$ne` | Not equal to | `{ status: { $ne: "deleted" } }` | - -### Comparison Operators - -| Operator | Types | Description | Example | -| :--- | :--- | :--- | :--- | -| `$gt` | number, Date | Greater than | `{ age: { $gt: 18 } }` | -| `$gte` | number, Date | Greater than or equal | `{ age: { $gte: 18 } }` | -| `$lt` | number, Date | Less than | `{ score: { $lt: 100 } }` | -| `$lte` | number, Date | Less than or equal | `{ age: { $lte: 65 } }` | - -### Set Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$in` | Value in array | `{ role: { $in: ["admin", "editor"] } }` | -| `$nin` | Value not in array | `{ status: { $nin: ["spam", "deleted"] } }` | -| `$between` | Between range (inclusive) | `{ age: { $between: [18, 65] } }` | - -### String Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$contains` | Contains substring | `{ email: { $contains: "@example.com" } }` | -| `$startsWith` | Starts with prefix | `{ username: { $startsWith: "admin_" } }` | -| `$endsWith` | Ends with suffix | `{ filename: { $endsWith: ".pdf" } }` | - -### Special Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$null` | Is null (true) or not null (false) | `{ deletedAt: { $null: true } }` | -| `$exist` | Field exists | `{ metadata: { $exist: true } }` | - -### Logical Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$and` | All conditions must be true | `{ $and: [{ a: 1 }, { b: 2 }] }` | -| `$or` | At least one condition must be true | `{ $or: [{ a: 1 }, { b: 2 }] }` | -| `$not` | Negates the condition | `{ $not: { status: "deleted" } }` | - -## Usage Examples - -### Basic Filtering - -```typescript -// Simple equality -const filter: FilterCondition = { - status: "active", - verified: true -}; - -// With operators -const filter: FilterCondition = { - age: { $gte: 18 }, - score: { $lt: 100 }, - email: { $contains: "@example.com" } -}; - -// Multiple operators on same field -const filter: FilterCondition = { - age: { $gte: 18, $lte: 65 } -}; -``` - -### Logical Combinations - -```typescript -// Implicit AND (fields at same level) -const filter: FilterCondition = { - status: "active", - age: { $gte: 18 }, - verified: true -}; - -// Explicit OR -const filter: FilterCondition = { - $or: [ - { role: "admin" }, - { role: "editor" }, - { role: "moderator" } - ] -}; - -// Complex nested logic -const filter: FilterCondition = { - status: "active", - $and: [ - { - $or: [ - { priority: "high" }, - { urgent: true } - ] - }, - { assignee: { $null: false } } - ] -}; -``` - -### Nested Relations - -```typescript -// Single level nesting -const filter: FilterCondition = { - status: "active", - owner: { - department: "Engineering" - } -}; - -// Multi-level nesting -const filter: FilterCondition = { - status: "active", - owner: { - department: { - company: { - country: "USA" - } - } - } -}; - -// Nested with operators -const filter: FilterCondition = { - task: { - status: { $in: ["todo", "in_progress"] }, - assignee: { - role: "developer", - team: { - name: { $startsWith: "Backend" } - } - } - } -}; -``` - -### Real-World Examples - -#### User Search Query -```typescript -const filter: FilterCondition = { - $or: [ - { name: { $contains: "john" } }, - { email: { $contains: "john" } }, - { username: { $contains: "john" } } - ], - status: "active", - role: { $in: ["user", "admin"] }, - createdAt: { $gte: new Date("2024-01-01") } -}; -``` - -#### Task Management Filter -```typescript -const filter: FilterCondition = { - $and: [ - { - $or: [ - { priority: "high" }, - { dueDate: { $lt: new Date() } } - ] - }, - { status: { $ne: "completed" } }, - { assignee: { $null: false } } - ], - project: { - status: "active", - team: { - department: "Engineering" - } - } -}; -``` - -#### E-commerce Product Filter -```typescript -const filter: FilterCondition = { - category: { $in: ["electronics", "computers"] }, - price: { $between: [100, 1000] }, - inStock: true, - $or: [ - { rating: { $gte: 4.5 } }, - { reviews: { $gt: 100 } } - ], - brand: { - verified: true, - country: { $in: ["USA", "Germany", "Japan"] } - } -}; -``` - -#### Content Management Filter -```typescript -const filter: FilterCondition = { - $and: [ - { published: true }, - { deletedAt: { $null: true } } - ], - $or: [ - { title: { $contains: "tutorial" } }, - { content: { $contains: "guide" } }, - { tags: { name: { $in: ["tutorial", "guide", "howto"] } } } - ], - author: { - verified: true, - role: { $in: ["editor", "admin"] }, - posts: { count: { $gt: 10 } } - }, - publishedAt: { - $gte: new Date("2024-01-01") - } -}; -``` - -## TypeScript Type Safety - -Use the generic `Filter` type for type-safe filters: - -```typescript -interface User { - id: number; - name: string; - age: number; - email: string; - verified: boolean; - profile: { - bio: string; - avatar?: string; - }; -} - -// Type-safe filter -const filter: Filter = { - age: { $gte: 18 }, // ✓ Valid - email: { $contains: "@" }, // ✓ Valid - verified: true, // ✓ Valid - profile: { - bio: { $contains: "developer" } // ✓ Valid - }, - // status: "active" // ✗ Error: 'status' doesn't exist on User -}; -``` - -## Combining with Other Query Features - -FilterCondition is typically used as the `where` clause in a `Query`: - -```typescript -const query: Query = { - object: "user", - fields: ["name", "email", "age"], - where: { // FilterCondition here - status: "active", - age: { $gte: 18 }, - $or: [ - { role: "admin" }, - { verified: true } - ] - }, - sort: [{ field: "createdAt", order: "desc" }], - top: 10 -}; -``` - -## Design Principles - -1. **Declarative**: Describe what you want, not how to get it -2. **Composable**: Build complex filters from simple conditions -3. **Type-safe**: Full TypeScript support with inference -4. **Database-agnostic**: Works with SQL, NoSQL, and other backends -5. **Human-readable**: Easy to write and understand - -## Best Practices - -### Do ✓ - -```typescript -// Use implicit equality for simple cases -{ status: "active", verified: true } - -// Combine operators on same field when needed -{ age: { $gte: 18, $lte: 65 } } - -// Use $or at appropriate level -{ - status: "active", - $or: [{ role: "admin" }, { role: "editor" }] -} -``` - -### Don't ✗ - -```typescript -// Don't use $eq unnecessarily -{ status: { $eq: "active" } } // Just use: { status: "active" } - -// Don't create redundant $and -{ - $and: [{ status: "active" }] // Just use: { status: "active" } -} - -// Don't mix patterns unnecessarily -{ - status: { $eq: "active" }, // Inconsistent - verified: true -} -``` - -## Performance Considerations - -1. **Index Fields**: Ensure frequently filtered fields have database indexes -2. **Avoid Complex Nesting**: Keep filter depth reasonable for query optimization -3. **Use Specific Operators**: `$eq` is faster than `$contains` for exact matches -4. **Limit OR Branches**: Too many `$or` branches can impact performance - -## See Also - -- [QueryFilter](./QueryFilter.mdx) - Top-level query filter wrapper -- [NormalizedFilter](./NormalizedFilter.mdx) - Internal AST representation -- [Query](./types/Query.mdx) - Complete query specification -- [Field Operators](./logic/FilterOperator.mdx) - Detailed operator reference \ No newline at end of file diff --git a/content/docs/references/data/NormalizedFilter.mdx b/content/docs/references/data/NormalizedFilter.mdx index 20462a1f1..babf352d3 100644 --- a/content/docs/references/data/NormalizedFilter.mdx +++ b/content/docs/references/data/NormalizedFilter.mdx @@ -1,342 +1,12 @@ --- title: NormalizedFilter -description: Internal AST representation of filter conditions after normalization +description: NormalizedFilter Schema Reference --- -# NormalizedFilter - -NormalizedFilter is the internal Abstract Syntax Tree (AST) representation of filter conditions after converting all syntactic sugar to explicit operators. This simplified structure makes it easier for driver implementations to process filters consistently. - -## Overview - -During the normalization pass, implicit syntax is converted to explicit operator-based conditions: - -**Input (User-friendly):** -```typescript -{ age: 18, role: "admin" } -``` - -**Output (Normalized):** -```typescript -{ - $and: [ - { age: { $eq: 18 } }, - { role: { $eq: "admin" } } - ] -} -``` - -## Schema - -```typescript -{ - $and?: Array, - $or?: Array, - $not?: FieldCondition | NormalizedFilter -} - -type FieldCondition = Record -``` - ## Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **$and** | `Array` | optional | All conditions must be true (logical AND) | -| **$or** | `Array` | optional | At least one condition must be true (logical OR) | -| **$not** | `FieldCondition \| NormalizedFilter` | optional | Negates the condition (logical NOT) | - -## Normalization Process - -### Stage 1: Implicit Equality → Explicit $eq - -**Before:** -```typescript -{ status: "active", verified: true } -``` - -**After:** -```typescript -{ - $and: [ - { status: { $eq: "active" } }, - { verified: { $eq: true } } - ] -} -``` - -### Stage 2: Flatten Top-level AND - -**Before:** -```typescript -{ - status: "active", - age: { $gte: 18 }, - role: "admin" -} -``` - -**After:** -```typescript -{ - $and: [ - { status: { $eq: "active" } }, - { age: { $gte: 18 } }, - { role: { $eq: "admin" } } - ] -} -``` - -### Stage 3: Preserve Logical Operators - -**Before:** -```typescript -{ - status: "active", - $or: [ - { role: "admin" }, - { permissions: { $contains: "write" } } - ] -} -``` - -**After:** -```typescript -{ - $and: [ - { status: { $eq: "active" } }, - { - $or: [ - { role: { $eq: "admin" } }, - { permissions: { $contains: "write" } } - ] - } - ] -} -``` - -## Examples - -### Simple AND Condition - -**Input:** -```typescript -const filter: QueryFilter = { - where: { - status: "active", - age: { $gte: 18 } - } -}; -``` - -**Normalized:** -```typescript -{ - $and: [ - { status: { $eq: "active" } }, - { age: { $gte: 18 } } - ] -} -``` - -### OR with Nested AND - -**Input:** -```typescript -const filter: QueryFilter = { - where: { - $or: [ - { role: "admin" }, - { - $and: [ - { verified: true }, - { score: { $gt: 80 } } - ] - } - ] - } -}; -``` - -**Normalized:** -```typescript -{ - $or: [ - { role: { $eq: "admin" } }, - { - $and: [ - { verified: { $eq: true } }, - { score: { $gt: 80 } } - ] - } - ] -} -``` - -### NOT Condition - -**Input:** -```typescript -const filter: QueryFilter = { - where: { - $not: { - status: "deleted", - archived: true - } - } -}; -``` - -**Normalized:** -```typescript -{ - $not: { - $and: [ - { status: { $eq: "deleted" } }, - { archived: { $eq: true } } - ] - } -} -``` - -### Complex Nested Structure - -**Input:** -```typescript -const filter: QueryFilter = { - where: { - status: "active", - $and: [ - { age: { $gte: 18 } }, - { - $or: [ - { role: "admin" }, - { permissions: { $contains: "edit" } } - ] - } - ], - department: { - name: "Engineering" - } - } -}; -``` - -**Normalized:** -```typescript -{ - $and: [ - { status: { $eq: "active" } }, - { age: { $gte: 18 } }, - { - $or: [ - { role: { $eq: "admin" } }, - { permissions: { $contains: "edit" } } - ] - }, - { "department.name": { $eq: "Engineering" } } // Flattened path - ] -} -``` - -## Benefits for Driver Implementation - -### 1. Consistent Structure -Every filter is guaranteed to have explicit operators, eliminating ambiguity. - -### 2. Simplified Traversal -Drivers can use a simple recursive pattern: -```typescript -function processFilter(filter: NormalizedFilter) { - if (filter.$and) return processAnd(filter.$and); - if (filter.$or) return processOr(filter.$or); - if (filter.$not) return processNot(filter.$not); - // Process field conditions -} -``` - -### 3. SQL Generation Example -```typescript -function toSQL(filter: NormalizedFilter): string { - if (filter.$and) { - return filter.$and.map(toSQL).join(' AND '); - } - if (filter.$or) { - return filter.$or.map(toSQL).join(' OR '); - } - if (filter.$not) { - return `NOT (${toSQL(filter.$not)})`; - } - // Handle field conditions -} -``` - -### 4. MongoDB Query Example -```typescript -function toMongo(filter: NormalizedFilter) { - if (filter.$and) { - return { $and: filter.$and.map(toMongo) }; - } - if (filter.$or) { - return { $or: filter.$or.map(toMongo) }; - } - if (filter.$not) { - return { $nor: [toMongo(filter.$not)] }; - } - // Handle field conditions -} -``` - -## Field Operators in Normalized Filters - -Each field condition uses explicit operators: - -```typescript -{ - $eq?: any, // Equal to - $ne?: any, // Not equal to - $gt?: number | Date, // Greater than - $gte?: number | Date,// Greater than or equal - $lt?: number | Date, // Less than - $lte?: number | Date,// Less than or equal - $in?: any[], // In array - $nin?: any[], // Not in array - $between?: [number | Date, number | Date], // Between range - $contains?: string, // Contains substring - $startsWith?: string,// Starts with prefix - $endsWith?: string, // Ends with suffix - $null?: boolean, // Is/isn't null - $exist?: boolean // Field exists -} -``` - -## Usage in Driver Implementation - -Drivers should normalize user input before processing: - -```typescript -import { normalizeFilter } from '@objectstack/spec'; - -class MyDriver implements Driver { - async find(object: string, query: Query) { - // Normalize filter - const normalized = normalizeFilter(query.where); - - // Convert to database-specific query - const dbQuery = this.toNativeQuery(normalized); - - // Execute - return this.execute(dbQuery); - } - - private toNativeQuery(filter: NormalizedFilter) { - // Driver-specific implementation - } -} -``` - -## See Also - -- [QueryFilter](./QueryFilter.mdx) - User-friendly filter syntax -- [FilterCondition](./FilterCondition.mdx) - Recursive filter structure -- [Driver Implementation Guide](/docs/guides/custom-driver.mdx) - Building custom drivers +| **$and** | `Record \| any[]` | optional | | +| **$or** | `Record \| any[]` | optional | | +| **$not** | `Record \| any` | optional | | diff --git a/content/docs/references/data/QueryFilter.mdx b/content/docs/references/data/QueryFilter.mdx index dc1ac28e3..ca1b37363 100644 --- a/content/docs/references/data/QueryFilter.mdx +++ b/content/docs/references/data/QueryFilter.mdx @@ -1,252 +1,10 @@ --- title: QueryFilter -description: MongoDB-style query filter for data retrieval operations +description: QueryFilter Schema Reference --- -# QueryFilter - -QueryFilter provides a MongoDB-style, database-agnostic query DSL for filtering data. It supports implicit equality, explicit operators, logical combinations, and nested relation queries. - -## Overview - -QueryFilter is the top-level wrapper for filter conditions, typically used as the `where` clause in queries. - -## Schema - -```typescript -{ - where?: FilterCondition // Optional filter criteria -} -``` - ## Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **where** | `FilterCondition` | optional | Filter criteria for the query | - -## Filter Operators - -### Equality Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$eq` | Equal to | `{ status: { $eq: "active" } }` | -| `$ne` | Not equal to | `{ status: { $ne: "deleted" } }` | - -### Comparison Operators (Numeric/Date) - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$gt` | Greater than | `{ age: { $gt: 18 } }` | -| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | -| `$lt` | Less than | `{ score: { $lt: 100 } }` | -| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | - -### Set & Range Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$in` | Value in array | `{ role: { $in: ["admin", "editor"] } }` | -| `$nin` | Value not in array | `{ status: { $nin: ["spam", "deleted"] } }` | -| `$between` | Between range (inclusive) | `{ age: { $between: [18, 65] } }` | - -### String Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$contains` | Contains substring | `{ email: { $contains: "@example.com" } }` | -| `$startsWith` | Starts with prefix | `{ username: { $startsWith: "admin_" } }` | -| `$endsWith` | Ends with suffix | `{ filename: { $endsWith: ".pdf" } }` | - -### Special Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$null` | Is null (true) or not null (false) | `{ deletedAt: { $null: true } }` | -| `$exist` | Field exists (true) or doesn't exist (false) | `{ metadata: { $exist: true } }` | - -### Logical Operators - -| Operator | Description | Example | -| :--- | :--- | :--- | -| `$and` | All conditions must be true | `{ $and: [{ status: "active" }, { age: { $gte: 18 } }] }` | -| `$or` | At least one condition must be true | `{ $or: [{ role: "admin" }, { role: "editor" }] }` | -| `$not` | Negates the condition | `{ $not: { status: "deleted" } }` | - -## Usage Examples - -### Basic Filtering - -#### Implicit Equality -```typescript -const filter: QueryFilter = { - where: { - status: "active", // Implicit equality - verified: true - } -}; -``` - -#### Explicit Operators -```typescript -const filter: QueryFilter = { - where: { - age: { $gte: 18 }, // Comparison - email: { $contains: "@company.com" }, // String match - role: { $in: ["admin", "editor"] } // Set membership - } -}; -``` - -### Logical Combinations - -#### AND Conditions (Implicit) -```typescript -const filter: QueryFilter = { - where: { - status: "active", // All fields at same level are AND'ed - age: { $gte: 18 }, - verified: true - } -}; -``` - -#### OR Conditions -```typescript -const filter: QueryFilter = { - where: { - $or: [ - { role: "admin" }, - { permissions: { $contains: "write" } } - ] - } -}; -``` - -#### Complex Nested Logic -```typescript -const filter: QueryFilter = { - where: { - status: "active", - $or: [ - { priority: "high" }, - { - $and: [ - { dueDate: { $lt: new Date() } }, - { assignee: { $null: false } } - ] - } - ] - } -}; -``` - -### Nested Relations - -```typescript -const filter: QueryFilter = { - where: { - status: "active", - department: { // Nested relation - name: "Engineering", - company: { // Deeply nested - country: "USA" - } - } - } -}; -``` - -### Real-World Examples - -#### User Search -```typescript -const userSearch: QueryFilter = { - where: { - $or: [ - { name: { $contains: "john" } }, - { email: { $contains: "john" } }, - { username: { $contains: "john" } } - ], - status: "active", - role: { $in: ["user", "admin"] } - } -}; -``` - -#### E-commerce Order Filter -```typescript -const orderFilter: QueryFilter = { - where: { - status: { $in: ["pending", "processing", "shipped"] }, - totalAmount: { $gte: 100 }, - createdAt: { - $between: [ - new Date("2024-01-01"), - new Date("2024-12-31") - ] - }, - customer: { - tier: "premium", - country: { $in: ["US", "CA", "UK"] } - } - } -}; -``` - -#### Content Management -```typescript -const contentFilter: QueryFilter = { - where: { - $and: [ - { published: true }, - { deletedAt: { $null: true } } - ], - $or: [ - { title: { $contains: "tutorial" } }, - { tags: { name: { $in: ["tutorial", "guide", "howto"] } } } - ], - author: { - verified: true, - role: { $in: ["editor", "admin"] } - } - } -}; -``` - -## TypeScript Type Safety - -```typescript -interface User { - id: number; - name: string; - age: number; - email: string; - profile: { - verified: boolean; - }; -} - -// Type-safe filter -const filter: Filter = { - age: { $gte: 18 }, - email: { $contains: "@example.com" }, - profile: { - verified: true - } -}; -``` - -## Design Principles - -1. **Declarative**: Frontend describes "what data to get", not "how to query" -2. **Database Agnostic**: Syntax contains no database-specific directives -3. **Type Safe**: Structure can be statically inferred by TypeScript -4. **Convention over Configuration**: Implicit syntax for common queries - -## See Also - -- [FilterCondition](./FilterCondition.mdx) - Recursive filter structure details -- [NormalizedFilter](./NormalizedFilter.mdx) - Internal AST representation -- [Query](./types/Query.mdx) - Complete query specification +| **where** | `any` | optional | | diff --git a/content/docs/references/data/logic/FilterNode.mdx b/content/docs/references/data/logic/FilterNode.mdx index 3b0cf8519..d554091bc 100644 --- a/content/docs/references/data/logic/FilterNode.mdx +++ b/content/docs/references/data/logic/FilterNode.mdx @@ -1,333 +1,5 @@ --- title: FilterNode -description: Array-based filter syntax for query conditions (Legacy) +description: FilterNode Schema Reference --- -# FilterNode - -FilterNode represents the legacy array-based filter syntax used in ObjectStack queries. This syntax is being deprecated in favor of the MongoDB-style `FilterCondition` syntax. - -## Overview - -FilterNode uses a simple array structure to represent filter conditions: -- **Leaf Node**: `[field, operator, value]` for simple conditions -- **Logic Node**: `[expression, "and"|"or", expression]` for logical combinations - -## Type Definition - -```typescript -type FilterNode = - | [string, FilterOperator, any] // Leaf: Simple condition - | Array; // Logic: Logical combination -``` - -## Syntax Patterns - -### 1. Simple Condition (Leaf Node) - -```typescript -[field, operator, value] -``` - -**Examples:** -```typescript -['status', '=', 'active'] -['age', '>=', 18] -['email', 'contains', '@example.com'] -['role', 'in', ['admin', 'editor']] -['deletedAt', 'is_null', true] -``` - -### 2. Logical Combinations - -```typescript -[condition1, 'and'|'or', condition2] -``` - -**Examples:** -```typescript -// AND -[ - ['status', '=', 'active'], - 'and', - ['age', '>=', 18] -] - -// OR -[ - ['role', '=', 'admin'], - 'or', - ['role', '=', 'editor'] -] - -// Nested logic -[ - ['status', '=', 'active'], - 'and', - [ - ['role', '=', 'admin'], - 'or', - ['verified', '=', true] - ] -] -``` - -## Available Operators - -See [FilterOperator](./FilterOperator.mdx) for complete operator documentation. - -**Common operators:** -- Equality: `=`, `!=`, `<>` -- Comparison: `>`, `>=`, `<`, `<=` -- String: `contains`, `startswith`, `notcontains` -- Set: `in`, `notin`, `between` -- Special: `is_null`, `is_not_null` - -## Usage Examples - -### Basic Filtering - -```typescript -// Equality -const filter: FilterNode = ['status', '=', 'active']; - -// Comparison -const filter: FilterNode = ['age', '>=', 18]; - -// String matching -const filter: FilterNode = ['email', 'contains', '@company.com']; - -// Set membership -const filter: FilterNode = ['role', 'in', ['admin', 'editor', 'viewer']]; -``` - -### Logical AND - -```typescript -const filter: FilterNode = [ - ['status', '=', 'active'], - 'and', - ['verified', '=', true] -]; -``` - -### Logical OR - -```typescript -const filter: FilterNode = [ - ['role', '=', 'admin'], - 'or', - ['permissions', 'contains', 'write'] -]; -``` - -### Complex Nested Logic - -```typescript -const filter: FilterNode = [ - ['status', '=', 'active'], - 'and', - [ - [ - ['priority', '=', 'high'], - 'or', - ['urgent', '=', true] - ], - 'and', - ['assignee', 'is_not_null', true] - ] -]; -``` - -### Real-World Examples - -#### Task Filter -```typescript -const taskFilter: FilterNode = [ - [ - ['status', '!=', 'completed'], - 'and', - ['assignee', 'is_not_null', true] - ], - 'and', - [ - ['priority', '=', 'high'], - 'or', - ['dueDate', '<', new Date()] - ] -]; -``` - -#### User Search -```typescript -const userSearch: FilterNode = [ - [ - ['name', 'contains', 'john'], - 'or', - ['email', 'contains', 'john'] - ], - 'and', - ['status', '=', 'active'] -]; -``` - -## Usage in Query AST - -FilterNode is used in the `filters` field of the Query AST: - -```typescript -const query: Query = { - object: 'user', - fields: ['name', 'email', 'age'], - filters: [ - ['status', '=', 'active'], - 'and', - ['age', '>=', 18] - ], - sort: [{ field: 'createdAt', order: 'desc' }], - top: 10 -}; -``` - -## Migration to Modern Syntax - -⚠️ **Deprecation Notice**: This array-based syntax is being replaced by the MongoDB-style `FilterCondition` syntax, which is more expressive and type-safe. - -### Simple Condition - -**Before (FilterNode):** -```typescript -['status', '=', 'active'] -``` - -**After (FilterCondition):** -```typescript -{ status: 'active' } -// or explicit -{ status: { $eq: 'active' } } -``` - -### Comparison - -**Before (FilterNode):** -```typescript -['age', '>=', 18] -``` - -**After (FilterCondition):** -```typescript -{ age: { $gte: 18 } } -``` - -### String Matching - -**Before (FilterNode):** -```typescript -['email', 'contains', '@example.com'] -['username', 'startswith', 'admin_'] -``` - -**After (FilterCondition):** -```typescript -{ email: { $contains: '@example.com' } } -{ username: { $startsWith: 'admin_' } } -``` - -### Set Operations - -**Before (FilterNode):** -```typescript -['role', 'in', ['admin', 'editor']] -``` - -**After (FilterCondition):** -```typescript -{ role: { $in: ['admin', 'editor'] } } -``` - -### Logical AND - -**Before (FilterNode):** -```typescript -[ - ['status', '=', 'active'], - 'and', - ['age', '>=', 18] -] -``` - -**After (FilterCondition):** -```typescript -{ - status: 'active', - age: { $gte: 18 } -} -``` - -### Logical OR - -**Before (FilterNode):** -```typescript -[ - ['role', '=', 'admin'], - 'or', - ['role', '=', 'editor'] -] -``` - -**After (FilterCondition):** -```typescript -{ - $or: [ - { role: 'admin' }, - { role: 'editor' } - ] -} -``` - -### Complex Nested Logic - -**Before (FilterNode):** -```typescript -[ - ['status', '=', 'active'], - 'and', - [ - ['priority', '=', 'high'], - 'or', - ['urgent', '=', true] - ] -] -``` - -**After (FilterCondition):** -```typescript -{ - status: 'active', - $or: [ - { priority: 'high' }, - { urgent: true } - ] -} -``` - -## Limitations - -1. **Less Type-Safe**: Array structure is harder for TypeScript to validate -2. **Less Readable**: Nested arrays become hard to read -3. **Limited Operators**: Not all operators are available -4. **No Nested Relations**: Cannot easily query nested object properties - -## Benefits of Modern Syntax - -1. **Type Safety**: Full TypeScript support with inference -2. **Readability**: Object syntax is more natural and clear -3. **Flexibility**: Supports nested relations and complex queries -4. **Industry Standard**: MongoDB-style syntax is widely recognized - -## See Also - -- [FilterCondition](../FilterCondition.mdx) - Modern MongoDB-style filter syntax (Recommended) -- [QueryFilter](../QueryFilter.mdx) - Top-level query filter wrapper -- [FilterOperator](./FilterOperator.mdx) - Operator reference and migration guide -- [Query](../types/Query.mdx) - Complete query specification \ No newline at end of file diff --git a/content/docs/references/data/logic/FilterOperator.mdx b/content/docs/references/data/logic/FilterOperator.mdx index 8b482f821..ca63cccc7 100644 --- a/content/docs/references/data/logic/FilterOperator.mdx +++ b/content/docs/references/data/logic/FilterOperator.mdx @@ -1,406 +1,22 @@ --- title: FilterOperator -description: Supported filter operators for query conditions +description: FilterOperator Schema Reference --- -# FilterOperator - -Filter operators define the comparison and logical operations available for filtering data in ObjectStack queries. - -## Overview - -ObjectStack supports two filter operator systems: - -1. **MongoDB-style operators** (Recommended): Modern `$` prefix operators (`$eq`, `$gte`, `$contains`) -2. **Array-based operators** (Legacy): String operators used in array-based filter syntax - -## MongoDB-Style Operators (Recommended) - -These operators are used with the `FilterCondition` and `QueryFilter` types. - -### Equality Operators - -| Operator | SQL Equivalent | MongoDB Equivalent | Description | -| :--- | :--- | :--- | :--- | -| `$eq` | `=` | `$eq` | Equal to | -| `$ne` | `!=` or `<>` | `$ne` | Not equal to | - -**Examples:** -```typescript -{ status: { $eq: "active" } } -{ status: { $ne: "deleted" } } -``` - -### Comparison Operators - -| Operator | SQL Equivalent | MongoDB Equivalent | Data Types | Description | -| :--- | :--- | :--- | :--- | :--- | -| `$gt` | `>` | `$gt` | number, Date | Greater than | -| `$gte` | `>=` | `$gte` | number, Date | Greater than or equal | -| `$lt` | `<` | `$lt` | number, Date | Less than | -| `$lte` | `<=` | `$lte` | number, Date | Less than or equal | - -**Examples:** -```typescript -{ age: { $gt: 18 } } -{ age: { $gte: 18 } } -{ score: { $lt: 100 } } -{ age: { $lte: 65 } } -{ createdAt: { $gte: new Date("2024-01-01") } } -``` - -### Set Operators - -| Operator | SQL Equivalent | MongoDB Equivalent | Description | -| :--- | :--- | :--- | :--- | -| `$in` | `IN (?, ?, ?)` | `$in` | Value in array | -| `$nin` | `NOT IN (?, ?, ?)` | `$nin` | Value not in array | -| `$between` | `BETWEEN ? AND ?` | `$gte` + `$lte` | Between range (inclusive) | - -**Examples:** -```typescript -{ role: { $in: ["admin", "editor", "moderator"] } } -{ status: { $nin: ["spam", "deleted", "archived"] } } -{ age: { $between: [18, 65] } } -{ price: { $between: [100, 1000] } } -``` - -### String Operators - -| Operator | SQL Equivalent | MongoDB Equivalent | Description | -| :--- | :--- | :--- | :--- | -| `$contains` | `LIKE '%?%'` | `$regex` | Contains substring (case-sensitive) | -| `$startsWith` | `LIKE '?%'` | `$regex` | Starts with prefix | -| `$endsWith` | `LIKE '%?'` | `$regex` | Ends with suffix | - -**Examples:** -```typescript -{ email: { $contains: "@example.com" } } -{ username: { $startsWith: "admin_" } } -{ filename: { $endsWith: ".pdf" } } -{ title: { $contains: "tutorial" } } -``` - -**Note:** Case sensitivity is handled at the backend/driver level. - -### Special Operators - -| Operator | SQL Equivalent | MongoDB Equivalent | Description | -| :--- | :--- | :--- | :--- | -| `$null` | `IS NULL` / `IS NOT NULL` | `field: null` | Is null (true) or not null (false) | -| `$exist` | N/A | `$exists` | Field exists (primarily for NoSQL) | - -**Examples:** -```typescript -{ deletedAt: { $null: true } } // IS NULL -{ deletedAt: { $null: false } } // IS NOT NULL -{ metadata: { $exist: true } } // Field exists -{ optionalField: { $exist: false } } // Field doesn't exist -``` - -### Logical Operators - -| Operator | SQL Equivalent | MongoDB Equivalent | Description | -| :--- | :--- | :--- | :--- | -| `$and` | `AND` | `$and` | All conditions must be true | -| `$or` | `OR` | `$or` | At least one condition must be true | -| `$not` | `NOT` | `$not` | Negates the condition | - -**Examples:** -```typescript -// AND (implicit at same level) -{ status: "active", verified: true } - -// AND (explicit) -{ $and: [{ status: "active" }, { age: { $gte: 18 } }] } - -// OR -{ $or: [{ role: "admin" }, { role: "editor" }] } - -// NOT -{ $not: { status: "deleted" } } - -// Complex combination -{ - status: "active", - $and: [ - { - $or: [ - { priority: "high" }, - { urgent: true } - ] - }, - { assignee: { $null: false } } - ] -} -``` - -## Array-Based Operators (Legacy) - -These operators are used in the older array-based filter syntax `[field, operator, value]`. - -### Allowed Values - -* `=` - Equal to -* `!=` - Not equal to -* `<>` - Not equal to (alternative) -* `>` - Greater than -* `>=` - Greater than or equal -* `<` - Less than -* `<=` - Less than or equal -* `startswith` - Starts with prefix -* `contains` - Contains substring -* `notcontains` - Does not contain substring -* `between` - Between range -* `in` - In array -* `notin` - Not in array -* `is_null` - Is null -* `is_not_null` - Is not null - -### Examples (Legacy Syntax) - -```typescript -// Simple filter -['status', '=', 'active'] - -// Comparison -['age', '>=', 18] - -// String matching -['email', 'contains', '@example.com'] -['username', 'startswith', 'admin_'] - -// Set membership -['role', 'in', ['admin', 'editor']] - -// Null check -['deletedAt', 'is_null', true] - -// Logical combinations -[ - ['status', '=', 'active'], - 'and', - ['age', '>=', 18] -] -``` - -## Migration Guide: Legacy → Modern - -### Simple Equality - -**Before (Legacy):** -```typescript -['status', '=', 'active'] -``` - -**After (Modern):** -```typescript -{ status: "active" } -// or explicit -{ status: { $eq: "active" } } -``` - -### Comparison - -**Before (Legacy):** -```typescript -['age', '>=', 18] -['score', '<', 100] -``` - -**After (Modern):** -```typescript -{ age: { $gte: 18 } } -{ score: { $lt: 100 } } -``` - -### String Operators - -**Before (Legacy):** -```typescript -['email', 'contains', '@example.com'] -['username', 'startswith', 'admin_'] -``` - -**After (Modern):** -```typescript -{ email: { $contains: "@example.com" } } -{ username: { $startsWith: "admin_" } } -``` - -### Set Operators - -**Before (Legacy):** -```typescript -['role', 'in', ['admin', 'editor']] -['status', 'notin', ['deleted', 'spam']] -``` - -**After (Modern):** -```typescript -{ role: { $in: ["admin", "editor"] } } -{ status: { $nin: ["deleted", "spam"] } } -``` - -### Null Checks - -**Before (Legacy):** -```typescript -['deletedAt', 'is_null', true] -['deletedAt', 'is_not_null', true] -``` - -**After (Modern):** -```typescript -{ deletedAt: { $null: true } } -{ deletedAt: { $null: false } } -``` - -### Logical AND - -**Before (Legacy):** -```typescript -[ - ['status', '=', 'active'], - 'and', - ['age', '>=', 18] -] -``` - -**After (Modern):** -```typescript -// Implicit AND (recommended) -{ - status: "active", - age: { $gte: 18 } -} - -// Explicit AND -{ - $and: [ - { status: "active" }, - { age: { $gte: 18 } } - ] -} -``` - -### Logical OR - -**Before (Legacy):** -```typescript -[ - ['role', '=', 'admin'], - 'or', - ['role', '=', 'editor'] -] -``` - -**After (Modern):** -```typescript -{ - $or: [ - { role: "admin" }, - { role: "editor" } - ] -} - -// Or more concisely -{ role: { $in: ["admin", "editor"] } } -``` - -## Operator Precedence - -When multiple operators are used, they follow this precedence (highest to lowest): - -1. Field operators (`$eq`, `$gt`, `$contains`, etc.) -2. `$not` -3. `$and` -4. `$or` - -**Example:** -```typescript -{ - status: "active", // 1. Field operator (implicit $eq) - $not: { archived: true }, // 2. NOT - $and: [ // 3. AND - { verified: true } - ], - $or: [ // 4. OR (evaluated last) - { role: "admin" } - ] -} -``` - -## Best Practices - -### Use Implicit Equality -```typescript -// ✓ Good -{ status: "active" } - -// ✗ Unnecessary -{ status: { $eq: "active" } } -``` - -### Combine Operators on Same Field -```typescript -// ✓ Good -{ age: { $gte: 18, $lte: 65 } } - -// ✗ Verbose -{ - $and: [ - { age: { $gte: 18 } }, - { age: { $lte: 65 } } - ] -} -``` - -### Use $in for Multiple OR on Same Field -```typescript -// ✓ Good -{ role: { $in: ["admin", "editor", "moderator"] } } - -// ✗ Verbose -{ - $or: [ - { role: "admin" }, - { role: "editor" }, - { role: "moderator" } - ] -} -``` - -### Prefer $null over Explicit Comparison -```typescript -// ✓ Good -{ deletedAt: { $null: true } } - -// ✗ Less clear -{ deletedAt: null } -``` - -## Database-Specific Notes - -### SQL Databases -- `$contains`, `$startsWith`, `$endsWith` map to `LIKE` with wildcards -- `$in` maps to `IN` clause -- `$between` maps to `BETWEEN AND` -- Case sensitivity depends on column collation - -### MongoDB -- Operators map directly to MongoDB query operators -- `$regex` used for string operators -- Native support for `$exists`, `$in`, `$nin` - -### In-Memory Drivers -- All operators implemented in JavaScript -- String operators are case-sensitive by default -- Performance may vary for large datasets - -## See Also - -- [FilterCondition](../FilterCondition.mdx) - Recursive filter structure -- [QueryFilter](../QueryFilter.mdx) - Top-level query filter wrapper -- [Query](../types/Query.mdx) - Complete query specification -- [Driver Guide](/docs/guides/custom-driver.mdx) - Implementing operators in custom drivers \ No newline at end of file +## Allowed Values + +* `=` +* `!=` +* `<>` +* `>` +* `>=` +* `<` +* `<=` +* `startswith` +* `contains` +* `notcontains` +* `between` +* `in` +* `notin` +* `is_null` +* `is_not_null` \ No newline at end of file diff --git a/examples/crm/src/ui/dashboards.ts b/examples/crm/src/ui/dashboards.ts index 441fae9c4..78c594ccb 100644 --- a/examples/crm/src/ui/dashboards.ts +++ b/examples/crm/src/ui/dashboards.ts @@ -12,10 +12,9 @@ export const SalesDashboard: Dashboard = { title: 'Total Pipeline Value', type: 'metric', object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], + filter: { + stage: { $nin: ['closed_won', 'closed_lost'] } + }, valueField: 'amount', aggregate: 'sum', layout: { x: 0, y: 0, w: 3, h: 2 }, @@ -28,10 +27,10 @@ export const SalesDashboard: Dashboard = { title: 'Closed Won This Quarter', type: 'metric', object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{current_quarter_start}'], - ], + filter: { + stage: 'closed_won', + close_date: { $gte: '{current_quarter_start}' } + }, valueField: 'amount', aggregate: 'sum', layout: { x: 3, y: 0, w: 3, h: 2 }, @@ -44,10 +43,9 @@ export const SalesDashboard: Dashboard = { title: 'Open Opportunities', type: 'metric', object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], + filter: { + stage: { $nin: ['closed_won', 'closed_lost'] } + }, aggregate: 'count', layout: { x: 6, y: 0, w: 3, h: 2 }, options: { @@ -58,9 +56,9 @@ export const SalesDashboard: Dashboard = { title: 'Win Rate', type: 'metric', object: 'opportunity', - filter: [ - ['close_date', '>=', '{current_quarter_start}'], - ], + filter: { + close_date: { $gte: '{current_quarter_start}' } + }, valueField: 'stage', aggregate: 'count', layout: { x: 9, y: 0, w: 3, h: 2 }, @@ -75,10 +73,9 @@ export const SalesDashboard: Dashboard = { title: 'Pipeline by Stage', type: 'funnel', object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], + filter: { + stage: { $nin: ['closed_won', 'closed_lost'] } + }, categoryField: 'stage', valueField: 'amount', aggregate: 'sum', @@ -91,10 +88,9 @@ export const SalesDashboard: Dashboard = { title: 'Opportunities by Owner', type: 'bar', object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], + filter: { + stage: { $nin: ['closed_won', 'closed_lost'] } + }, categoryField: 'owner', valueField: 'amount', aggregate: 'sum', @@ -109,10 +105,10 @@ export const SalesDashboard: Dashboard = { title: 'Monthly Revenue Trend', type: 'line', object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{last_12_months}'], - ], + filter: { + stage: 'closed_won', + close_date: { $gte: '{last_12_months}' } + }, categoryField: 'close_date', valueField: 'amount', aggregate: 'sum', @@ -126,10 +122,9 @@ export const SalesDashboard: Dashboard = { title: 'Top Opportunities', type: 'table', object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], + filter: { + stage: { $nin: ['closed_won', 'closed_lost'] } + }, aggregate: 'count', layout: { x: 8, y: 6, w: 4, h: 4 }, options: { @@ -154,7 +149,7 @@ export const ServiceDashboard: Dashboard = { title: 'Open Cases', type: 'metric', object: 'case', - filter: [['is_closed', '=', false]], + filter: { is_closed: false }, aggregate: 'count', layout: { x: 0, y: 0, w: 3, h: 2 }, options: { @@ -165,10 +160,10 @@ export const ServiceDashboard: Dashboard = { title: 'Critical Cases', type: 'metric', object: 'case', - filter: [ - ['priority', '=', 'critical'], - ['is_closed', '=', false], - ], + filter: { + priority: 'critical', + is_closed: false + }, aggregate: 'count', layout: { x: 3, y: 0, w: 3, h: 2 }, options: { @@ -179,7 +174,7 @@ export const ServiceDashboard: Dashboard = { title: 'Avg Resolution Time (hrs)', type: 'metric', object: 'case', - filter: [['is_closed', '=', true]], + filter: { is_closed: true }, valueField: 'resolution_time_hours', aggregate: 'avg', layout: { x: 6, y: 0, w: 3, h: 2 }, @@ -192,7 +187,7 @@ export const ServiceDashboard: Dashboard = { title: 'SLA Violations', type: 'metric', object: 'case', - filter: [['is_sla_violated', '=', true]], + filter: { is_sla_violated: true }, aggregate: 'count', layout: { x: 9, y: 0, w: 3, h: 2 }, options: { @@ -205,7 +200,7 @@ export const ServiceDashboard: Dashboard = { title: 'Cases by Status', type: 'donut', object: 'case', - filter: [['is_closed', '=', false]], + filter: { is_closed: false }, categoryField: 'status', aggregate: 'count', layout: { x: 0, y: 2, w: 4, h: 4 }, @@ -217,7 +212,7 @@ export const ServiceDashboard: Dashboard = { title: 'Cases by Priority', type: 'pie', object: 'case', - filter: [['is_closed', '=', false]], + filter: { is_closed: false }, categoryField: 'priority', aggregate: 'count', layout: { x: 4, y: 2, w: 4, h: 4 }, @@ -239,9 +234,9 @@ export const ServiceDashboard: Dashboard = { title: 'Daily Case Volume', type: 'line', object: 'case', - filter: [ - ['created_date', '>=', '{last_30_days}'], - ], + filter: { + created_date: { $gte: '{last_30_days}' } + }, categoryField: 'created_date', aggregate: 'count', layout: { x: 0, y: 6, w: 8, h: 4 }, @@ -253,10 +248,10 @@ export const ServiceDashboard: Dashboard = { title: 'My Open Cases', type: 'table', object: 'case', - filter: [ - ['owner', '=', '{current_user}'], - ['is_closed', '=', false], - ], + filter: { + owner: '{current_user}', + is_closed: false + }, aggregate: 'count', layout: { x: 8, y: 6, w: 4, h: 4 }, options: { @@ -281,10 +276,10 @@ export const ExecutiveDashboard: Dashboard = { title: 'Total Revenue (YTD)', type: 'metric', object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{current_year_start}'], - ], + filter: { + stage: 'closed_won', + close_date: { $gte: '{current_year_start}' } + }, valueField: 'amount', aggregate: 'sum', layout: { x: 0, y: 0, w: 3, h: 2 }, @@ -297,7 +292,7 @@ export const ExecutiveDashboard: Dashboard = { title: 'Total Accounts', type: 'metric', object: 'account', - filter: [['is_active', '=', true]], + filter: { is_active: true }, aggregate: 'count', layout: { x: 3, y: 0, w: 3, h: 2 }, options: { @@ -318,7 +313,7 @@ export const ExecutiveDashboard: Dashboard = { title: 'Total Leads', type: 'metric', object: 'lead', - filter: [['is_converted', '=', false]], + filter: { is_converted: false }, aggregate: 'count', layout: { x: 9, y: 0, w: 3, h: 2 }, options: { @@ -331,10 +326,10 @@ export const ExecutiveDashboard: Dashboard = { title: 'Revenue by Industry', type: 'bar', object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{current_year_start}'], - ], + filter: { + stage: 'closed_won', + close_date: { $gte: '{current_year_start}' } + }, categoryField: 'account.industry', valueField: 'amount', aggregate: 'sum', @@ -344,10 +339,10 @@ export const ExecutiveDashboard: Dashboard = { title: 'Quarterly Revenue Trend', type: 'line', object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{last_4_quarters}'], - ], + filter: { + stage: 'closed_won', + close_date: { $gte: '{last_4_quarters}' } + }, categoryField: 'close_date', valueField: 'amount', aggregate: 'sum', @@ -362,9 +357,9 @@ export const ExecutiveDashboard: Dashboard = { title: 'New Accounts by Month', type: 'bar', object: 'account', - filter: [ - ['created_date', '>=', '{last_6_months}'], - ], + filter: { + created_date: { $gte: '{last_6_months}' } + }, categoryField: 'created_date', aggregate: 'count', layout: { x: 0, y: 6, w: 4, h: 4 }, diff --git a/examples/crm/src/ui/dashboards.ts.bak b/examples/crm/src/ui/dashboards.ts.bak new file mode 100644 index 000000000..441fae9c4 --- /dev/null +++ b/examples/crm/src/ui/dashboards.ts.bak @@ -0,0 +1,407 @@ +import type { Dashboard } from '@objectstack/spec'; + +// Sales Performance Dashboard +export const SalesDashboard: Dashboard = { + name: 'sales_dashboard', + label: 'Sales Performance', + description: 'Key sales metrics and pipeline overview', + + widgets: [ + // Row 1: Key Metrics + { + title: 'Total Pipeline Value', + type: 'metric', + object: 'opportunity', + filter: [ + ['stage', '!=', 'closed_won'], + ['stage', '!=', 'closed_lost'], + ], + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 0, w: 3, h: 2 }, + options: { + prefix: '$', + color: '#4169E1', + } + }, + { + title: 'Closed Won This Quarter', + type: 'metric', + object: 'opportunity', + filter: [ + ['stage', '=', 'closed_won'], + ['close_date', '>=', '{current_quarter_start}'], + ], + valueField: 'amount', + aggregate: 'sum', + layout: { x: 3, y: 0, w: 3, h: 2 }, + options: { + prefix: '$', + color: '#00AA00', + } + }, + { + title: 'Open Opportunities', + type: 'metric', + object: 'opportunity', + filter: [ + ['stage', '!=', 'closed_won'], + ['stage', '!=', 'closed_lost'], + ], + aggregate: 'count', + layout: { x: 6, y: 0, w: 3, h: 2 }, + options: { + color: '#FFA500', + } + }, + { + title: 'Win Rate', + type: 'metric', + object: 'opportunity', + filter: [ + ['close_date', '>=', '{current_quarter_start}'], + ], + valueField: 'stage', + aggregate: 'count', + layout: { x: 9, y: 0, w: 3, h: 2 }, + options: { + suffix: '%', + color: '#9370DB', + } + }, + + // Row 2: Pipeline Analysis + { + title: 'Pipeline by Stage', + type: 'funnel', + object: 'opportunity', + filter: [ + ['stage', '!=', 'closed_won'], + ['stage', '!=', 'closed_lost'], + ], + categoryField: 'stage', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 2, w: 6, h: 4 }, + options: { + showValues: true, + } + }, + { + title: 'Opportunities by Owner', + type: 'bar', + object: 'opportunity', + filter: [ + ['stage', '!=', 'closed_won'], + ['stage', '!=', 'closed_lost'], + ], + categoryField: 'owner', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 6, y: 2, w: 6, h: 4 }, + options: { + horizontal: true, + } + }, + + // Row 3: Trends + { + title: 'Monthly Revenue Trend', + type: 'line', + object: 'opportunity', + filter: [ + ['stage', '=', 'closed_won'], + ['close_date', '>=', '{last_12_months}'], + ], + categoryField: 'close_date', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 6, w: 8, h: 4 }, + options: { + dateGranularity: 'month', + showTrend: true, + } + }, + { + title: 'Top Opportunities', + type: 'table', + object: 'opportunity', + filter: [ + ['stage', '!=', 'closed_won'], + ['stage', '!=', 'closed_lost'], + ], + aggregate: 'count', + layout: { x: 8, y: 6, w: 4, h: 4 }, + options: { + columns: ['name', 'amount', 'stage', 'close_date'], + sortBy: 'amount', + sortOrder: 'desc', + limit: 10, + } + }, + ] +}; + +// Customer Service Dashboard +export const ServiceDashboard: Dashboard = { + name: 'service_dashboard', + label: 'Customer Service', + description: 'Support case metrics and performance', + + widgets: [ + // Row 1: Key Metrics + { + title: 'Open Cases', + type: 'metric', + object: 'case', + filter: [['is_closed', '=', false]], + aggregate: 'count', + layout: { x: 0, y: 0, w: 3, h: 2 }, + options: { + color: '#FFA500', + } + }, + { + title: 'Critical Cases', + type: 'metric', + object: 'case', + filter: [ + ['priority', '=', 'critical'], + ['is_closed', '=', false], + ], + aggregate: 'count', + layout: { x: 3, y: 0, w: 3, h: 2 }, + options: { + color: '#FF0000', + } + }, + { + title: 'Avg Resolution Time (hrs)', + type: 'metric', + object: 'case', + filter: [['is_closed', '=', true]], + valueField: 'resolution_time_hours', + aggregate: 'avg', + layout: { x: 6, y: 0, w: 3, h: 2 }, + options: { + suffix: 'h', + color: '#4169E1', + } + }, + { + title: 'SLA Violations', + type: 'metric', + object: 'case', + filter: [['is_sla_violated', '=', true]], + aggregate: 'count', + layout: { x: 9, y: 0, w: 3, h: 2 }, + options: { + color: '#FF4500', + } + }, + + // Row 2: Case Distribution + { + title: 'Cases by Status', + type: 'donut', + object: 'case', + filter: [['is_closed', '=', false]], + categoryField: 'status', + aggregate: 'count', + layout: { x: 0, y: 2, w: 4, h: 4 }, + options: { + showLegend: true, + } + }, + { + title: 'Cases by Priority', + type: 'pie', + object: 'case', + filter: [['is_closed', '=', false]], + categoryField: 'priority', + aggregate: 'count', + layout: { x: 4, y: 2, w: 4, h: 4 }, + options: { + showLegend: true, + } + }, + { + title: 'Cases by Origin', + type: 'bar', + object: 'case', + categoryField: 'origin', + aggregate: 'count', + layout: { x: 8, y: 2, w: 4, h: 4 }, + }, + + // Row 3: Trends and Lists + { + title: 'Daily Case Volume', + type: 'line', + object: 'case', + filter: [ + ['created_date', '>=', '{last_30_days}'], + ], + categoryField: 'created_date', + aggregate: 'count', + layout: { x: 0, y: 6, w: 8, h: 4 }, + options: { + dateGranularity: 'day', + } + }, + { + title: 'My Open Cases', + type: 'table', + object: 'case', + filter: [ + ['owner', '=', '{current_user}'], + ['is_closed', '=', false], + ], + aggregate: 'count', + layout: { x: 8, y: 6, w: 4, h: 4 }, + options: { + columns: ['case_number', 'subject', 'priority', 'status'], + sortBy: 'priority', + sortOrder: 'desc', + limit: 10, + } + }, + ] +}; + +// Executive Dashboard +export const ExecutiveDashboard: Dashboard = { + name: 'executive_dashboard', + label: 'Executive Overview', + description: 'High-level business metrics', + + widgets: [ + // Row 1: Revenue Metrics + { + title: 'Total Revenue (YTD)', + type: 'metric', + object: 'opportunity', + filter: [ + ['stage', '=', 'closed_won'], + ['close_date', '>=', '{current_year_start}'], + ], + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 0, w: 3, h: 2 }, + options: { + prefix: '$', + color: '#00AA00', + } + }, + { + title: 'Total Accounts', + type: 'metric', + object: 'account', + filter: [['is_active', '=', true]], + aggregate: 'count', + layout: { x: 3, y: 0, w: 3, h: 2 }, + options: { + color: '#4169E1', + } + }, + { + title: 'Total Contacts', + type: 'metric', + object: 'contact', + aggregate: 'count', + layout: { x: 6, y: 0, w: 3, h: 2 }, + options: { + color: '#9370DB', + } + }, + { + title: 'Total Leads', + type: 'metric', + object: 'lead', + filter: [['is_converted', '=', false]], + aggregate: 'count', + layout: { x: 9, y: 0, w: 3, h: 2 }, + options: { + color: '#FFA500', + } + }, + + // Row 2: Revenue Analysis + { + title: 'Revenue by Industry', + type: 'bar', + object: 'opportunity', + filter: [ + ['stage', '=', 'closed_won'], + ['close_date', '>=', '{current_year_start}'], + ], + categoryField: 'account.industry', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 0, y: 2, w: 6, h: 4 }, + }, + { + title: 'Quarterly Revenue Trend', + type: 'line', + object: 'opportunity', + filter: [ + ['stage', '=', 'closed_won'], + ['close_date', '>=', '{last_4_quarters}'], + ], + categoryField: 'close_date', + valueField: 'amount', + aggregate: 'sum', + layout: { x: 6, y: 2, w: 6, h: 4 }, + options: { + dateGranularity: 'quarter', + } + }, + + // Row 3: Customer & Activity Metrics + { + title: 'New Accounts by Month', + type: 'bar', + object: 'account', + filter: [ + ['created_date', '>=', '{last_6_months}'], + ], + categoryField: 'created_date', + aggregate: 'count', + layout: { x: 0, y: 6, w: 4, h: 4 }, + options: { + dateGranularity: 'month', + } + }, + { + title: 'Lead Conversion Rate', + type: 'metric', + object: 'lead', + valueField: 'is_converted', + aggregate: 'avg', + layout: { x: 4, y: 6, w: 4, h: 4 }, + options: { + suffix: '%', + color: '#00AA00', + } + }, + { + title: 'Top Accounts by Revenue', + type: 'table', + object: 'account', + aggregate: 'count', + layout: { x: 8, y: 6, w: 4, h: 4 }, + options: { + columns: ['name', 'annual_revenue', 'type'], + sortBy: 'annual_revenue', + sortOrder: 'desc', + limit: 10, + } + }, + ] +}; + +export const CrmDashboards = { + SalesDashboard, + ServiceDashboard, + ExecutiveDashboard, +}; diff --git a/examples/crm/src/ui/reports.ts b/examples/crm/src/ui/reports.ts index 65bad7dd0..b27957717 100644 --- a/examples/crm/src/ui/reports.ts +++ b/examples/crm/src/ui/reports.ts @@ -41,21 +41,10 @@ export const OpportunitiesByStageReport: Report = { } ], - filter: '1 AND 2', - filterItems: [ - { - id: 1, - field: 'stage', - operator: '!=', - value: 'closed_lost', - }, - { - id: 2, - field: 'close_date', - operator: '>=', - value: '{current_year_start}', - } - ], + filter: { + stage: { $ne: 'closed_lost' }, + close_date: { $gte: '{current_year_start}' } + }, chart: { type: 'bar', @@ -102,15 +91,9 @@ export const WonOpportunitiesByOwnerReport: Report = { } ], - filter: '1', - filterItems: [ - { - id: 1, - field: 'stage', - operator: '=', - value: 'closed_won', - } - ], + filter: { + stage: 'closed_won' + }, chart: { type: 'column', @@ -155,15 +138,9 @@ export const AccountsByIndustryTypeReport: Report = { } ], - filter: '1', - filterItems: [ - { - id: 1, - field: 'is_active', - operator: '=', - value: true, - } - ], + filter: { + is_active: true + }, }; // Support Report - Cases by Status and Priority @@ -252,15 +229,9 @@ export const SlaPerformanceReport: Report = { } ], - filter: '1', - filterItems: [ - { - id: 1, - field: 'is_closed', - operator: '=', - value: true, - } - ], + filter: { + is_closed: true + }, chart: { type: 'column', @@ -306,15 +277,9 @@ export const LeadsBySourceReport: Report = { } ], - filter: '1', - filterItems: [ - { - id: 1, - field: 'is_converted', - operator: '=', - value: false, - } - ], + filter: { + is_converted: false + }, chart: { type: 'pie', @@ -405,15 +370,9 @@ export const TasksByOwnerReport: Report = { } ], - filter: '1', - filterItems: [ - { - id: 1, - field: 'is_completed', - operator: '=', - value: false, - } - ], + filter: { + is_completed: false + }, }; export const CrmReports = { diff --git a/packages/spec/json-schema/Dashboard.json b/packages/spec/json-schema/Dashboard.json index 3f7376fd8..a0421fd29 100644 --- a/packages/spec/json-schema/Dashboard.json +++ b/packages/spec/json-schema/Dashboard.json @@ -46,6 +46,26 @@ "description": "Data source object name" }, "filter": { + "allOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "object", + "properties": { + "$and": { + "type": "array", + "items": {} + }, + "$or": { + "type": "array", + "items": {} + }, + "$not": {} + } + } + ], "description": "Data filter criteria" }, "categoryField": { diff --git a/packages/spec/json-schema/DashboardWidget.json b/packages/spec/json-schema/DashboardWidget.json index 3a9823f27..8f9288d24 100644 --- a/packages/spec/json-schema/DashboardWidget.json +++ b/packages/spec/json-schema/DashboardWidget.json @@ -28,6 +28,26 @@ "description": "Data source object name" }, "filter": { + "allOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "object", + "properties": { + "$and": { + "type": "array", + "items": {} + }, + "$or": { + "type": "array", + "items": {} + }, + "$not": {} + } + } + ], "description": "Data filter criteria" }, "categoryField": { diff --git a/packages/spec/src/ui/report.zod.ts b/packages/spec/src/ui/report.zod.ts index 44d601e6c..504f75bdb 100644 --- a/packages/spec/src/ui/report.zod.ts +++ b/packages/spec/src/ui/report.zod.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { FilterConditionSchema } from '../data/filter.zod'; /** * Report Type Enum @@ -62,14 +63,8 @@ export const ReportSchema = z.object({ groupingsDown: z.array(ReportGroupingSchema).optional().describe('Row groupings'), groupingsAcross: z.array(ReportGroupingSchema).optional().describe('Column groupings (Matrix only)'), - /** Filtering */ - filter: z.string().optional().describe('Filter logic (e.g. "1 AND (2 OR 3)")'), - filterItems: z.array(z.object({ - id: z.number(), - field: z.string(), - operator: z.string(), - value: z.any() - })).optional().describe('Filter criteria lines'), + /** Filtering (MongoDB-style FilterCondition) */ + filter: FilterConditionSchema.optional().describe('Filter criteria'), /** Visualization */ chart: ReportChartSchema.optional().describe('Embedded chart configuration'), From 503ec7a60df6ee47e6f01f48ec47cb18c443205c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:18:21 +0000 Subject: [PATCH 5/6] Remove backup file --- examples/crm/src/ui/dashboards.ts.bak | 407 -------------------------- 1 file changed, 407 deletions(-) delete mode 100644 examples/crm/src/ui/dashboards.ts.bak diff --git a/examples/crm/src/ui/dashboards.ts.bak b/examples/crm/src/ui/dashboards.ts.bak deleted file mode 100644 index 441fae9c4..000000000 --- a/examples/crm/src/ui/dashboards.ts.bak +++ /dev/null @@ -1,407 +0,0 @@ -import type { Dashboard } from '@objectstack/spec'; - -// Sales Performance Dashboard -export const SalesDashboard: Dashboard = { - name: 'sales_dashboard', - label: 'Sales Performance', - description: 'Key sales metrics and pipeline overview', - - widgets: [ - // Row 1: Key Metrics - { - title: 'Total Pipeline Value', - type: 'metric', - object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], - valueField: 'amount', - aggregate: 'sum', - layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { - prefix: '$', - color: '#4169E1', - } - }, - { - title: 'Closed Won This Quarter', - type: 'metric', - object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{current_quarter_start}'], - ], - valueField: 'amount', - aggregate: 'sum', - layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { - prefix: '$', - color: '#00AA00', - } - }, - { - title: 'Open Opportunities', - type: 'metric', - object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], - aggregate: 'count', - layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { - color: '#FFA500', - } - }, - { - title: 'Win Rate', - type: 'metric', - object: 'opportunity', - filter: [ - ['close_date', '>=', '{current_quarter_start}'], - ], - valueField: 'stage', - aggregate: 'count', - layout: { x: 9, y: 0, w: 3, h: 2 }, - options: { - suffix: '%', - color: '#9370DB', - } - }, - - // Row 2: Pipeline Analysis - { - title: 'Pipeline by Stage', - type: 'funnel', - object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], - categoryField: 'stage', - valueField: 'amount', - aggregate: 'sum', - layout: { x: 0, y: 2, w: 6, h: 4 }, - options: { - showValues: true, - } - }, - { - title: 'Opportunities by Owner', - type: 'bar', - object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], - categoryField: 'owner', - valueField: 'amount', - aggregate: 'sum', - layout: { x: 6, y: 2, w: 6, h: 4 }, - options: { - horizontal: true, - } - }, - - // Row 3: Trends - { - title: 'Monthly Revenue Trend', - type: 'line', - object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{last_12_months}'], - ], - categoryField: 'close_date', - valueField: 'amount', - aggregate: 'sum', - layout: { x: 0, y: 6, w: 8, h: 4 }, - options: { - dateGranularity: 'month', - showTrend: true, - } - }, - { - title: 'Top Opportunities', - type: 'table', - object: 'opportunity', - filter: [ - ['stage', '!=', 'closed_won'], - ['stage', '!=', 'closed_lost'], - ], - aggregate: 'count', - layout: { x: 8, y: 6, w: 4, h: 4 }, - options: { - columns: ['name', 'amount', 'stage', 'close_date'], - sortBy: 'amount', - sortOrder: 'desc', - limit: 10, - } - }, - ] -}; - -// Customer Service Dashboard -export const ServiceDashboard: Dashboard = { - name: 'service_dashboard', - label: 'Customer Service', - description: 'Support case metrics and performance', - - widgets: [ - // Row 1: Key Metrics - { - title: 'Open Cases', - type: 'metric', - object: 'case', - filter: [['is_closed', '=', false]], - aggregate: 'count', - layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { - color: '#FFA500', - } - }, - { - title: 'Critical Cases', - type: 'metric', - object: 'case', - filter: [ - ['priority', '=', 'critical'], - ['is_closed', '=', false], - ], - aggregate: 'count', - layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { - color: '#FF0000', - } - }, - { - title: 'Avg Resolution Time (hrs)', - type: 'metric', - object: 'case', - filter: [['is_closed', '=', true]], - valueField: 'resolution_time_hours', - aggregate: 'avg', - layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { - suffix: 'h', - color: '#4169E1', - } - }, - { - title: 'SLA Violations', - type: 'metric', - object: 'case', - filter: [['is_sla_violated', '=', true]], - aggregate: 'count', - layout: { x: 9, y: 0, w: 3, h: 2 }, - options: { - color: '#FF4500', - } - }, - - // Row 2: Case Distribution - { - title: 'Cases by Status', - type: 'donut', - object: 'case', - filter: [['is_closed', '=', false]], - categoryField: 'status', - aggregate: 'count', - layout: { x: 0, y: 2, w: 4, h: 4 }, - options: { - showLegend: true, - } - }, - { - title: 'Cases by Priority', - type: 'pie', - object: 'case', - filter: [['is_closed', '=', false]], - categoryField: 'priority', - aggregate: 'count', - layout: { x: 4, y: 2, w: 4, h: 4 }, - options: { - showLegend: true, - } - }, - { - title: 'Cases by Origin', - type: 'bar', - object: 'case', - categoryField: 'origin', - aggregate: 'count', - layout: { x: 8, y: 2, w: 4, h: 4 }, - }, - - // Row 3: Trends and Lists - { - title: 'Daily Case Volume', - type: 'line', - object: 'case', - filter: [ - ['created_date', '>=', '{last_30_days}'], - ], - categoryField: 'created_date', - aggregate: 'count', - layout: { x: 0, y: 6, w: 8, h: 4 }, - options: { - dateGranularity: 'day', - } - }, - { - title: 'My Open Cases', - type: 'table', - object: 'case', - filter: [ - ['owner', '=', '{current_user}'], - ['is_closed', '=', false], - ], - aggregate: 'count', - layout: { x: 8, y: 6, w: 4, h: 4 }, - options: { - columns: ['case_number', 'subject', 'priority', 'status'], - sortBy: 'priority', - sortOrder: 'desc', - limit: 10, - } - }, - ] -}; - -// Executive Dashboard -export const ExecutiveDashboard: Dashboard = { - name: 'executive_dashboard', - label: 'Executive Overview', - description: 'High-level business metrics', - - widgets: [ - // Row 1: Revenue Metrics - { - title: 'Total Revenue (YTD)', - type: 'metric', - object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{current_year_start}'], - ], - valueField: 'amount', - aggregate: 'sum', - layout: { x: 0, y: 0, w: 3, h: 2 }, - options: { - prefix: '$', - color: '#00AA00', - } - }, - { - title: 'Total Accounts', - type: 'metric', - object: 'account', - filter: [['is_active', '=', true]], - aggregate: 'count', - layout: { x: 3, y: 0, w: 3, h: 2 }, - options: { - color: '#4169E1', - } - }, - { - title: 'Total Contacts', - type: 'metric', - object: 'contact', - aggregate: 'count', - layout: { x: 6, y: 0, w: 3, h: 2 }, - options: { - color: '#9370DB', - } - }, - { - title: 'Total Leads', - type: 'metric', - object: 'lead', - filter: [['is_converted', '=', false]], - aggregate: 'count', - layout: { x: 9, y: 0, w: 3, h: 2 }, - options: { - color: '#FFA500', - } - }, - - // Row 2: Revenue Analysis - { - title: 'Revenue by Industry', - type: 'bar', - object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{current_year_start}'], - ], - categoryField: 'account.industry', - valueField: 'amount', - aggregate: 'sum', - layout: { x: 0, y: 2, w: 6, h: 4 }, - }, - { - title: 'Quarterly Revenue Trend', - type: 'line', - object: 'opportunity', - filter: [ - ['stage', '=', 'closed_won'], - ['close_date', '>=', '{last_4_quarters}'], - ], - categoryField: 'close_date', - valueField: 'amount', - aggregate: 'sum', - layout: { x: 6, y: 2, w: 6, h: 4 }, - options: { - dateGranularity: 'quarter', - } - }, - - // Row 3: Customer & Activity Metrics - { - title: 'New Accounts by Month', - type: 'bar', - object: 'account', - filter: [ - ['created_date', '>=', '{last_6_months}'], - ], - categoryField: 'created_date', - aggregate: 'count', - layout: { x: 0, y: 6, w: 4, h: 4 }, - options: { - dateGranularity: 'month', - } - }, - { - title: 'Lead Conversion Rate', - type: 'metric', - object: 'lead', - valueField: 'is_converted', - aggregate: 'avg', - layout: { x: 4, y: 6, w: 4, h: 4 }, - options: { - suffix: '%', - color: '#00AA00', - } - }, - { - title: 'Top Accounts by Revenue', - type: 'table', - object: 'account', - aggregate: 'count', - layout: { x: 8, y: 6, w: 4, h: 4 }, - options: { - columns: ['name', 'annual_revenue', 'type'], - sortBy: 'annual_revenue', - sortOrder: 'desc', - limit: 10, - } - }, - ] -}; - -export const CrmDashboards = { - SalesDashboard, - ServiceDashboard, - ExecutiveDashboard, -}; From 9dc055e153e78da065ae5ca8922e4052300e7a2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:55:18 +0000 Subject: [PATCH 6/6] Fix build error: use 'filters' instead of 'where' in QueryOptions Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../docs/references/ui/analytics/Report.mdx | 3 +- examples/todo/src/client-test.ts | 2 +- packages/spec/json-schema/Report.json | 46 ++++++++----------- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/content/docs/references/ui/analytics/Report.mdx b/content/docs/references/ui/analytics/Report.mdx index e16bac929..68954c597 100644 --- a/content/docs/references/ui/analytics/Report.mdx +++ b/content/docs/references/ui/analytics/Report.mdx @@ -15,6 +15,5 @@ description: Report Schema Reference | **columns** | `object[]` | ✅ | Columns to display | | **groupingsDown** | `object[]` | optional | Row groupings | | **groupingsAcross** | `object[]` | optional | Column groupings (Matrix only) | -| **filter** | `string` | optional | Filter logic (e.g. "1 AND (2 OR 3)") | -| **filterItems** | `object[]` | optional | Filter criteria lines | +| **filter** | `any` | optional | Filter criteria | | **chart** | `object` | optional | Embedded chart configuration | diff --git a/examples/todo/src/client-test.ts b/examples/todo/src/client-test.ts index d27c01803..d47a8b4b0 100644 --- a/examples/todo/src/client-test.ts +++ b/examples/todo/src/client-test.ts @@ -59,7 +59,7 @@ async function main() { console.log('\n🧠 Testing Advanced Query (Select & Modern Filter)...'); const advancedResult = await client.data.find('todo_task', { select: ['subject', 'priority'], - where: { + filters: { priority: { $gte: 2 } // Modern MongoDB-style filter syntax }, sort: ['-priority'] diff --git a/packages/spec/json-schema/Report.json b/packages/spec/json-schema/Report.json index b877b54c2..24cd89c78 100644 --- a/packages/spec/json-schema/Report.json +++ b/packages/spec/json-schema/Report.json @@ -137,33 +137,27 @@ "description": "Column groupings (Matrix only)" }, "filter": { - "type": "string", - "description": "Filter logic (e.g. \"1 AND (2 OR 3)\")" - }, - "filterItems": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "field": { - "type": "string" - }, - "operator": { - "type": "string" - }, - "value": {} + "allOf": [ + { + "type": "object", + "additionalProperties": {} }, - "required": [ - "id", - "field", - "operator" - ], - "additionalProperties": false - }, - "description": "Filter criteria lines" + { + "type": "object", + "properties": { + "$and": { + "type": "array", + "items": {} + }, + "$or": { + "type": "array", + "items": {} + }, + "$not": {} + } + } + ], + "description": "Filter criteria" }, "chart": { "type": "object",