From 47b639a983d980e7958c5739d42fdfc7e81b65e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 02:41:24 +0000 Subject: [PATCH] docs(core,data-objectstack): let each adapter README document its own directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/core/src/adapters/README.md` was entirely about an ObjectStack adapter that no longer lives in that directory. Its headings, feature list, filter-operator table, query-parameter table and one-entry "Available Adapters" list all described `@object-ui/data-objectstack`, while the exports that directory really ships — `ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction`, `emulateBatchTransaction` — were named nowhere. `packages/core` publishes its `src/`, so this shipped to consumers. Measurement first, as the triage required: `packages/data-objectstack`'s own README already carries the setup, features, error hierarchy and API reference, but its query translation is one feature bullet — the operator and parameter mapping tables exist ONLY in the `core` copy. So this is route A as a port, not route B: the tables move to the package that owns the behaviour rather than being dropped. `## Creating Custom Adapters` is unchanged — the one section always about this directory, importing only from `@object-ui/types`. Part of #6213 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- ...core-adapters-readme-owns-its-directory.md | 28 +++ packages/core/src/adapters/README.md | 206 +++++++++--------- packages/data-objectstack/README.md | 90 ++++++++ 3 files changed, 216 insertions(+), 108 deletions(-) create mode 100644 .changeset/6213-core-adapters-readme-owns-its-directory.md diff --git a/.changeset/6213-core-adapters-readme-owns-its-directory.md b/.changeset/6213-core-adapters-readme-owns-its-directory.md new file mode 100644 index 0000000000..2f152e2974 --- /dev/null +++ b/.changeset/6213-core-adapters-readme-owns-its-directory.md @@ -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. diff --git a/packages/core/src/adapters/README.md b/packages/core/src/adapters/README.md index e2befe801b..7392fa5cd4 100644 --- a/packages/core/src/adapters/README.md +++ b/packages/core/src/adapters/README.md @@ -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` interface: @@ -168,13 +162,9 @@ export class MyCustomAdapter implements DataSource { } ``` -## 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 diff --git a/packages/data-objectstack/README.md b/packages/data-objectstack/README.md index a1c99c134f..99c9d28dde 100644 --- a/packages/data-objectstack/README.md +++ b/packages/data-objectstack/README.md @@ -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: