@metaobjectsdev/sdk 0.21.3 → 0.21.4
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.
|
@@ -719,8 +719,56 @@ Multi-source: multiple `source.rdb` children, each with a `@role`, exactly one
|
|
|
719
719
|
```
|
|
720
720
|
|
|
721
721
|
**An entity's PRIMARY source must be writable** (`table`) — read-only kinds are
|
|
722
|
-
legal only in non-primary roles
|
|
723
|
-
|
|
722
|
+
legal only in non-primary roles.
|
|
723
|
+
|
|
724
|
+
### Derived/computed columns: reach for an ENTITY READ-VIEW first
|
|
725
|
+
|
|
726
|
+
**Do not default to `object.projection` for a list screen, grid, or "one extra
|
|
727
|
+
column" read.** The usual need — an entity's own rows plus a joined display name or a
|
|
728
|
+
per-row count, filterable and sortable like any other column — is an **entity
|
|
729
|
+
read-view**, not a projection:
|
|
730
|
+
|
|
731
|
+
```jsonc
|
|
732
|
+
{ "object.entity": { "name": "Order", "children": [
|
|
733
|
+
{ "source.rdb": { "@kind": "table", "@table": "orders" } }, // writes
|
|
734
|
+
{ "source.rdb": { "@kind": "view", "@view": "v_order", "@role": "replica" } }, // reads
|
|
735
|
+
// …the entity's own fields stay as they are — you re-state NOTHING…
|
|
736
|
+
{ "field.string": { "name": "customerName", "@filterable": true, "children": [
|
|
737
|
+
{ "origin.passthrough": { "@from": "Customer.name" } } ]}},
|
|
738
|
+
{ "field.int": { "name": "itemCount", "@filterable": true, "children": [
|
|
739
|
+
{ "origin.aggregate": { "@agg": "count", "@of": "OrderItem.id", "@via": "Order.items" } } ]}}
|
|
740
|
+
]}}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
That is the whole change. Writes route to the table (derived fields are excluded from
|
|
744
|
+
the write codecs); reads route to the view; `meta migrate` emits the `CREATE VIEW` from
|
|
745
|
+
the **same assembly logic** a projection view uses — one emitter, two hosts (FR-024 §7,
|
|
746
|
+
#213/#214). Filtering and sorting on `customerName` / `itemCount` work like any other
|
|
747
|
+
column because the filter tier runs against the view.
|
|
748
|
+
|
|
749
|
+
**Reach for a projection only when one of these is true** (from
|
|
750
|
+
`docs/features/source-kinds.md`, which carries the full decision table):
|
|
751
|
+
|
|
752
|
+
| Entity read-view | Projection |
|
|
753
|
+
|---|---|
|
|
754
|
+
| extras sit over the entity's **own** table | an independent **exposure contract** |
|
|
755
|
+
| same trust domain — a new entity field showing up in the view is correct | a subset, **renamed** columns, versioned, or external consumers |
|
|
756
|
+
| the base is still INSERTable and is the record of truth | keyless, multi-base, proc-backed, all-derived, or borrowed identity |
|
|
757
|
+
|
|
758
|
+
Two things genuinely force a projection: **renamed base columns** (a field carries one
|
|
759
|
+
`@column` per paradigm) and **row-filtered views** (soft-delete / `WHERE status='active'`
|
|
760
|
+
— an entity read-view exposes the whole entity; only a projection carries a row-scope
|
|
761
|
+
`@filter`, #207).
|
|
762
|
+
|
|
763
|
+
Choosing a projection when a read-view would do costs a second URL, a second type, a
|
|
764
|
+
second identity declaration, and re-stating every passthrough column — for no gain.
|
|
765
|
+
|
|
766
|
+
> Historical note for anyone porting older advice: before 0.17.0 (2026-07-18) an entity
|
|
767
|
+
> hosting a derived field silently produced no view and read the wrong table, so a
|
|
768
|
+
> projection genuinely WAS the only way to get a grid with a derived column. That is
|
|
769
|
+
> fixed; guidance written before then is stale.
|
|
770
|
+
|
|
771
|
+
A derived read model that IS an independent exposure is an **`object.projection`**
|
|
724
772
|
(FR-024): its fields `extends` entity fields (`extends: "Author.id"` — dotted
|
|
725
773
|
child traversal, package only on the root segment) and/or carry `origin.*`
|
|
726
774
|
children (`passthrough` / `aggregate` / `collection` / `computed` / `first`)
|
|
@@ -125,6 +125,24 @@ fetcher-provider at the tree root; every generated hook reads it from context. T
|
|
|
125
125
|
generated grid and form components, filter-qs serializer, and cell renderers all
|
|
126
126
|
sit on top of this one seam.
|
|
127
127
|
|
|
128
|
+
## Grids are opt-in per entity
|
|
129
|
+
|
|
130
|
+
Read/CRUD hooks are generated for **every** entity. Grids are the exception:
|
|
131
|
+
wherever your stack generates them (the TanStack client today), an entity produces
|
|
132
|
+
grid artifacts only when it declares a `layout.dataGrid` child. So wiring the grid
|
|
133
|
+
generator and seeing no grid files is expected metadata, not a broken build — the
|
|
134
|
+
run says so in its `meta gen` warnings. Opt an entity in with:
|
|
135
|
+
|
|
136
|
+
```jsonc
|
|
137
|
+
{ "layout.dataGrid": { "name": "default", "@columns": ["name", "email"] } }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Generated grid components are fully **controlled** — they need row-count and
|
|
141
|
+
sort/pagination/filter state on top of the column definitions — so pair the columns
|
|
142
|
+
generator with the grid-**hook** generator, which generates that state plumbing
|
|
143
|
+
instead of leaving you to hand-write it. Your client's reference fragment has the
|
|
144
|
+
generator names and a rendered example.
|
|
145
|
+
|
|
128
146
|
---
|
|
129
147
|
|
|
130
148
|
For this project's runtime + web-client specifics, read every `references/*.md` file in this skill's directory (one per server language and client framework in this project's stack).
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
hooks + a TanStack Table grid component. Like the React client it is **universal**:
|
|
5
5
|
it consumes any backend (TS / Java / Kotlin / C# / Python) that speaks the
|
|
6
6
|
cross-port REST contract. It pairs with `codegen-ts-tanstack`, which emits
|
|
7
|
-
`<Entity>.hooks.ts` and `<Entity>.
|
|
7
|
+
`<Entity>.hooks.ts`, `<Entity>.columns.tsx` and `<Entity>.grid.ts` that import from
|
|
8
|
+
this package.
|
|
8
9
|
|
|
9
10
|
## Contents
|
|
10
11
|
- Install
|
|
11
12
|
- Key exports
|
|
12
13
|
- The `EntityFetcher` contract
|
|
13
|
-
- Generated hooks (`tanstackQuery()`)
|
|
14
|
-
- Generated grid (`tanstackGrid()`)
|
|
14
|
+
- Generated hooks (`tanstackQuery()`) — every entity
|
|
15
|
+
- Generated grid (`tanstackGrid()` + `tanstackGridHook()`) — **opt-in per entity**
|
|
15
16
|
- Cell renderer overrides
|
|
16
17
|
|
|
17
18
|
## Install
|
|
@@ -84,23 +85,68 @@ projections):
|
|
|
84
85
|
Query hooks return `UseQueryResult`; mutation hooks return `UseMutationResult` and
|
|
85
86
|
invalidate the entity's query keys so lists re-fetch after writes.
|
|
86
87
|
|
|
87
|
-
## Generated grid (`tanstackGrid()`)
|
|
88
|
+
## Generated grid (`tanstackGrid()`) — **opt-in per entity**
|
|
89
|
+
|
|
90
|
+
Grid artifacts are the one generator pair that is **not** emitted for every entity.
|
|
91
|
+
`tanstackGrid()` emits `<Entity>.columns.tsx` **only for an entity that declares a
|
|
92
|
+
`layout.dataGrid` child**; an entity without one gets its `.hooks.ts` and no
|
|
93
|
+
columns file at all. That is intended — a grid is a presentation decision about a
|
|
94
|
+
particular entity, so declaring one is how you say "this entity is displayed in a
|
|
95
|
+
grid"; emitting columns for every entity in the model would be noise. A run that
|
|
96
|
+
skips grids for this reason says so in its `meta gen` warnings.
|
|
97
|
+
|
|
98
|
+
So the minimum to get a grid is a `layout.dataGrid` on the entity:
|
|
99
|
+
|
|
100
|
+
```jsonc
|
|
101
|
+
{ "object.entity": { "name": "Author", "children": [
|
|
102
|
+
// ...fields...
|
|
103
|
+
{ "layout.dataGrid": {
|
|
104
|
+
"name": "default",
|
|
105
|
+
"@columns": ["name", "email", "createdAt"], // ordered; omit for every field
|
|
106
|
+
"@pageSize": 25,
|
|
107
|
+
"@defaultSortField": "createdAt",
|
|
108
|
+
"@defaultSortOrder": "desc"
|
|
109
|
+
}}
|
|
110
|
+
]}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One `layout.dataGrid` → one pair of generated consts, named
|
|
114
|
+
`<entity><Grid>Columns` (the `ColumnDef<T>[]`, each carrying `meta.view` for the
|
|
115
|
+
renderer registry) and `<entity><Grid>Grid` (the `GridConfig`). The grid's `name`
|
|
116
|
+
is capitalized into both, so `"name": "default"` on `Author` yields
|
|
117
|
+
`authorDefaultColumns` + `authorDefaultGrid`. Declare several named grids on one
|
|
118
|
+
entity and you get several pairs.
|
|
88
119
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`<EntityGrid
|
|
120
|
+
### Rendering: pair it with `tanstackGridHook()`
|
|
121
|
+
|
|
122
|
+
`<EntityGrid>` is **fully controlled** — beyond `columns`/`grid`/`data` it also
|
|
123
|
+
requires `rowCount`, a `state` object, and three `onChange` callbacks. Wiring that
|
|
124
|
+
by hand (sorting + pagination + column filters + the `withCount=1` query and its
|
|
125
|
+
`buildFilterQs` serialization) is a page of boilerplate that the metadata already
|
|
126
|
+
describes, so **`tanstackGridHook()` generates it**: add it to the config and each
|
|
127
|
+
grid gets a `use<Entity><Grid>Grid()` returning exactly the prop shape
|
|
128
|
+
`<EntityGrid>` wants.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// metaobjects.config.ts
|
|
132
|
+
generators: [entityFile(), tanstackQuery(), tanstackGrid(), tanstackGridHook()],
|
|
133
|
+
```
|
|
92
134
|
|
|
93
135
|
```tsx
|
|
94
|
-
import { useAuthors } from "./generated/Author.hooks";
|
|
95
|
-
import { authorColumns } from "./generated/Author.columns";
|
|
96
136
|
import { EntityGrid } from "@metaobjectsdev/tanstack";
|
|
137
|
+
import { authorDefaultColumns, authorDefaultGrid } from "./generated/Author.columns";
|
|
138
|
+
import { useAuthorDefaultGrid } from "./generated/Author.grid";
|
|
97
139
|
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
export function AuthorList() {
|
|
141
|
+
const grid = useAuthorDefaultGrid(); // owns sorting/pagination/filters + the query
|
|
142
|
+
return <EntityGrid {...grid} columns={authorDefaultColumns} grid={authorDefaultGrid} />;
|
|
143
|
+
}
|
|
100
144
|
```
|
|
101
145
|
|
|
102
|
-
`tanstackGridHook()`
|
|
103
|
-
|
|
146
|
+
`tanstackGridHook()` is optional only in the sense that you may own that state
|
|
147
|
+
yourself; if you do, supply `data`, `rowCount`, `state`, `onSortingChange`,
|
|
148
|
+
`onPaginationChange` and `onColumnFiltersChange` by hand — the hook exists so you
|
|
149
|
+
don't have to.
|
|
104
150
|
|
|
105
151
|
## Cell renderer overrides
|
|
106
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/sdk",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
4
4
|
"description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@metaobjectsdev/metadata": "0.21.
|
|
59
|
+
"@metaobjectsdev/metadata": "0.21.4",
|
|
60
60
|
"zod": "^3.23.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|