Skip to content

Commit 93aac10

Browse files
authored
Merge pull request #131 from objectstack-ai/copilot/update-objectstack-filter-rules
2 parents f0c1d9c + ceea0d0 commit 93aac10

25 files changed

+955
-48
lines changed

packages/core/src/adapters/README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const adapter = new ObjectStackAdapter({
5252
// Manually connect (optional, auto-connects on first request)
5353
await adapter.connect();
5454

55-
// Query with filters
55+
// Query with filters (MongoDB-like operators)
5656
const result = await adapter.find('tasks', {
5757
$filter: {
5858
status: 'active',
@@ -68,14 +68,53 @@ const client = adapter.getClient();
6868
const metadata = await client.meta.getObject('task');
6969
```
7070

71+
### Filter Conversion
72+
73+
The adapter automatically converts MongoDB-like filter operators to **ObjectStack FilterNode AST format**. This ensures compatibility with the latest ObjectStack Protocol (v0.1.2+).
74+
75+
#### Supported Filter Operators
76+
77+
| MongoDB Operator | ObjectStack Operator | Example |
78+
|------------------|---------------------|---------|
79+
| `$eq` or simple value | `=` | `{ status: 'active' }``['status', '=', 'active']` |
80+
| `$ne` | `!=` | `{ status: { $ne: 'archived' } }``['status', '!=', 'archived']` |
81+
| `$gt` | `>` | `{ age: { $gt: 18 } }``['age', '>', 18]` |
82+
| `$gte` | `>=` | `{ age: { $gte: 18 } }``['age', '>=', 18]` |
83+
| `$lt` | `<` | `{ age: { $lt: 65 } }``['age', '<', 65]` |
84+
| `$lte` | `<=` | `{ age: { $lte: 65 } }``['age', '<=', 65]` |
85+
| `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }``['status', 'in', ['active', 'pending']]` |
86+
| `$nin` / `$notin` | `notin` | `{ status: { $nin: ['archived'] } }``['status', 'notin', ['archived']]` |
87+
| `$contains` / `$regex` | `contains` | `{ name: { $contains: 'John' } }``['name', 'contains', 'John']` |
88+
| `$startswith` | `startswith` | `{ email: { $startswith: 'admin' } }``['email', 'startswith', 'admin']` |
89+
| `$between` | `between` | `{ age: { $between: [18, 65] } }``['age', 'between', [18, 65]]` |
90+
91+
#### Complex Filter Examples
92+
93+
**Multiple conditions** are combined with `'and'`:
94+
95+
```typescript
96+
// Input
97+
$filter: {
98+
age: { $gte: 18, $lte: 65 },
99+
status: 'active'
100+
}
101+
102+
// Converted to AST
103+
['and',
104+
['age', '>=', 18],
105+
['age', '<=', 65],
106+
['status', '=', 'active']
107+
]
108+
```
109+
71110
### Query Parameter Mapping
72111

73112
The adapter automatically converts ObjectUI query parameters (OData-style) to ObjectStack protocol:
74113

75114
| ObjectUI ($) | ObjectStack | Description |
76115
|--------------|-------------|-------------|
77116
| `$select` | `select` | Field selection |
78-
| `$filter` | `filters` | Filter conditions |
117+
| `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
79118
| `$orderby` | `sort` | Sort order |
80119
| `$skip` | `skip` | Pagination offset |
81120
| `$top` | `top` | Limit records |
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import { ObjectStackClient } from '@objectstack/client';
9+
import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
10+
/**
11+
* ObjectStack Data Source Adapter
12+
*
13+
* Bridges the ObjectStack Client SDK with the ObjectUI DataSource interface.
14+
* This allows Object UI applications to seamlessly integrate with ObjectStack
15+
* backends while maintaining the universal DataSource abstraction.
16+
*
17+
* @example
18+
* ```typescript
19+
* import { ObjectStackAdapter } from '@object-ui/core/adapters';
20+
*
21+
* const dataSource = new ObjectStackAdapter({
22+
* baseUrl: 'https://api.example.com',
23+
* token: 'your-api-token'
24+
* });
25+
*
26+
* const users = await dataSource.find('users', {
27+
* $filter: { status: 'active' },
28+
* $top: 10
29+
* });
30+
* ```
31+
*/
32+
export declare class ObjectStackAdapter<T = any> implements DataSource<T> {
33+
private client;
34+
private connected;
35+
constructor(config: {
36+
baseUrl: string;
37+
token?: string;
38+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
39+
});
40+
/**
41+
* Ensure the client is connected to the server.
42+
* Call this before making requests or it will auto-connect on first request.
43+
*/
44+
connect(): Promise<void>;
45+
/**
46+
* Find multiple records with query parameters.
47+
* Converts OData-style params to ObjectStack query options.
48+
*/
49+
find(resource: string, params?: QueryParams): Promise<QueryResult<T>>;
50+
/**
51+
* Find a single record by ID.
52+
*/
53+
findOne(resource: string, id: string | number, _params?: QueryParams): Promise<T | null>;
54+
/**
55+
* Create a new record.
56+
*/
57+
create(resource: string, data: Partial<T>): Promise<T>;
58+
/**
59+
* Update an existing record.
60+
*/
61+
update(resource: string, id: string | number, data: Partial<T>): Promise<T>;
62+
/**
63+
* Delete a record.
64+
*/
65+
delete(resource: string, id: string | number): Promise<boolean>;
66+
/**
67+
* Bulk operations (optional implementation).
68+
*/
69+
bulk(resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]>;
70+
/**
71+
* Convert ObjectUI QueryParams to ObjectStack QueryOptions.
72+
* Maps OData-style conventions to ObjectStack conventions.
73+
*/
74+
private convertQueryParams;
75+
/**
76+
* Get access to the underlying ObjectStack client for advanced operations.
77+
*/
78+
getClient(): ObjectStackClient;
79+
}
80+
/**
81+
* Factory function to create an ObjectStack data source.
82+
*
83+
* @example
84+
* ```typescript
85+
* const dataSource = createObjectStackAdapter({
86+
* baseUrl: process.env.API_URL,
87+
* token: process.env.API_TOKEN
88+
* });
89+
* ```
90+
*/
91+
export declare function createObjectStackAdapter<T = any>(config: {
92+
baseUrl: string;
93+
token?: string;
94+
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
95+
}): DataSource<T>;
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import { ObjectStackClient } from '@objectstack/client';
9+
import { convertFiltersToAST } from '../utils/filter-converter';
10+
/**
11+
* ObjectStack Data Source Adapter
12+
*
13+
* Bridges the ObjectStack Client SDK with the ObjectUI DataSource interface.
14+
* This allows Object UI applications to seamlessly integrate with ObjectStack
15+
* backends while maintaining the universal DataSource abstraction.
16+
*
17+
* @example
18+
* ```typescript
19+
* import { ObjectStackAdapter } from '@object-ui/core/adapters';
20+
*
21+
* const dataSource = new ObjectStackAdapter({
22+
* baseUrl: 'https://api.example.com',
23+
* token: 'your-api-token'
24+
* });
25+
*
26+
* const users = await dataSource.find('users', {
27+
* $filter: { status: 'active' },
28+
* $top: 10
29+
* });
30+
* ```
31+
*/
32+
export class ObjectStackAdapter {
33+
constructor(config) {
34+
Object.defineProperty(this, "client", {
35+
enumerable: true,
36+
configurable: true,
37+
writable: true,
38+
value: void 0
39+
});
40+
Object.defineProperty(this, "connected", {
41+
enumerable: true,
42+
configurable: true,
43+
writable: true,
44+
value: false
45+
});
46+
this.client = new ObjectStackClient(config);
47+
}
48+
/**
49+
* Ensure the client is connected to the server.
50+
* Call this before making requests or it will auto-connect on first request.
51+
*/
52+
async connect() {
53+
if (!this.connected) {
54+
await this.client.connect();
55+
this.connected = true;
56+
}
57+
}
58+
/**
59+
* Find multiple records with query parameters.
60+
* Converts OData-style params to ObjectStack query options.
61+
*/
62+
async find(resource, params) {
63+
await this.connect();
64+
const queryOptions = this.convertQueryParams(params);
65+
const result = await this.client.data.find(resource, queryOptions);
66+
return {
67+
data: result.value,
68+
total: result.count,
69+
page: params?.$skip ? Math.floor(params.$skip / (params.$top || 20)) + 1 : 1,
70+
pageSize: params?.$top,
71+
hasMore: result.value.length === params?.$top,
72+
};
73+
}
74+
/**
75+
* Find a single record by ID.
76+
*/
77+
async findOne(resource, id, _params) {
78+
await this.connect();
79+
try {
80+
const record = await this.client.data.get(resource, String(id));
81+
return record;
82+
}
83+
catch (error) {
84+
// If record not found, return null instead of throwing
85+
if (error?.status === 404) {
86+
return null;
87+
}
88+
throw error;
89+
}
90+
}
91+
/**
92+
* Create a new record.
93+
*/
94+
async create(resource, data) {
95+
await this.connect();
96+
return this.client.data.create(resource, data);
97+
}
98+
/**
99+
* Update an existing record.
100+
*/
101+
async update(resource, id, data) {
102+
await this.connect();
103+
return this.client.data.update(resource, String(id), data);
104+
}
105+
/**
106+
* Delete a record.
107+
*/
108+
async delete(resource, id) {
109+
await this.connect();
110+
const result = await this.client.data.delete(resource, String(id));
111+
return result.success;
112+
}
113+
/**
114+
* Bulk operations (optional implementation).
115+
*/
116+
async bulk(resource, operation, data) {
117+
await this.connect();
118+
switch (operation) {
119+
case 'create':
120+
return this.client.data.createMany(resource, data);
121+
case 'delete': {
122+
const ids = data.map(item => item.id).filter(Boolean);
123+
await this.client.data.deleteMany(resource, ids);
124+
return [];
125+
}
126+
case 'update': {
127+
// For update, we need to handle each record individually
128+
// or use the batch update if all records get the same changes
129+
const results = await Promise.all(data.map(item => this.client.data.update(resource, String(item.id), item)));
130+
return results;
131+
}
132+
default:
133+
throw new Error(`Unsupported bulk operation: ${operation}`);
134+
}
135+
}
136+
/**
137+
* Convert ObjectUI QueryParams to ObjectStack QueryOptions.
138+
* Maps OData-style conventions to ObjectStack conventions.
139+
*/
140+
convertQueryParams(params) {
141+
if (!params)
142+
return {};
143+
const options = {};
144+
// Selection
145+
if (params.$select) {
146+
options.select = params.$select;
147+
}
148+
// Filtering - convert to ObjectStack FilterNode AST format
149+
if (params.$filter) {
150+
options.filters = convertFiltersToAST(params.$filter);
151+
}
152+
// Sorting - convert to ObjectStack format
153+
if (params.$orderby) {
154+
const sortArray = Object.entries(params.$orderby).map(([field, order]) => {
155+
return order === 'desc' ? `-${field}` : field;
156+
});
157+
options.sort = sortArray;
158+
}
159+
// Pagination
160+
if (params.$skip !== undefined) {
161+
options.skip = params.$skip;
162+
}
163+
if (params.$top !== undefined) {
164+
options.top = params.$top;
165+
}
166+
return options;
167+
}
168+
/**
169+
* Get access to the underlying ObjectStack client for advanced operations.
170+
*/
171+
getClient() {
172+
return this.client;
173+
}
174+
}
175+
/**
176+
* Factory function to create an ObjectStack data source.
177+
*
178+
* @example
179+
* ```typescript
180+
* const dataSource = createObjectStackAdapter({
181+
* baseUrl: process.env.API_URL,
182+
* token: process.env.API_TOKEN
183+
* });
184+
* ```
185+
*/
186+
export function createObjectStackAdapter(config) {
187+
return new ObjectStackAdapter(config);
188+
}

packages/core/src/adapters/objectstack-adapter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
1010
import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
11+
import { convertFiltersToAST } from '../utils/filter-converter';
1112

1213
/**
1314
* ObjectStack Data Source Adapter
@@ -159,9 +160,9 @@ export class ObjectStackAdapter<T = any> implements DataSource<T> {
159160
options.select = params.$select;
160161
}
161162

162-
// Filtering - convert object to simple map
163+
// Filtering - convert to ObjectStack FilterNode AST format
163164
if (params.$filter) {
164-
options.filters = params.$filter;
165+
options.filters = convertFiltersToAST(params.$filter);
165166
}
166167

167168
// Sorting - convert to ObjectStack format

packages/core/src/builder/schema-builder.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* This source code is licensed under the MIT license found in the
66
* LICENSE file in the root directory of this source tree.
77
*/
8-
98
/**
109
* @object-ui/core - Schema Builder
1110
*

0 commit comments

Comments
 (0)