Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ObjectStack Protocol — Road Map

> **Last Updated:** 2026-02-28
> **Last Updated:** 2026-03-27
> **Current Version:** v3.0.11
> **Status:** Protocol Specification Complete · Runtime Implementation In Progress

Expand Down Expand Up @@ -129,6 +129,7 @@ This strategy ensures rapid iteration while maintaining a clear path to producti
| Dispatcher async `getService` bug fix | ✅ | All `getService`/`getObjectQLService` calls in `http-dispatcher.ts` now properly `await` async service factories. Covers `handleAnalytics`, `handleAuth`, `handleStorage`, `handleAutomation`, `handleMetadata`, `handleUi`, `handlePackages`. All 7 framework adapters (Express, Fastify, Hono, Next.js, SvelteKit, NestJS, Nuxt) updated to use `getServiceAsync()` for auth service resolution. |
| Analytics `getMetadata` → `getMeta` naming fix | ✅ | `handleAnalytics` in `http-dispatcher.ts` called `getMetadata({ request })` which didn't match the `IAnalyticsService` contract (`getMeta(cubeName?: string)`). Renamed to `getMeta()` and aligned call signature. Updated test mocks accordingly. |
| Unified ID/audit/tenant field naming | ✅ | Eliminated `_id`/`modified_at`/`modified_by`/`space_id` from protocol layer. All protocol code uses `id`, `updated_at`, `updated_by`, `tenant_id` per `SystemFieldName`. Storage-layer (NoSQL driver internals) retains `_id` for MongoDB/Mingo compat. |
| **Query syntax canonical unification** | ✅ | All layers (Client SDK, React Hooks, Studio QueryBuilder, HTTP Dispatcher, docs) unified to Spec canonical field names (`where`/`fields`/`orderBy`/`limit`/`offset`/`expand`). `QueryOptionsV2` interface added. Legacy names (`filter`/`select`/`sort`/`top`/`skip`) accepted with `@deprecated` markers. HTTP Dispatcher normalizes transport params to canonical QueryAST before broker dispatch. |

---

Expand Down
12 changes: 6 additions & 6 deletions apps/studio/src/components/QueryBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,26 @@ function buildQueryJson(objectName: string, state: QueryState): Record<string, u
const query: Record<string, unknown> = { object: objectName };

if (state.selectedFields.length > 0) {
query.select = state.selectedFields;
query.fields = state.selectedFields;
}

if (state.filters.length > 0) {
query.filters = state.filters.map((f) => ({
query.where = state.filters.map((f) => ({
field: f.field,
operator: f.operator,
value: f.value,
}));
}

if (state.sorts.length > 0) {
query.sort = state.sorts.map((s) => ({
query.orderBy = state.sorts.map((s) => ({
field: s.field,
direction: s.direction,
order: s.direction,
}));
}

if (state.limit > 0) query.top = state.limit;
if (state.offset > 0) query.skip = state.offset;
if (state.limit > 0) query.limit = state.limit;
if (state.offset > 0) query.offset = state.offset;

return query;
}
Expand Down
41 changes: 23 additions & 18 deletions content/docs/guides/client-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ async function main() {

// 3. Query data
const tasks = await client.data.find('todo_task', {
select: ['subject', 'priority'],
filters: ['priority', '>=', 2],
sort: ['-priority'],
top: 10
fields: ['subject', 'priority'],
where: { priority: { $gte: 2 } },
orderBy: ['-priority'],
limit: 10
Comment on lines +47 to +50
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example uses client.data.find() with where: { priority: { $gte: 2 } }, but the current find() implementation serializes object-valued filters as String(value) in flat query params, which results in priority=[object Object]. Either switch the example to client.data.query() for operator-based filters, or update find() to JSON-encode operator objects (e.g. via the filter param).

Copilot uses AI. Check for mistakes.
});

// 4. Create a record
Expand Down Expand Up @@ -161,11 +161,11 @@ const listView = await client.meta.getView('account', 'list');
```typescript
// Find with query options
const accounts = await client.data.find('account', {
select: ['name', 'industry', 'revenue'],
filters: ['industry', '=', 'Technology'],
sort: ['-revenue'],
top: 20,
skip: 0,
fields: ['name', 'industry', 'revenue'],
where: { industry: 'Technology' },
orderBy: ['-revenue'],
limit: 20,
offset: 0,
});

// Get by ID
Expand Down Expand Up @@ -359,15 +359,20 @@ const filter = createFilter()

## Query Options

The `find` method accepts an options object:
The `find` method accepts an options object with **canonical** (recommended) field names:

| Property | Type | Description | Example |
|:---------|:-----|:------------|:--------|
| `select` | `string[]` | Fields to retrieve | `['name', 'email']` |
| `filters` | `Map` or `AST` | Filter criteria | `['status', '=', 'active']` |
| `sort` | `string` or `string[]` | Sort order | `['-created_at']` |
| `top` | `number` | Limit records | `20` |
| `skip` | `number` | Offset for pagination | `0` |
| `where` | `Object` or `FilterCondition` | Filter conditions (WHERE clause) | `{ status: 'active' }` |
| `fields` | `string[]` | Fields to retrieve (SELECT) | `['name', 'email']` |
| `orderBy` | `string` or `string[]` or `SortNode[]` | Sort order (ORDER BY) | `['-created_at']` |
| `limit` | `number` | Max records to return (LIMIT) | `20` |
| `offset` | `number` | Records to skip (OFFSET) | `0` |
| `expand` | `Record<string, any>` or `string[]` | Relation loading (JOIN) | `{ owner: {} }` |

<Callout type="warn">
**Deprecated aliases:** The following legacy field names are still accepted for backward compatibility but will be removed in a future major version: `select` → `fields`, `filter`/`filters` → `where`, `sort` → `orderBy`, `top` → `limit`, `skip` → `offset`.
</Callout>

### Batch Options

Expand Down Expand Up @@ -457,9 +462,9 @@ function App() {
// 2. Use hooks in child components
function AccountList() {
const { data, isLoading, error, refetch } = useQuery('account', {
filters: ['status', '=', 'active'],
sort: ['-created_at'],
top: 20,
where: { status: 'active' },
orderBy: ['-created_at'],
limit: 20,
});

if (isLoading) return <div>Loading...</div>;
Expand Down
24 changes: 16 additions & 8 deletions content/docs/protocol/objectos/http-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,22 @@ GET /{base_path}/{object_name}

**Query Parameters:**

| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| `select` | string | Comma-separated field list | `id,name,status` |
| `filter` | JSON | Filter criteria (see Filtering section) | `{"status":"active"}` |
| `sort` | string | Sort fields (prefix `-` for desc) | `-created_at,name` |
| `page` | number | Page number (1-indexed) | `2` |
| `per_page` | number | Items per page (max from limits) | `50` |
| `include` | string | Related objects to embed | `assignee,comments` |
| Parameter | Type | Description | Canonical Equivalent | Example |
|-----------|------|-------------|---------------------|---------|
| `select` | string | Comma-separated field list | `fields` | `id,name,status` |
| `filter` | JSON | Filter criteria (see Filtering section) | `where` | `{"status":"active"}` |
| `sort` | string | Sort fields (prefix `-` for desc) | `orderBy` | `-created_at,name` |
| `top` | number | Max records to return | `limit` | `25` |
| `skip` | number | Records to skip (offset) | `offset` | `50` |
| `expand` | string | Related objects to embed | `expand` | `assignee,comments` |
| `page` | number | Page number (1-indexed) | — | `2` |
| `per_page` | number | Items per page (max from limits) | — | `50` |

Comment on lines +138 to +140
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page and per_page are still documented here, but runtime query normalization only supports offset-based pagination (top/skiplimit/offset) and there’s no handling for page/per_page. As-is, these would be treated as implicit field filters rather than pagination. Either document a mapping to limit/offset or remove these params from the table.

Copilot uses AI. Check for mistakes.
> **Transport → Protocol normalization:** The HTTP dispatcher normalizes transport-level
> parameter names to Spec canonical (QueryAST) field names before forwarding to the
> broker layer: `filter`→`where`, `select`→`fields`, `sort`→`orderBy`, `top`→`limit`,
> `skip`→`offset`. The deprecated `filters` (plural) parameter is also accepted and
> normalized to `where`.

**Example Request:**
```http
Expand Down
109 changes: 74 additions & 35 deletions packages/client-react/src/data-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,47 @@ import { useClient } from './context';

/**
* Query options for useQuery hook
*
* Supports both **canonical** (Spec protocol) and **legacy** field names.
* Canonical names are preferred; legacy names are accepted for backward
* compatibility and will be removed in a future major release.
*
* | Canonical | Legacy (deprecated) |
* |-----------|---------------------|
* | `where` | `filters` |
* | `fields` | `select` |
* | `orderBy` | `sort` |
* | `limit` | `top` |
* | `offset` | `skip` |
*/
export interface UseQueryOptions<T = any> {
/** Query AST or simplified query options */
query?: Partial<QueryAST>;
/** Simple field selection */

// ── Canonical (Spec protocol) field names ──────────────────────────
/** Filter conditions (WHERE clause). */
where?: FilterCondition;
/** Fields to retrieve (SELECT clause). */
fields?: string[];
/** Sort definition (ORDER BY clause). */
orderBy?: string | string[];
/** Maximum number of records to return (LIMIT). */
limit?: number;
/** Number of records to skip (OFFSET). */
offset?: number;

// ── Legacy field names (deprecated) ────────────────────────────────
/** @deprecated Use `fields` instead. */
select?: string[];
/** Simple filters */
/** @deprecated Use `where` instead. */
filters?: FilterCondition;
/** Sort configuration */
/** @deprecated Use `orderBy` instead. */
sort?: string | string[];
/** Limit results */
/** @deprecated Use `limit` instead. */
top?: number;
/** Skip results (for pagination) */
/** @deprecated Use `offset` instead. */
skip?: number;

/** Enable/disable automatic query execution */
enabled?: boolean;
/** Refetch interval in milliseconds */
Expand Down Expand Up @@ -60,9 +87,9 @@ export interface UseQueryResult<T = any> {
* ```tsx
* function TaskList() {
* const { data, isLoading, error, refetch } = useQuery('todo_task', {
* select: ['id', 'subject', 'priority'],
* sort: ['-created_at'],
* top: 20
* fields: ['id', 'subject', 'priority'],
* orderBy: ['-created_at'],
* limit: 20
* });
*
* if (isLoading) return <div>Loading...</div>;
Expand Down Expand Up @@ -91,17 +118,23 @@ export function useQuery<T = any>(

const {
query,
select,
filters,
sort,
top,
skip,
// Canonical names take precedence over legacy names
where, fields, orderBy, limit, offset,
// Legacy names (deprecated fallbacks)
select, filters, sort, top, skip,
enabled = true,
refetchInterval,
onSuccess,
onError
} = options;

// Resolve canonical vs legacy: canonical wins when both are provided
const resolvedFields = fields ?? select;
const resolvedWhere = where ?? filters;
const resolvedSort = orderBy ?? sort;
const resolvedLimit = limit ?? top;
const resolvedOffset = offset ?? skip;

const fetchData = useCallback(async (isRefetch = false) => {
if (!enabled) return;

Expand All @@ -119,13 +152,13 @@ export function useQuery<T = any>(
// Use advanced query API
result = await client.data.query<T>(object, query);
} else {
// Use simplified find API
// Use canonical QueryOptionsV2 for the find call
result = await client.data.find<T>(object, {
select,
filters: filters as any,
sort,
top,
skip
where: resolvedWhere as any,
fields: resolvedFields,
orderBy: resolvedSort,
limit: resolvedLimit,
offset: resolvedOffset,
});
}

Expand All @@ -139,7 +172,7 @@ export function useQuery<T = any>(
setIsLoading(false);
setIsRefetching(false);
}
}, [client, object, query, select, filters, sort, top, skip, enabled, onSuccess, onError]);
}, [client, object, query, resolvedFields, resolvedWhere, resolvedSort, resolvedLimit, resolvedOffset, enabled, onSuccess, onError]);

// Initial fetch and dependency-based refetch
useEffect(() => {
Expand Down Expand Up @@ -319,7 +352,7 @@ export function useMutation<TData = any, TVariables = any>(
/**
* Pagination options for usePagination hook
*/
export interface UsePaginationOptions<T = any> extends Omit<UseQueryOptions<T>, 'top' | 'skip'> {
export interface UsePaginationOptions<T = any> extends Omit<UseQueryOptions<T>, 'top' | 'skip' | 'limit' | 'offset'> {
/** Page size */
pageSize?: number;
/** Initial page (1-based) */
Expand Down Expand Up @@ -365,7 +398,7 @@ export interface UsePaginationResult<T = any> extends UseQueryResult<T> {
* hasPreviousPage
* } = usePagination('todo_task', {
* pageSize: 10,
* sort: ['-created_at']
* orderBy: ['-created_at']
* });
*
* return (
Expand All @@ -388,8 +421,8 @@ export function usePagination<T = any>(

const queryResult = useQuery<T>(object, {
...queryOptions,
top: pageSize,
skip: (page - 1) * pageSize
limit: pageSize,
offset: (page - 1) * pageSize
});

const totalCount = queryResult.data?.total || 0;
Expand Down Expand Up @@ -430,7 +463,7 @@ export function usePagination<T = any>(
/**
* Infinite query options for useInfiniteQuery hook
*/
export interface UseInfiniteQueryOptions<T = any> extends Omit<UseQueryOptions<T>, 'skip'> {
export interface UseInfiniteQueryOptions<T = any> extends Omit<UseQueryOptions<T>, 'skip' | 'offset'> {
/** Page size for each fetch */
pageSize?: number;
/** Get next page parameter */
Expand Down Expand Up @@ -473,7 +506,7 @@ export interface UseInfiniteQueryResult<T = any> {
* isFetchingNextPage
* } = useInfiniteQuery('todo_task', {
* pageSize: 20,
* sort: ['-created_at']
* orderBy: ['-created_at']
* });
*
* return (
Expand All @@ -498,14 +531,20 @@ export function useInfiniteQuery<T = any>(
pageSize = 20,
// getNextPageParam is reserved for future use
query,
select,
filters,
sort,
// Canonical names take precedence over legacy names
where, fields, orderBy,
// Legacy names (deprecated fallbacks)
select, filters, sort,
enabled = true,
onSuccess,
onError
} = options;

// Resolve canonical vs legacy: canonical wins
const resolvedFields = fields ?? select;
const resolvedWhere = where ?? filters;
const resolvedSort = orderBy ?? sort;

const [pages, setPages] = useState<PaginatedResult<T>[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
Expand All @@ -531,11 +570,11 @@ export function useInfiniteQuery<T = any>(
});
} else {
result = await client.data.find<T>(object, {
select,
filters: filters as any,
sort,
top: pageSize,
skip
where: resolvedWhere as any,
fields: resolvedFields,
orderBy: resolvedSort,
limit: pageSize,
offset: skip,
});
}

Expand All @@ -559,7 +598,7 @@ export function useInfiniteQuery<T = any>(
setIsLoading(false);
setIsFetchingNextPage(false);
}
}, [client, object, query, select, filters, sort, pageSize, onSuccess, onError]);
}, [client, object, query, resolvedFields, resolvedWhere, resolvedSort, pageSize, onSuccess, onError]);

// Initial fetch
useEffect(() => {
Expand Down
15 changes: 9 additions & 6 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,15 @@ Initializes the client by fetching the system discovery manifest from `/api/v1`.
- `setDefault(id, object)`: Set a view as default for an object.

### Query Options
The `find` method accepts an options object:
- `select`: Array of field names to retrieve.
- `filters`: Simple key-value map OR Filter AST `['field', 'op', 'value']`.
- `sort`: Sort string (`'name'`) or array `['-created_at', 'name']`.
- `top`: Limit number of records.
- `skip`: Offset for pagination.
The `find` method accepts an options object with canonical field names:
- `where`: Filter conditions (WHERE clause). Accepts object or FilterCondition AST.
- `fields`: Array of field names to retrieve (SELECT clause).
- `orderBy`: Sort definition — `'name'`, `['-created_at', 'name']`, or `SortNode[]`.
- `limit`: Maximum number of records to return (LIMIT).
- `offset`: Number of records to skip (OFFSET).
- `expand`: Relations to expand (JOIN / eager-load).
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This README section lists expand as a supported find() option, but the current data.find() implementation does not serialize expand at all. Update the README to match actual behavior, or add expand handling in find() so this documentation stays accurate.

Suggested change
- `expand`: Relations to expand (JOIN / eager-load).
- `expand`: Planned support for relation expansion (JOIN / eager-load). Currently ignored by the client.

Copilot uses AI. Check for mistakes.

> **Deprecated aliases** (accepted for backward compatibility): `select` → `fields`, `filter`/`filters` → `where`, `sort` → `orderBy`, `top` → `limit`, `skip` → `offset`.

### Batch Options
Batch operations support the following options:
Expand Down
Loading
Loading