Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .changeset/6213-core-adapters-readme-owns-its-directory.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
---
'@object-ui/core': patch
'@object-ui/data-objectstack': patch
---

`packages/core/src/adapters/README.md` now documents the adapters that are actually in that
directory, and the ObjectStack material it carried moved to the package that owns the behaviour
(objectui#6213). Both files ship to consumers — `@object-ui/core` publishes its `src/`, and a
README rides every tarball — so this was published documentation describing the wrong package.

The page had been left behind when the ObjectStack adapter moved out to
`@object-ui/data-objectstack`: its headings, feature list, filter-operator table and
query-parameter table were all about that adapter, and its one-entry "Available Adapters" list
told a reader Object UI has exactly one adapter and that it comes from `@object-ui/core`.
`ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction` and
`emulateBatchTransaction` — the five exports that directory really ships — were named nowhere.

- **`@object-ui/core`**: the page now opens with what the directory holds, gives each export a
usage snippet and a `provider` mapping, and points at `@object-ui/data-objectstack` for the
ObjectStack adapter. `## Creating Custom Adapters` is unchanged — it is the one section that was
always about this directory.
- **`@object-ui/data-objectstack`**: gains a `## Query Translation` section carrying the
filter-operator and query-parameter mapping tables, the AST conversion example and the sorting
example. That material existed **only** in the `core` copy — this package's README documented
query translation as a single feature bullet — so it is ported, not dropped.

No runtime behaviour changes; the duplicate copy of one package's documentation living under
another package is what goes away.
206 changes: 98 additions & 108 deletions packages/core/src/adapters/README.md
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,132 @@
# Data Source Adapters

This directory contains data source adapters that bridge various backend protocols with the ObjectUI DataSource interface.
This directory holds the `DataSource` adapters that ship **inside
`@object-ui/core`** — the backend-agnostic ones, with no external SDK
dependency. They are what a schema's `viewData` block resolves to at runtime.

## ObjectStack Adapter
> **Looking for the ObjectStack adapter?** It is not in this directory.
> `ObjectStackAdapter` / `createObjectStackAdapter` ship as their own package,
> **[`@object-ui/data-objectstack`](../../../data-objectstack/README.md)**,
> whose README owns the ObjectStack material — connection handling, metadata
> caching, error hierarchy, and the filter-operator and query-parameter
> translation tables.

The `ObjectStackAdapter` provides seamless integration with ObjectStack Protocol servers.
## Available Adapters

Every export below is re-exported from the package root, so consumers import
them from `@object-ui/core`.

| Export | Kind | Role |
| --- | --- | --- |
| `ApiDataSource` | class | `provider: 'api'` — raw HTTP against the `HttpRequest` configs carried in `ViewData` |
| `ValueDataSource` | class | `provider: 'value'` — an in-memory array; no network at all |
| `resolveDataSource` | function | builds the adapter a `ViewData` config asks for (`object` / `api` / `value`) |
| `runBatchTransaction` / `emulateBatchTransaction` | functions | ordered cross-object save — the adapter's own `batchTransaction` when it has one, a sequential non-atomic emulation when it does not |

### Features
Backend-specific adapters live in their own packages rather than here; today
that is `@object-ui/data-objectstack`.

- ✅ Full CRUD operations (find, findOne, create, update, delete)
- ✅ Bulk operations (createMany, updateMany, deleteMany)
- ✅ Auto-discovery of server capabilities
- ✅ Query parameter translation (OData-style → ObjectStack)
- ✅ Proper error handling
- ✅ TypeScript types
### `ApiDataSource`

### Usage
For `provider: 'api'`. The endpoint comes from the `HttpRequest` configs, not
from the `resource` argument — `find`/`findOne` use `read`, and
`create`/`update`/`delete` use `write` (falling back to `read` when `write` is
absent). `QueryParams` are flattened onto the query string.

```typescript
import { createObjectStackAdapter } from '@object-ui/core';
import { ApiDataSource } from '@object-ui/core';

// Create the adapter
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.example.com',
token: 'your-auth-token', // Optional
const dataSource = new ApiDataSource({
read: { url: 'https://api.example.com/users', method: 'GET' },
write: { url: 'https://api.example.com/users' },
defaultHeaders: { Authorization: 'Bearer …' },
// fetch: customFetch, // optional; defaults to globalThis.fetch
});

// Use it with ObjectUI components
const schema = {
type: 'data-table',
dataSource,
resource: 'users',
columns: [
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
]
};
const { data, total } = await dataSource.find('users', { $top: 20 });
```

### Advanced Usage
A generic HTTP endpoint exposes no metadata, so `getObjectSchema()` returns a
minimal stub (`{ name, fields: {} }`) and `getView()` / `getApp()` return
`null` — enough that schema-dependent components do not crash.

```typescript
import { ObjectStackAdapter } from '@object-ui/core';
### `ValueDataSource`

const adapter = new ObjectStackAdapter({
baseUrl: 'https://api.example.com',
token: process.env.API_TOKEN,
fetch: customFetch // Optional: use custom fetch (e.g., Next.js fetch)
});
For `provider: 'value'`. Everything runs against an in-memory array, which is
deep-cloned on construction so the caller's array is never mutated. Useful for
static content, fixtures, and previews.

// Manually connect (optional, auto-connects on first request)
await adapter.connect();

// Query with filters (MongoDB-like operators)
const result = await adapter.find('tasks', {
$filter: {
status: 'active',
priority: { $gte: 2 }
},
$orderby: { createdAt: 'desc' },
$top: 20,
$skip: 0
```typescript
import { ValueDataSource } from '@object-ui/core';

const dataSource = new ValueDataSource({
items: [
{ id: '1', name: 'Alice', age: 30 },
{ id: '2', name: 'Bob', age: 24 },
],
// idField: 'id', // optional; defaults to `id`, then `_id`
});

// Access the underlying client for advanced operations
const client = adapter.getClient();
const metadata = await client.meta.getObject('task');
const { data, total } = await dataSource.find('people', {
$filter: { age: { $gte: 25 } },
$orderby: { name: 'asc' },
});
```

### Filter Conversion

The adapter automatically converts MongoDB-like filter operators to **ObjectStack FilterNode AST format**. This ensures compatibility with the latest ObjectStack Protocol (v0.1.2+).

#### Supported Filter Operators
It implements `$filter` (both MongoDB-style objects and FilterNode AST arrays),
`$search`, `$orderby`, `$skip`, `$top` and `$select` locally, plus `bulk()`,
`aggregate()` and `onMutation()`. `getAll()` returns a cloned snapshot and
`count` the current length.

| MongoDB Operator | ObjectStack Operator | Example |
|------------------|---------------------|---------|
| `$eq` or simple value | `=` | `{ status: 'active' }` → `['status', '=', 'active']` |
| `$ne` | `!=` | `{ status: { $ne: 'archived' } }` → `['status', '!=', 'archived']` |
| `$gt` | `>` | `{ age: { $gt: 18 } }` → `['age', '>', 18]` |
| `$gte` | `>=` | `{ age: { $gte: 18 } }` → `['age', '>=', 18]` |
| `$lt` | `<` | `{ age: { $lt: 65 } }` → `['age', '<', 65]` |
| `$lte` | `<=` | `{ age: { $lte: 65 } }` → `['age', '<=', 65]` |
| `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }` → `['status', 'in', ['active', 'pending']]` |
| `$nin` / `$notin` | `notin` | `{ status: { $nin: ['archived'] } }` → `['status', 'notin', ['archived']]` |
| `$contains` / `$regex` | `contains` | `{ name: { $contains: 'John' } }` → `['name', 'contains', 'John']` |
| `$startswith` | `startswith` | `{ email: { $startswith: 'admin' } }` → `['email', 'startswith', 'admin']` |
| `$between` | `between` | `{ age: { $between: [18, 65] } }` → `['age', 'between', [18, 65]]` |
### `resolveDataSource`

#### Complex Filter Examples

**Multiple conditions** are combined with `'and'`:
Turns a `ViewData` config into a concrete adapter. This is the function a
renderer calls; components do not branch on `provider` themselves.

```typescript
// Input
$filter: {
age: { $gte: 18, $lte: 65 },
status: 'active'
}
import { resolveDataSource } from '@object-ui/core';

// Converted to AST
['and',
['age', '>=', 18],
['age', '<=', 65],
['status', '=', 'active']
]
const dataSource = resolveDataSource(
{ provider: 'api', read: { url: '/api/users' } },
contextDataSource, // used for `provider: 'object'`, and as the fallback
);
```

### Query Parameter Mapping

The adapter automatically converts ObjectUI query parameters (OData-style) to ObjectStack protocol:
| `viewData.provider` | Result |
| --- | --- |
| `'object'` | the `fallback` — the `DataSource` from context, typically `ObjectStackAdapter` |
| `'api'` | a new `ApiDataSource` built from `read` / `write` |
| `'value'` | a new `ValueDataSource` over `items` |
| unknown, or no `viewData` | the `fallback`, else `null` |

| ObjectUI ($) | ObjectStack | Description |
|--------------|-------------|-------------|
| `$select` | `select` | Field selection |
| `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
| `$orderby` | `sort` | Sort order |
| `$skip` | `skip` | Pagination offset |
| `$top` | `top` | Limit records |
### `runBatchTransaction` / `emulateBatchTransaction`

### Example with Sorting
The single entry point for an ordered **cross-object** save (the master-detail
case). `runBatchTransaction` calls the adapter's native `batchTransaction` when
it implements one — `ObjectStackAdapter` does, and against a backend
advertising `capabilities.transactionalBatch` that is a real server
transaction — and otherwise falls back to `emulateBatchTransaction`. Callers
stay ignorant of which one ran.

```typescript
// OData-style
await dataSource.find('users', {
$orderby: {
createdAt: 'desc',
name: 'asc'
}
});
import { runBatchTransaction } from '@object-ui/core';

// Converted to ObjectStack: ['-createdAt', 'name']
// `{ $ref: 0 }` resolves to the id minted by operation 0 (the parent).
await runBatchTransaction(dataSource, [
{ object: 'invoice', action: 'create', data: { no: 'INV-1' } },
{ object: 'invoice_line', action: 'create', data: { invoice: { $ref: 0 }, amount: 10 } },
]);
```

⚠️ The emulation is **not** atomic. It runs the operations in order and, on
failure, best-effort deletes the records it created (children before parent)
before rethrowing; updates and deletes that already ran cannot be undone, and a
create's side effects (hooks, rollups, webhooks) are not undone by a later
delete. It exists so a save is still possible against a backend without server
atomicity — see
[`@object-ui/data-objectstack`](../../../data-objectstack/README.md#cross-object-atomic-batch-batchtransaction)
for the capability negotiation that decides which path is taken.

## Creating Custom Adapters

To create a custom adapter, implement the `DataSource<T>` interface:
Expand DownExpand Up@@ -168,13 +162,9 @@ export class MyCustomAdapter<T = any> implements DataSource<T> {
}
```

## Available Adapters

- **ObjectStackAdapter** - For ObjectStack Protocol servers
- More adapters coming soon (REST, GraphQL, Supabase, Firebase, etc.)

## Related Packages

- `@objectstack/client` - ObjectStack Client SDK
- `@objectstack/spec` - ObjectStack Protocol Specification
- `@object-ui/types` - ObjectUI Type Definitions
- `@object-ui/types` — the `DataSource`, `QueryParams` and `ViewData` definitions these adapters implement
- `@object-ui/data-objectstack` — the ObjectStack Protocol adapter, and the owner of the ObjectStack documentation
- `@objectstack/client` — ObjectStack Client SDK (a dependency of `@object-ui/data-objectstack`, not of `@object-ui/core`)
- `@objectstack/spec` — ObjectStack Protocol Specification
90 changes: 90 additions & 0 deletions packages/data-objectstack/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,6 +71,96 @@ const dataSource = createObjectStackAdapter({
- ✅ **Auto-Reconnect**: Automatic reconnection with exponential backoff on connection failures.
- ✅ **Batch Progress**: Progress events for tracking bulk operation status.

## Query Translation

`find()` accepts Object UI's OData-style `QueryParams` and translates them into
ObjectStack's native query format, so a schema never has to be written in the
protocol's own shape:

```typescript
// Query with filters (MongoDB-like operators)
const result = await dataSource.find('tasks', {
$filter: {
status: 'active',
priority: { $gte: 2 },
},
$orderby: { createdAt: 'desc' },
$top: 20,
$skip: 0,
});

// Escape hatch: reach the underlying ObjectStack client for anything
// the DataSource interface does not cover
const client = dataSource.getClient();
const metadata = await client.meta.getObject('task');
```

### Query Parameter Mapping

| Object UI (`$`) | ObjectStack | Description |
|--------------|-------------|-------------|
| `$select` | `select` | Field selection |
| `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
| `$orderby` | `sort` | Sort order |
| `$skip` | `skip` | Pagination offset |
| `$top` | `top` | Limit records |

### Filter Conversion

The adapter converts MongoDB-like filter operators into **ObjectStack FilterNode
AST format**. This is what keeps it compatible with the ObjectStack Protocol
(v0.1.2+).

#### Supported Filter Operators

| MongoDB Operator | ObjectStack Operator | Example |
|------------------|---------------------|---------|
| `$eq` or simple value | `=` | `{ status: 'active' }` → `['status', '=', 'active']` |
| `$ne` | `!=` | `{ status: { $ne: 'archived' } }` → `['status', '!=', 'archived']` |
| `$gt` | `>` | `{ age: { $gt: 18 } }` → `['age', '>', 18]` |
| `$gte` | `>=` | `{ age: { $gte: 18 } }` → `['age', '>=', 18]` |
| `$lt` | `<` | `{ age: { $lt: 65 } }` → `['age', '<', 65]` |
| `$lte` | `<=` | `{ age: { $lte: 65 } }` → `['age', '<=', 65]` |
| `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }` → `['status', 'in', ['active', 'pending']]` |
| `$nin` / `$notin` | `notin` | `{ status: { $nin: ['archived'] } }` → `['status', 'notin', ['archived']]` |
| `$contains` / `$regex` | `contains` | `{ name: { $contains: 'John' } }` → `['name', 'contains', 'John']` |
| `$startswith` | `startswith` | `{ email: { $startswith: 'admin' } }` → `['email', 'startswith', 'admin']` |
| `$between` | `between` | `{ age: { $between: [18, 65] } }` → `['age', 'between', [18, 65]]` |

#### Complex Filter Examples

**Multiple conditions** are combined with `'and'`:

```typescript
// Input
const $filter = {
age: { $gte: 18, $lte: 65 },
status: 'active',
};

// Converted to AST
const ast = [
'and',
['age', '>=', 18],
['age', '<=', 65],
['status', '=', 'active'],
];
```

### Sorting

```typescript
// OData-style
await dataSource.find('users', {
$orderby: {
createdAt: 'desc',
name: 'asc',
},
});

// Converted to ObjectStack: ['-createdAt', 'name']
```

## Metadata Caching

The adapter includes built-in metadata caching to improve performance when fetching schemas:
Expand Down