@object-ui/data-objectstack 17.5.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2311 -0
- package/README.md +557 -11
- package/dist/index.cjs +1663 -463
- package/dist/index.d.cts +1780 -518
- package/dist/index.d.ts +1780 -518
- package/dist/index.js +1648 -457
- package/package.json +9 -9
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/aggregate-capability.test.ts +0 -177
- package/src/apiAccessDenied.test.ts +0 -233
- package/src/appAccessProbe.test.ts +0 -147
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/deleteView.homes.test.ts +0 -389
- package/src/deleteViewContract.types.test.ts +0 -246
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -135
- package/src/getObjectSchema.test.ts +0 -145
- package/src/httpFailureLogging.test.ts +0 -109
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -4442
- package/src/integration.ts +0 -200
- package/src/listViewOverrides.test.ts +0 -241
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.get-envelope.test.ts +0 -171
- package/src/metadata-client.saveAdvisories.test.ts +0 -226
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -1046
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -589
- package/src/onSaveAdvisory.test.ts +0 -311
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -543
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -326
- package/src/studio.ts +0 -152
- package/src/updateView.draft.test.ts +0 -242
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -229
- package/src/validation-error.test.ts +0 -94
- package/src/viewCacheInvalidation.pin.test.ts +0 -260
- package/src/viewInvalidationSeam.pin.test.ts +0 -162
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ npm install @object-ui/data-objectstack
|
|
|
23
23
|
```typescript
|
|
24
24
|
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
25
25
|
import { SchemaRenderer } from '@object-ui/react';
|
|
26
|
+
import type { DeclaredNode } from '@object-ui/types';
|
|
27
|
+
|
|
28
|
+
declare const mySchema: DeclaredNode;
|
|
26
29
|
|
|
27
30
|
// 1. Create the adapter
|
|
28
31
|
const dataSource = createObjectStackAdapter({
|
|
@@ -44,6 +47,8 @@ function App() {
|
|
|
44
47
|
### Advanced Configuration
|
|
45
48
|
|
|
46
49
|
```typescript
|
|
50
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
51
|
+
|
|
47
52
|
const dataSource = createObjectStackAdapter({
|
|
48
53
|
baseUrl: 'https://api.example.com',
|
|
49
54
|
token: 'your-api-token',
|
|
@@ -71,15 +76,367 @@ const dataSource = createObjectStackAdapter({
|
|
|
71
76
|
- ✅ **Auto-Reconnect**: Automatic reconnection with exponential backoff on connection failures.
|
|
72
77
|
- ✅ **Batch Progress**: Progress events for tracking bulk operation status.
|
|
73
78
|
|
|
79
|
+
## Query Translation
|
|
80
|
+
|
|
81
|
+
`find()` accepts Object UI's OData-style `QueryParams` and translates them into
|
|
82
|
+
ObjectStack's native query format, so a schema never has to be written in the
|
|
83
|
+
protocol's own shape:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
87
|
+
|
|
88
|
+
declare const dataSource: ObjectStackAdapter;
|
|
89
|
+
|
|
90
|
+
// Query with filters (MongoDB-like operators)
|
|
91
|
+
const result = await dataSource.find('tasks', {
|
|
92
|
+
$filter: {
|
|
93
|
+
status: 'active',
|
|
94
|
+
priority: { $gte: 2 },
|
|
95
|
+
},
|
|
96
|
+
$orderby: { createdAt: 'desc' },
|
|
97
|
+
$top: 20,
|
|
98
|
+
$skip: 0,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Escape hatch: reach the underlying ObjectStack client for anything
|
|
102
|
+
// the DataSource interface does not cover
|
|
103
|
+
const client = dataSource.getClient();
|
|
104
|
+
const metadata = await client.meta.getItem('object', 'task');
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Query Parameter Mapping
|
|
108
|
+
|
|
109
|
+
| Object UI (`$`) | ObjectStack | Description |
|
|
110
|
+
|--------------|-------------|-------------|
|
|
111
|
+
| `$select` | `select` | Field selection |
|
|
112
|
+
| `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
|
|
113
|
+
| `$orderby` | `sort` | Sort order |
|
|
114
|
+
| `$skip` | `skip` | Pagination offset |
|
|
115
|
+
| `$top` | `top` | Limit records |
|
|
116
|
+
|
|
117
|
+
### Filter Conversion
|
|
118
|
+
|
|
119
|
+
The adapter converts MongoDB-like filter operators into **ObjectStack FilterNode
|
|
120
|
+
AST format**. This is what keeps it compatible with the ObjectStack Protocol
|
|
121
|
+
(v0.1.2+).
|
|
122
|
+
|
|
123
|
+
#### Supported Filter Operators
|
|
124
|
+
|
|
125
|
+
Every row below is decided by `@object-ui/core`'s `convertFiltersToAST`, and
|
|
126
|
+
this package's `src/readme-filter-operator-table.test.ts` runs each worked
|
|
127
|
+
example through it on every test run, so a row cannot drift from the code
|
|
128
|
+
unnoticed again (objectui#8558). Every spelling below is the spec's own
|
|
129
|
+
(`FILTER_OPERATORS` in `@objectstack/spec`'s `data/filter.zod.ts`) and there are
|
|
130
|
+
no aliases: the four lowercase spellings this table used to list beside the
|
|
131
|
+
camelCase keys — `$notin`, `$notcontains`, `$startswith`, `$endswith` — were
|
|
132
|
+
retired by objectui#8568 and moved to the refused table below.
|
|
133
|
+
|
|
134
|
+
`$icontains` is the case-insensitive member of the `$contains` family, and its
|
|
135
|
+
ObjectStack spelling is the SAME word: `icontains` is itself a member of the
|
|
136
|
+
spec's `VALID_AST_OPERATORS`, so nothing is squashed on the way down. It had no
|
|
137
|
+
row here at all until objectui#8976 — `convertFiltersToAST` refused it as an
|
|
138
|
+
unknown operator while `ValueDataSource` executed it and this repo's own filter
|
|
139
|
+
builder emitted it. The pin now holds these two tables complete against the
|
|
140
|
+
spec's `FILTER_OPERATORS` as well as against the code, so a canonical operator
|
|
141
|
+
that is documented NEITHER as supported NOR as refused fails the suite instead
|
|
142
|
+
of going unnoticed.
|
|
143
|
+
|
|
144
|
+
| MongoDB Operator | ObjectStack Operator | Example |
|
|
145
|
+
|------------------|---------------------|---------|
|
|
146
|
+
| plain value (no operator) | `=` | `{ status: 'active' }` → `['status', '=', 'active']` |
|
|
147
|
+
| `$eq` | `=` | `{ status: { $eq: 'active' } }` → `['status', '=', 'active']` |
|
|
148
|
+
| `$ne` | `!=` | `{ status: { $ne: 'archived' } }` → `['status', '!=', 'archived']` |
|
|
149
|
+
| `$gt` | `>` | `{ age: { $gt: 18 } }` → `['age', '>', 18]` |
|
|
150
|
+
| `$gte` | `>=` | `{ age: { $gte: 18 } }` → `['age', '>=', 18]` |
|
|
151
|
+
| `$lt` | `<` | `{ age: { $lt: 65 } }` → `['age', '<', 65]` |
|
|
152
|
+
| `$lte` | `<=` | `{ age: { $lte: 65 } }` → `['age', '<=', 65]` |
|
|
153
|
+
| `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }` → `['status', 'in', ['active', 'pending']]` |
|
|
154
|
+
| `$nin` | `nin` | `{ status: { $nin: ['archived'] } }` → `['status', 'nin', ['archived']]` |
|
|
155
|
+
| `$between` | `between` | `{ age: { $between: [18, 65] } }` → `['age', 'between', [18, 65]]` |
|
|
156
|
+
| `$contains` | `contains` | `{ name: { $contains: 'John' } }` → `['name', 'contains', 'John']` |
|
|
157
|
+
| `$notContains` | `notcontains` | `{ name: { $notContains: 'test' } }` → `['name', 'notcontains', 'test']` |
|
|
158
|
+
| `$startsWith` | `startswith` | `{ email: { $startsWith: 'admin' } }` → `['email', 'startswith', 'admin']` |
|
|
159
|
+
| `$endsWith` | `endswith` | `{ email: { $endsWith: '@example.com' } }` → `['email', 'endswith', '@example.com']` |
|
|
160
|
+
| `$icontains` | `icontains` | `{ name: { $icontains: 'john' } }` → `['name', 'icontains', 'john']` |
|
|
161
|
+
| `$null` | `is_null` / `is_not_null` | `{ email: { $null: true } }` → `['email', 'is_null', true]` |
|
|
162
|
+
| `$exists` | `is_not_null` / `is_null` | `{ email: { $exists: true } }` → `['email', 'is_not_null', true]` |
|
|
163
|
+
| `$empty` | `is_empty` / `is_not_empty` | `{ email: { $empty: true } }` → `['email', 'is_empty', true]` |
|
|
164
|
+
|
|
165
|
+
`$null`, `$exists` and `$empty` read their boolean: `$null: false` lowers to
|
|
166
|
+
`is_not_null`, `$exists: false` to `is_null` and `$empty: false` to
|
|
167
|
+
`is_not_empty`. The lowered node's value slot is always `true` — the direction
|
|
168
|
+
comes from the operator name, which is how the spec's `data/filter.zod.ts`
|
|
169
|
+
reads it. `$empty` takes ONLY a boolean: any other flag throws `INVALID_FILTER`
|
|
170
|
+
/ 400 at lowering time, which is how the spec's own doors treat it.
|
|
171
|
+
|
|
172
|
+
`$empty` is not `$null`. objectstack#20446 admitted it to the spec's
|
|
173
|
+
`FILTER_OPERATORS` and flipped the view operators `is_empty` / `is_not_empty`
|
|
174
|
+
to lower to it; they lowered to `$null` before. Its meaning is the spec's ruled
|
|
175
|
+
per-type table: a text-like field is empty when null or `''`, a multi-value
|
|
176
|
+
field when null or `[]`, and any other field when null. Which backend applies
|
|
177
|
+
that table, and how, is listed in the docblock of the spec's
|
|
178
|
+
`FILTER_OPERATORS`, not here. Until objectui#11094 `convertFiltersToAST`
|
|
179
|
+
refused `$empty` as an unknown operator.
|
|
180
|
+
|
|
181
|
+
`$icontains` constrains its **comparand**, which no other row in this table
|
|
182
|
+
does: `@objectstack/spec`'s `FILTER_TEXT_CASES` declares an empty or non-string
|
|
183
|
+
comparand REFUSED, so an empty box or an uncoerced number throws
|
|
184
|
+
`INVALID_FILTER` / 400 at lowering time instead of sending a predicate that
|
|
185
|
+
constrains nothing or a question nobody wrote (objectui#9001). The operator is
|
|
186
|
+
supported exactly as the row says — write a non-empty string, or drop the
|
|
187
|
+
condition.
|
|
188
|
+
|
|
189
|
+
#### Logical combinators
|
|
190
|
+
|
|
191
|
+
`$and` and `$or` take an array of filter conditions and lower to an AST group
|
|
192
|
+
node whose members are lowered with the table above. Sibling keys of one
|
|
193
|
+
object are still combined with `and` (see "Complex Filter Examples" below). An
|
|
194
|
+
empty `$and` is TRUE and is dropped rather than emitted as an empty group —
|
|
195
|
+
`['and']` is not a filter, so emitting it would widen the result set — and an
|
|
196
|
+
empty `$or` is FALSE and matches no row.
|
|
197
|
+
|
|
198
|
+
| MongoDB Key | AST Keyword | Example |
|
|
199
|
+
|-------------|-------------|---------|
|
|
200
|
+
| `$and` | `and` | `{ $and: [{ status: 'open' }, { priority: { $gte: 2 } }] }` → `['and', ['status', '=', 'open'], ['priority', '>=', 2]]` |
|
|
201
|
+
| `$or` | `or` | `{ $or: [{ status: 'open' }, { status: 'blocked' }] }` → `['or', ['status', '=', 'open'], ['status', '=', 'blocked']]` |
|
|
202
|
+
|
|
203
|
+
#### Refused at lowering time
|
|
204
|
+
|
|
205
|
+
These shapes throw a `FilterOperatorError` (`code: 'INVALID_FILTER'`,
|
|
206
|
+
`httpStatus: 400`) from `convertFiltersToAST` instead of reaching the wire. The
|
|
207
|
+
refusal names the field and the value, so a filter that carries one fails at
|
|
208
|
+
the call site rather than as a `400` from the server or as an empty list.
|
|
209
|
+
|
|
210
|
+
| Shape | Why | Example |
|
|
211
|
+
|-------|-----|---------|
|
|
212
|
+
| `$regex` | The spec has no `$regex`, and it is not downgraded to `contains`: a pattern match and a substring match are different questions, not stronger and weaker forms of one. Use `$contains` for a case-sensitive substring, `$icontains` for a case-insensitive one, or `$startsWith` / `$endsWith`. | `{ name: { $regex: '^J' } }` → throws `INVALID_FILTER` |
|
|
213
|
+
| `$not` | The AST has no negation keyword, and rewriting the negation inward would be silently partial. Use a negated operator instead: `$ne`, `$nin`, `$notContains`. | `{ $not: { status: 'open' } }` → throws `INVALID_FILTER` |
|
|
214
|
+
| a bare array as a field's value | The AST has no array-equality node, and the array is deliberately not read as `$in` (see below). | `{ tags: ['a', 'b'] }` → throws `INVALID_FILTER` |
|
|
215
|
+
| `$notin` / `$notcontains` / `$startswith` / `$endswith` | Retired lowercase aliases (objectui#8568). The `$` dialect follows `@objectstack/spec`'s spellings, and this repo's in-memory matcher already refused these; accepting them here made one authored filter behave differently depending on the data source behind the view. The refusal names the canonical spelling for the alias you wrote — rename the key, the operator is unchanged. | `{ email: { $startswith: 'a' } }` → throws `INVALID_FILTER` |
|
|
216
|
+
| any other `$` key in operator position | Unknown operator; the error message lists the supported ones. | `{ age: { $foo: 1 } }` → throws `INVALID_FILTER` |
|
|
217
|
+
|
|
218
|
+
A bare array as a field's value — `{ tags: ['a', 'b'] }` — is **refused** at
|
|
219
|
+
lowering time with a `FilterOperatorError` (`INVALID_FILTER` / 400) naming the
|
|
220
|
+
field and the comparand, not read as `$in` (objectui#8530): the ObjectQL AST
|
|
221
|
+
has no array-equality node, so `['tags', '=', ['a', 'b']]` was answered by a
|
|
222
|
+
`400` from the wire or an empty list from every in-memory matcher. Spell
|
|
223
|
+
membership as `{ tags: { $in: ['a', 'b'] } }` (or `$nin` for its negation).
|
|
224
|
+
|
|
225
|
+
An **exotic object** as a field's value — a `RegExp`, a `Set`, a `Map`, a `URL`,
|
|
226
|
+
any class instance — is refused the same way (objectui#8567). It has no row in
|
|
227
|
+
the table above because the table's examples are executed as JSON literals and
|
|
228
|
+
none of these shapes can be written as one; the refusal is no less real for
|
|
229
|
+
that. `{ name: /abc/ }` used to lower to **nothing at all**: `Object.entries` of
|
|
230
|
+
a `RegExp` is `[]`, so the operator loop ran zero times and the `name` condition
|
|
231
|
+
was dropped from the filter entirely, silently widening the result set. The spec
|
|
232
|
+
rules these out — a comparison value must be a string, number, bigint, boolean,
|
|
233
|
+
null or `Date` (`ACCEPTED_FILTER_COMPARAND_TYPES`), and
|
|
234
|
+
`normalizeFilterComparandTypes` answers `400 INVALID_FILTER` for a `RegExp` —
|
|
235
|
+
so the refusal now lands at lowering time instead of the field vanishing. Spell
|
|
236
|
+
a text match as `{ name: { $contains: 'abc' } }` (`$startsWith` / `$endsWith`), a
|
|
237
|
+
membership test as `{ name: { $in: [...] } }`, and a date bound as
|
|
238
|
+
`{ created: { $gte: someDate } }`. A `Date` comparand is **not** exotic: the spec
|
|
239
|
+
accepts it and it lowers as an equality node (objectui#8555). An **empty
|
|
240
|
+
operator object** — `{ created: {} }`, a field with no operator — is refused too,
|
|
241
|
+
alone or beside other keys, with a `FilterOperatorError` naming the field
|
|
242
|
+
(objectui#9164 alone, objectui#10788 beside other keys). `@objectstack/spec` records
|
|
243
|
+
`{ field: {} }` as REJECTED (objectstack#5240), and beside a key that lowers it
|
|
244
|
+
used to be dropped the same silent way: `{ status: 'a', created: {} }` sent
|
|
245
|
+
exactly what `{ status: 'a' }` sends. Give the field an operator (`$eq`, `$in`,
|
|
246
|
+
`$null`, …) or remove the key.
|
|
247
|
+
|
|
248
|
+
#### Complex Filter Examples
|
|
249
|
+
|
|
250
|
+
**Multiple conditions** are combined with `'and'`:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// Input
|
|
254
|
+
const $filter = {
|
|
255
|
+
age: { $gte: 18, $lte: 65 },
|
|
256
|
+
status: 'active',
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Converted to AST
|
|
260
|
+
const ast = [
|
|
261
|
+
'and',
|
|
262
|
+
['age', '>=', 18],
|
|
263
|
+
['age', '<=', 65],
|
|
264
|
+
['status', '=', 'active'],
|
|
265
|
+
];
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Rule-shaped arrays, on `find()` **and** `aggregate()`
|
|
269
|
+
|
|
270
|
+
Server-driven view configs store their conditions as an array of rules
|
|
271
|
+
(`ViewFilterRule[]`), not as a MongoDB-style object:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const filter = [{ field: 'stage', operator: 'equals', value: 'won' }];
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Both read paths lower that array to the same AST before it reaches the wire —
|
|
278
|
+
`find()` via `$filter`, and `aggregate()` via the analytics `where`. They share
|
|
279
|
+
one translator, so a stored filter cannot mean one thing on a list and another
|
|
280
|
+
on a KPI:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// find(): filter=["stage","=","won"]
|
|
284
|
+
// aggregate(): { ..., where: ["stage", "=", "won"] }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Operator aliases (`equals`, `greater_than_or_equal`, `not_in`, `before`, ...)
|
|
288
|
+
map to the canonical AST symbols, and rules spread into a logical node
|
|
289
|
+
(`['and', ...rules, ...tuples]`) are lowered at depth. A rule that cannot be
|
|
290
|
+
translated raises `MalformedFilterError` rather than being dropped — dropping
|
|
291
|
+
one condition of an `and` would widen the result set and report success.
|
|
292
|
+
|
|
293
|
+
An `icontains` rule constrains its **comparand** here exactly as `$icontains`
|
|
294
|
+
does in the object form: `@objectstack/spec`'s `FILTER_TEXT_CASES` declares an
|
|
295
|
+
empty or non-string comparand REFUSED, so a rule such as
|
|
296
|
+
`{ field: 'name', operator: 'icontains', value: '' }` — or one whose `value` is
|
|
297
|
+
a number, `null` or missing — throws `MalformedFilterError` (`INVALID_FILTER` /
|
|
298
|
+
400) before any request is sent, instead of sending a predicate that constrains
|
|
299
|
+
nothing or a question nobody wrote (objectui#9048). The reason is the spec's
|
|
300
|
+
own, the one the object form's refusal carries (objectui#9001), and the message
|
|
301
|
+
names the operator spelling that arrived (`icontains`, `ICONTAINS`, …) beside
|
|
302
|
+
the `$icontains` spelling the contract uses. The rule is refused wherever it
|
|
303
|
+
sits — at the top level, in a nested group, or spread into a logical node — and
|
|
304
|
+
on both `find()` and `aggregate()`. Write a non-empty string, or drop the rule.
|
|
305
|
+
|
|
306
|
+
Non-array filters are passed through unchanged on the aggregate path: a
|
|
307
|
+
MongoDB-style object is already what `/analytics/query` accepts.
|
|
308
|
+
|
|
309
|
+
#### `aggregate({ where })` does NOT lower — it refuses
|
|
310
|
+
|
|
311
|
+
The lowering above is the `filter` parameter, on the analytics path.
|
|
312
|
+
`aggregate()` has a second, spec-shape branch — entered when the params carry
|
|
313
|
+
an array `groupBy`, an array `aggregations`, or **any** `where` key — which
|
|
314
|
+
posts `where` to `POST /data/:object/query` verbatim. That `where` is the spec
|
|
315
|
+
Query DSL's `where`, so it must ALREADY be lowered, and since objectui#6825 an
|
|
316
|
+
array that the spec's own `isFilterAST` gate rejects is refused here rather
|
|
317
|
+
than shipped:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
321
|
+
|
|
322
|
+
declare const dataSource: ObjectStackAdapter;
|
|
323
|
+
|
|
324
|
+
// ✅ lowered — reaches the wire unchanged
|
|
325
|
+
await dataSource.aggregate('opportunity', {
|
|
326
|
+
groupBy: ['stage'],
|
|
327
|
+
aggregations: [{ function: 'count', field: 'id', alias: 'n' }],
|
|
328
|
+
where: ['stage', '=', 'won'],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ⛔ throws UnloweredAggregateWhereError — authoring sugar, not a filter
|
|
332
|
+
await dataSource.aggregate('opportunity', {
|
|
333
|
+
groupBy: ['stage'],
|
|
334
|
+
aggregations: [{ function: 'count', field: 'id', alias: 'n' }],
|
|
335
|
+
where: [{ field: 'stage', operator: 'equals', value: 'won' }],
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
This is a producer-side refusal of a value the server already refused (`is not
|
|
340
|
+
a filter`, `400 INVALID_FILTER`): the point is that you now find out at the call
|
|
341
|
+
site, with the offending value named, instead of on the wire — or not at all,
|
|
342
|
+
with a chart quietly rendering unfiltered numbers. Lower the rules in whatever
|
|
343
|
+
built the params, or use the analytics branch (`filter` + the legacy `field` /
|
|
344
|
+
`function` / `groupBy` params), which lowers for you.
|
|
345
|
+
|
|
346
|
+
Two shapes are deliberately NOT refused, because the receiving door accepts
|
|
347
|
+
them: a `FilterCondition` object (`{ stage: 'won' }` — what `QuerySchema.where`
|
|
348
|
+
declares), and an empty array (`[]` means "no filter").
|
|
349
|
+
|
|
350
|
+
#### The two shapes are alternatives, not a mixture
|
|
351
|
+
|
|
352
|
+
The spec-shape branch reads exactly four keys — `groupBy`, `aggregations`,
|
|
353
|
+
`where`, `limit` — and `filter` / `field` / `function` are the **analytics**
|
|
354
|
+
branch's own parameters. Until objectui#6864 they were neither read nor
|
|
355
|
+
reported on the spec-shape branch: they simply were not in the body that went
|
|
356
|
+
out. Since #6864 they are refused, by name:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
360
|
+
|
|
361
|
+
declare const dataSource: ObjectStackAdapter;
|
|
362
|
+
|
|
363
|
+
// ⛔ throws AnalyticsKeysOnSpecShapeError — an ARRAY `groupBy` selects the
|
|
364
|
+
// spec-shape branch, and these three keys would have vanished from the query:
|
|
365
|
+
// no filter, and no measure at all.
|
|
366
|
+
await dataSource.aggregate('opportunity', {
|
|
367
|
+
field: 'amount',
|
|
368
|
+
function: 'sum',
|
|
369
|
+
groupBy: ['stage'],
|
|
370
|
+
filter: [{ field: 'stage', operator: 'equals', value: 'won' }],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ✅ the analytics shape, unchanged — a STRING `groupBy` keeps this call on the
|
|
374
|
+
// analytics branch, where all three keys are read and `filter` is lowered for you
|
|
375
|
+
await dataSource.aggregate('opportunity', {
|
|
376
|
+
field: 'amount',
|
|
377
|
+
function: 'sum',
|
|
378
|
+
groupBy: 'stage',
|
|
379
|
+
filter: [{ field: 'stage', operator: 'equals', value: 'won' }],
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Pick one shape per call. A key that is present but nullish (`filter: undefined`)
|
|
384
|
+
carries nothing to drop and is not refused, so params built by spreading
|
|
385
|
+
possibly-absent authored values keep working. Keys outside those three are not
|
|
386
|
+
refused either — the gate names `filter`, `field` and `function`, and only
|
|
387
|
+
those.
|
|
388
|
+
|
|
389
|
+
### Group headers for server-side grid grouping
|
|
390
|
+
|
|
391
|
+
`queryGroupHeaders(resource, query)` answers a grouped list view's **group
|
|
392
|
+
header query** — the `EngineAggregateOptions` that `@objectstack/spec/ui`'s
|
|
393
|
+
`compileListViewGroupQuery` compiles from the view's `grouping` (and its column
|
|
394
|
+
summaries). It is how a grouped `object-grid` gets its group set and every
|
|
395
|
+
header number from the server instead of bucketing a fetched page
|
|
396
|
+
(objectui#7189): one row per group, each grouped field under its own name
|
|
397
|
+
holding the raw stored value (`null` for the empty group), and the group's
|
|
398
|
+
total row count under `count`.
|
|
399
|
+
|
|
400
|
+
The query is posted **verbatim** to the existing `POST /data/:object/query`
|
|
401
|
+
door (the same one the spec-shape `aggregate()` branch uses), and the answer is
|
|
402
|
+
its `records`. Unlike `aggregate()`, it never degrades to a client-side
|
|
403
|
+
reduction: a refusal is thrown with the server's code and status, and an answer
|
|
404
|
+
without a `records` array is refused rather than read as "no groups". The rows
|
|
405
|
+
inside a group are an ordinary `find()` with the group's compiled row query
|
|
406
|
+
(`compileListViewGroupRowsQuery`).
|
|
407
|
+
|
|
408
|
+
### Sorting
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
412
|
+
|
|
413
|
+
declare const dataSource: ObjectStackAdapter;
|
|
414
|
+
|
|
415
|
+
// OData-style
|
|
416
|
+
await dataSource.find('users', {
|
|
417
|
+
$orderby: {
|
|
418
|
+
createdAt: 'desc',
|
|
419
|
+
name: 'asc',
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Converted to ObjectStack: ['-createdAt', 'name']
|
|
424
|
+
```
|
|
425
|
+
|
|
74
426
|
## Metadata Caching
|
|
75
427
|
|
|
76
428
|
The adapter includes built-in metadata caching to improve performance when fetching schemas:
|
|
77
429
|
|
|
78
430
|
```typescript
|
|
431
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
432
|
+
|
|
433
|
+
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
|
|
434
|
+
|
|
79
435
|
// Get cache statistics
|
|
80
436
|
const stats = dataSource.getCacheStats();
|
|
81
437
|
console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
|
|
82
438
|
console.log(`Cache size: ${stats.size}/${stats.maxSize}`);
|
|
439
|
+
console.log(`Fetches coalesced onto an in-flight request: ${stats.coalesced}`);
|
|
83
440
|
|
|
84
441
|
// Manually invalidate cache entries
|
|
85
442
|
dataSource.invalidateCache('users'); // Invalidate specific schema
|
|
@@ -95,13 +452,23 @@ dataSource.clearCache();
|
|
|
95
452
|
- **TTL Expiration**: Entries expire after the configured time-to-live from creation (default: 5 minutes)
|
|
96
453
|
- Note: TTL is fixed from creation time, not sliding based on access
|
|
97
454
|
- **Memory Limits**: Configurable maximum cache size (default: 100 entries)
|
|
98
|
-
- **
|
|
455
|
+
- **Request Coalescing**: Concurrent `get` calls for the same uncached key share a single
|
|
456
|
+
fetch. The first caller invokes the fetcher; every caller that arrives while that promise
|
|
457
|
+
is still in flight is handed the same promise instead of starting a second request, and
|
|
458
|
+
each one increments the `coalesced` counter in `getCacheStats()` — so the saving is
|
|
459
|
+
something you can read off the adapter, not just a claim in this page.
|
|
460
|
+
- The in-flight slot is released in a `finally`, so a rejected fetch is not cached and
|
|
461
|
+
does not poison the key: the next call starts a fresh fetch.
|
|
99
462
|
|
|
100
463
|
## Connection State Monitoring
|
|
101
464
|
|
|
102
465
|
The adapter provides real-time connection state monitoring with automatic reconnection:
|
|
103
466
|
|
|
104
467
|
```typescript
|
|
468
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
469
|
+
|
|
470
|
+
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
|
|
471
|
+
|
|
105
472
|
// Monitor connection state changes
|
|
106
473
|
const unsubscribe = dataSource.onConnectionStateChange((event) => {
|
|
107
474
|
console.log('Connection state:', event.state);
|
|
@@ -146,6 +513,12 @@ The adapter automatically attempts to reconnect on connection failures:
|
|
|
146
513
|
Track progress of bulk operations in real-time:
|
|
147
514
|
|
|
148
515
|
```typescript
|
|
516
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
517
|
+
|
|
518
|
+
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
|
|
519
|
+
|
|
520
|
+
declare const largeDataset: Array<Record<string, unknown>>;
|
|
521
|
+
|
|
149
522
|
// Monitor batch operation progress
|
|
150
523
|
const unsubscribe = dataSource.onBatchProgress((event) => {
|
|
151
524
|
console.log(`${event.operation}: ${event.percentage.toFixed(1)}%`);
|
|
@@ -187,30 +560,60 @@ import {
|
|
|
187
560
|
// SYMBOL is prefixed because @objectstack/spec/kernel
|
|
188
561
|
// owns `ValidationError` for a { field, message, code? }
|
|
189
562
|
// record (objectui#3160).
|
|
563
|
+
MalformedFilterError, // A filter rule that cannot be translated (400
|
|
564
|
+
// INVALID_FILTER) — thrown rather than dropped,
|
|
565
|
+
// because dropping one condition of an `and` widens
|
|
566
|
+
// the result set and reports success. Also thrown,
|
|
567
|
+
// before any request, for an `icontains` rule whose
|
|
568
|
+
// `value` is empty or not a string, naming the
|
|
569
|
+
// spelling that arrived. See "Rule-shaped arrays".
|
|
570
|
+
UnloweredAggregateWhereError, // aggregate()'s spec-shape `where` was an array
|
|
571
|
+
// the spec's filter-AST gate rejects (400
|
|
572
|
+
// INVALID_FILTER). See "aggregate({ where }) does
|
|
573
|
+
// NOT lower" above.
|
|
574
|
+
AnalyticsKeysOnSpecShapeError, // aggregate()'s spec-shape branch was handed the
|
|
575
|
+
// ANALYTICS branch's keys (`filter` / `field` /
|
|
576
|
+
// `function`), which it does not read (400
|
|
577
|
+
// INVALID_FILTER). See "the two shapes are
|
|
578
|
+
// alternatives, not a mixture" above.
|
|
579
|
+
isMalformedFilterError, // Recognises ALL THREE of the above, and the server's
|
|
580
|
+
// own version of the same refusal.
|
|
190
581
|
} from '@object-ui/data-objectstack';
|
|
191
582
|
```
|
|
192
583
|
|
|
193
584
|
### Error Handling Example
|
|
194
585
|
|
|
195
586
|
```typescript
|
|
587
|
+
import {
|
|
588
|
+
ObjectStackError,
|
|
589
|
+
MetadataNotFoundError,
|
|
590
|
+
ConnectionError,
|
|
591
|
+
AuthenticationError,
|
|
592
|
+
type ObjectStackAdapter,
|
|
593
|
+
} from '@object-ui/data-objectstack';
|
|
594
|
+
|
|
595
|
+
declare const dataSource: ObjectStackAdapter;
|
|
596
|
+
|
|
196
597
|
try {
|
|
197
598
|
const schema = await dataSource.getObjectSchema('users');
|
|
198
599
|
} catch (error) {
|
|
199
600
|
if (error instanceof MetadataNotFoundError) {
|
|
200
|
-
console.error(`Schema not found: ${error.details
|
|
601
|
+
console.error(`Schema not found: ${error.details?.objectName}`);
|
|
201
602
|
} else if (error instanceof ConnectionError) {
|
|
202
603
|
console.error(`Connection failed to: ${error.url}`);
|
|
203
604
|
} else if (error instanceof AuthenticationError) {
|
|
204
605
|
console.error('Authentication required');
|
|
205
606
|
}
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
607
|
+
|
|
608
|
+
// Every error this adapter throws carries the same shape
|
|
609
|
+
if (error instanceof ObjectStackError) {
|
|
610
|
+
console.error({
|
|
611
|
+
code: error.code,
|
|
612
|
+
message: error.message,
|
|
613
|
+
statusCode: error.statusCode,
|
|
614
|
+
details: error.details,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
214
617
|
}
|
|
215
618
|
```
|
|
216
619
|
|
|
@@ -219,6 +622,12 @@ try {
|
|
|
219
622
|
Bulk operations provide detailed error reporting with partial success information:
|
|
220
623
|
|
|
221
624
|
```typescript
|
|
625
|
+
import { BulkOperationError, type ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
626
|
+
|
|
627
|
+
declare const dataSource: ObjectStackAdapter;
|
|
628
|
+
|
|
629
|
+
declare const records: Array<Record<string, unknown>>;
|
|
630
|
+
|
|
222
631
|
try {
|
|
223
632
|
await dataSource.bulk('users', 'update', records);
|
|
224
633
|
} catch (error) {
|
|
@@ -244,6 +653,9 @@ All errors include unique error codes for programmatic handling:
|
|
|
244
653
|
- `CONNECTION_ERROR` - Connection/network error
|
|
245
654
|
- `AUTHENTICATION_ERROR` - Authentication failure
|
|
246
655
|
- `VALIDATION_ERROR` - Data validation error
|
|
656
|
+
- `INVALID_FILTER` - A request the adapter refuses to send (`MalformedFilterError`,
|
|
657
|
+
`UnloweredAggregateWhereError`, `AnalyticsKeysOnSpecShapeError`); matches the data
|
|
658
|
+
API's own code for the same refusal
|
|
247
659
|
- `UNSUPPORTED_OPERATION` - Unsupported operation
|
|
248
660
|
- `NOT_FOUND` - Resource not found
|
|
249
661
|
- `UNKNOWN_ERROR` - Unknown error
|
|
@@ -253,6 +665,10 @@ All errors include unique error codes for programmatic handling:
|
|
|
253
665
|
The adapter supports optimized batch operations with automatic fallback:
|
|
254
666
|
|
|
255
667
|
```typescript
|
|
668
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
669
|
+
|
|
670
|
+
declare const dataSource: ObjectStackAdapter;
|
|
671
|
+
|
|
256
672
|
// Batch create
|
|
257
673
|
const newUsers = await dataSource.bulk('users', 'create', [
|
|
258
674
|
{ name: 'Alice', email: 'alice@example.com' },
|
|
@@ -286,6 +702,10 @@ writes as a single all-or-nothing unit — the master-detail case, where a paren
|
|
|
286
702
|
and its children must commit or roll back together — use `batchTransaction`:
|
|
287
703
|
|
|
288
704
|
```typescript
|
|
705
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
706
|
+
|
|
707
|
+
declare const dataSource: ObjectStackAdapter;
|
|
708
|
+
|
|
289
709
|
// Create a parent and a child that references it, atomically.
|
|
290
710
|
// `{ $ref: 0 }` resolves to the id minted by operation 0 (the parent).
|
|
291
711
|
await dataSource.batchTransaction([
|
|
@@ -341,6 +761,115 @@ still succeeds, but non-atomically via the fallback above. Treat the advertised
|
|
|
341
761
|
capability as the floor for the atomicity guarantee, not as a connection
|
|
342
762
|
prerequisite.
|
|
343
763
|
|
|
764
|
+
## Object-Metadata Write Guard
|
|
765
|
+
|
|
766
|
+
`MetadataClient.save` refuses an `object` document whose `fields` carry either
|
|
767
|
+
of two incomplete fields, **before** issuing the request:
|
|
768
|
+
|
|
769
|
+
- a relationship field (`lookup`, `master_detail`) with a missing, empty or
|
|
770
|
+
whitespace-only `reference`;
|
|
771
|
+
- a choice field (`select`, `radio`) with no option source: neither a non-empty
|
|
772
|
+
`options` list nor a shared `picklist`. `options: []` counts as none.
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
import { MetadataClient } from '@object-ui/data-objectstack';
|
|
776
|
+
|
|
777
|
+
const client = new MetadataClient({ baseUrl: '/api/v1' });
|
|
778
|
+
|
|
779
|
+
await client.save('object', 'account', {
|
|
780
|
+
name: 'account',
|
|
781
|
+
fields: { owner: { type: 'lookup', label: 'Owner' } },
|
|
782
|
+
});
|
|
783
|
+
// throws: MetadataClient.save refused this object metadata write: the field
|
|
784
|
+
// `owner` is a `lookup` and carries no `reference` key at all ...
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
For the relationship rule, nothing that previously succeeded now fails.
|
|
788
|
+
`@objectstack/spec` refuses the same document at the server with a 422 on
|
|
789
|
+
`fields.owner.reference`, and that refusal blocks every *later* save of the
|
|
790
|
+
object for as long as the half-filled field rides along in the draft. The guard
|
|
791
|
+
moves the identical refusal earlier, names the field while it is still on screen,
|
|
792
|
+
and leaves the draft in the client.
|
|
793
|
+
|
|
794
|
+
The choice rule is different, and on purpose: it holds a document the installed
|
|
795
|
+
server still stores. It is the objectui half of the maintainer's ruling A on
|
|
796
|
+
[objectstack#20827](https://github.com/objectstack-ai/objectstack/issues/20827),
|
|
797
|
+
which refuses such a field at the `FieldSchema` door and has objectui stop
|
|
798
|
+
sending it first. A `select` added in Studio, and saved before its first option,
|
|
799
|
+
now waits in the client, with the field named, until it has one:
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
import { MetadataClient } from '@object-ui/data-objectstack';
|
|
803
|
+
|
|
804
|
+
const client = new MetadataClient({ baseUrl: '/api/v1' });
|
|
805
|
+
|
|
806
|
+
await client.save('object', 'deal', {
|
|
807
|
+
name: 'deal',
|
|
808
|
+
fields: { stage: { type: 'select', label: 'Stage', options: [] } },
|
|
809
|
+
});
|
|
810
|
+
// throws: MetadataClient.save refused this object metadata write: the field
|
|
811
|
+
// `stage` is a `select` with no options ...
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Writes of every other metadata type are untouched, and the guard never strips
|
|
815
|
+
the offending field — a dropped field reported as saved would be a silent
|
|
816
|
+
deletion.
|
|
817
|
+
|
|
818
|
+
Hosts that write object metadata through their own transport can apply the same
|
|
819
|
+
invariant at their own door:
|
|
820
|
+
|
|
821
|
+
```ts
|
|
822
|
+
import { assertObjectMetadataWritable } from '@object-ui/data-objectstack';
|
|
823
|
+
|
|
824
|
+
async function uploadObject(name: string, body: unknown) {
|
|
825
|
+
assertObjectMetadataWritable('object', body, 'uploadObject');
|
|
826
|
+
await fetch(`/api/v1/meta/object/${encodeURIComponent(name)}`, {
|
|
827
|
+
method: 'PUT',
|
|
828
|
+
headers: { 'Content-Type': 'application/json' },
|
|
829
|
+
body: JSON.stringify(body),
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
`RELATIONSHIP_TYPES_REQUIRING_REFERENCE`, `CHOICE_TYPES_REQUIRING_OPTIONS` and
|
|
835
|
+
`OBJECT_METADATA_TYPE` are exported beside it. Both type sets are derived from
|
|
836
|
+
the installed `@objectstack/spec` by this package's own pin (the choice set from
|
|
837
|
+
its `field/choice-without-options` completeness rule), so they follow the
|
|
838
|
+
contract rather than a remembered list.
|
|
839
|
+
|
|
840
|
+
## Showing a Refused Metadata Save
|
|
841
|
+
|
|
842
|
+
When the server's spec check refuses a metadata save, it answers `422
|
|
843
|
+
INVALID_METADATA`. The error's `message` is a headline: a count of issues and
|
|
844
|
+
where they are. What the author should do is in the structured issues, which
|
|
845
|
+
`MetadataClient` puts on the thrown error's `issues`. Show the error through
|
|
846
|
+
`formatMetadataError`, not through `message` alone:
|
|
847
|
+
|
|
848
|
+
```ts
|
|
849
|
+
import { MetadataClient, formatMetadataError } from '@object-ui/data-objectstack';
|
|
850
|
+
|
|
851
|
+
const client = new MetadataClient({ baseUrl: '/api/v1' });
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
await client.save('object', 'account', {
|
|
855
|
+
name: 'account',
|
|
856
|
+
fields: { 'Bad Name': { type: 'text', label: 'Bad' } },
|
|
857
|
+
});
|
|
858
|
+
} catch (err) {
|
|
859
|
+
showBanner(formatMetadataError(err));
|
|
860
|
+
// • fields.Bad Name — Invalid key in record
|
|
861
|
+
// • fields.Bad Name — Field names must be lowercase snake_case (e.g., "first_name", …)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
declare function showBanner(text: string): void;
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
It writes one line per issue, naming the field, and falls back to the error's
|
|
868
|
+
`message` when there are no issues. Render the text with a class that keeps
|
|
869
|
+
newlines (`whitespace-pre-line`). `formatMetadataIssue(issue)` is the same
|
|
870
|
+
one-line format for a single issue, for code that lists several failures, such
|
|
871
|
+
as a publish response's `failed[]`.
|
|
872
|
+
|
|
344
873
|
## User-Scoped State Adapter
|
|
345
874
|
|
|
346
875
|
In addition to the main `DataSource` adapter, this package ships
|
|
@@ -348,9 +877,12 @@ In addition to the main `DataSource` adapter, this package ships
|
|
|
348
877
|
persist per-user UI state (favorites, recent items) into ObjectStack.
|
|
349
878
|
|
|
350
879
|
```typescript
|
|
351
|
-
import { createObjectStackUserStateAdapter } from '@object-ui/data-objectstack';
|
|
880
|
+
import { createObjectStackUserStateAdapter, type ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
352
881
|
import { useAttachUserStateAdapters } from '@object-ui/app-shell';
|
|
353
882
|
|
|
883
|
+
declare const dataSource: ObjectStackAdapter;
|
|
884
|
+
declare const user: { id: string };
|
|
885
|
+
|
|
354
886
|
const favoritesAdapter = createObjectStackUserStateAdapter({
|
|
355
887
|
dataSource, // the ObjectStack DataSource
|
|
356
888
|
userId: user.id,
|
|
@@ -359,6 +891,8 @@ const favoritesAdapter = createObjectStackUserStateAdapter({
|
|
|
359
891
|
// onError: (op, err) => console.warn(`[user-state] ${op} failed`, err),
|
|
360
892
|
});
|
|
361
893
|
|
|
894
|
+
const attach = useAttachUserStateAdapters();
|
|
895
|
+
|
|
362
896
|
attach('favorites', favoritesAdapter);
|
|
363
897
|
```
|
|
364
898
|
|
|
@@ -393,6 +927,8 @@ for the full design.
|
|
|
393
927
|
|
|
394
928
|
#### Constructor
|
|
395
929
|
|
|
930
|
+
<!-- doc-snippet: fragment — the constructor SIGNATURE in prose notation, not a statement: `new ObjectStackAdapter(config: { ... })` names the parameter type where a call would carry a value. Measured TS1005x10. -->
|
|
931
|
+
|
|
396
932
|
```typescript
|
|
397
933
|
new ObjectStackAdapter(config: {
|
|
398
934
|
baseUrl: string;
|
|
@@ -446,6 +982,10 @@ new ObjectStackAdapter(config: {
|
|
|
446
982
|
#### Schema Not Found
|
|
447
983
|
|
|
448
984
|
```typescript
|
|
985
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
986
|
+
|
|
987
|
+
declare const dataSource: ObjectStackAdapter;
|
|
988
|
+
|
|
449
989
|
// Error: MetadataNotFoundError
|
|
450
990
|
// Solution: Verify object name and ensure schema exists on server
|
|
451
991
|
const schema = await dataSource.getObjectSchema('correct_object_name');
|
|
@@ -454,6 +994,8 @@ const schema = await dataSource.getObjectSchema('correct_object_name');
|
|
|
454
994
|
#### Connection Errors
|
|
455
995
|
|
|
456
996
|
```typescript
|
|
997
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
998
|
+
|
|
457
999
|
// Error: ConnectionError
|
|
458
1000
|
// Solution: Check baseUrl and network connectivity
|
|
459
1001
|
const dataSource = createObjectStackAdapter({
|
|
@@ -465,6 +1007,10 @@ const dataSource = createObjectStackAdapter({
|
|
|
465
1007
|
#### Cache Issues
|
|
466
1008
|
|
|
467
1009
|
```typescript
|
|
1010
|
+
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
1011
|
+
|
|
1012
|
+
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
|
|
1013
|
+
|
|
468
1014
|
// Clear cache if stale data is being returned
|
|
469
1015
|
dataSource.clearCache();
|
|
470
1016
|
|