Skip to content

Commit 55743eb

Browse files
docs(skills): document fieldGroups MVP in schema skill and data-modeling guide
- Add "Field Groups (MVP)" section to skills/objectstack-schema/SKILL.md with a full worked example, ObjectFieldGroup property table, and migration scope - Link fieldGroups in the "Important optional properties" table and references list - Add "Field Groups" section to content/docs/guides/data-modeling.mdx (with TOC entry) - Regenerate skill references via `pnpm --filter @objectstack/spec gen:skill-refs` so every skill's object.zod.ts copy now includes ObjectFieldGroupSchema (sync also picks up unrelated drift in manifest.zod.ts / dashboard.zod.ts / filter+query → objectstack-query move, per the script's source-of-truth role) Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/2dfd93fb-72b4-4e49-bbba-c239c6b5b81c Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent eda40c0 commit 55743eb

9 files changed

Lines changed: 226 additions & 17 deletions

File tree

content/docs/guides/data-modeling.mdx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ Complete guide to designing robust data models in ObjectStack following enterpri
1414
3. [Relationships & Lookups](#relationships--lookups)
1515
4. [Validation Rules](#validation-rules)
1616
5. [Formula Fields](#formula-fields)
17-
6. [Database Indexing](#database-indexing)
18-
7. [Best Practices](#best-practices)
17+
6. [Field Groups](#field-groups)
18+
7. [Database Indexing](#database-indexing)
19+
8. [Best Practices](#best-practices)
1920

2021
---
2122

@@ -482,6 +483,65 @@ Field.formula({
482483

483484
---
484485

486+
## Field Groups
487+
488+
Organize related fields into logical groups for forms, detail pages, and
489+
editors. The group protocol is intentionally minimal (MVP):
490+
491+
- `ObjectSchema.fieldGroups` declares the groups; **array order is the
492+
display order** (no separate `order` property).
493+
- `Field.group` assigns a field to a group by referencing an
494+
`ObjectFieldGroup.key`. In-group display order equals the traversal
495+
order of `fields`.
496+
- Fields whose `group` is unset (or references an undeclared key) render
497+
in a default bucket after the declared groups.
498+
499+
```typescript
500+
import { ObjectSchema } from '@objectstack/spec/data';
501+
502+
export const Account = ObjectSchema.create({
503+
name: 'account',
504+
label: 'Account',
505+
506+
fieldGroups: [
507+
{ key: 'contact_info', label: 'Contact Information', icon: 'user' },
508+
{ key: 'billing', label: 'Billing', defaultExpanded: false },
509+
{ key: 'system', label: 'System', visibleOn: '$user.isAdmin' },
510+
],
511+
512+
fields: {
513+
name: { type: 'text', required: true, group: 'contact_info' },
514+
email: { type: 'email', group: 'contact_info' },
515+
phone: { type: 'phone', group: 'contact_info' },
516+
vat_id: { type: 'text', group: 'billing' },
517+
billing_address: { type: 'address', group: 'billing' },
518+
created_at: { type: 'datetime', readonly: true, group: 'system' },
519+
created_by: { type: 'lookup', reference: 'user', readonly: true, group: 'system' },
520+
},
521+
});
522+
```
523+
524+
### `ObjectFieldGroup` Properties
525+
526+
| Property | Type | Description |
527+
|:---------|:-----|:------------|
528+
| `key` | `string` (snake_case) | Group machine key; referenced by `Field.group` |
529+
| `label` | `string` | Human-readable group header |
530+
| `icon` | `string?` | Lucide/Material icon name for the group header |
531+
| `description` | `string?` | Optional description under the header |
532+
| `defaultExpanded` | `boolean` (default `true`) | Whether the group is expanded initially |
533+
| `visibleOn` | `string?` | Expression controlling group-level visibility |
534+
535+
### Supported Migrations (MVP)
536+
537+
✅ Add / rename / delete / reorder groups — edit the `fieldGroups` array.
538+
✅ Assign an existing field to a group — set `Field.group`.
539+
540+
⏳ Deferred (future iterations): explicit per-field in-group ordering, nested
541+
groups, permission-scoped group visibility beyond `visibleOn`.
542+
543+
---
544+
485545
## Database Indexing
486546

487547
Optimize query performance with indexes:

skills/objectstack-query/references/_index.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,5 @@
66
77
## Core Schemas
88

9-
- [`data/query.zod.ts`](./data/query.zod.ts) — QueryAST, SortNode, AggregationNode, JoinNode, WindowFunction
10-
- [`data/filter.zod.ts`](./data/filter.zod.ts) — FilterCondition, FieldOperators, Comparison/Set/String operators
11-
12-
## Dependencies (auto-resolved)
9+
- [`data/filter.zod.ts`](./data/filter.zod.ts) — Unified Query DSL Specification
10+
- [`data/query.zod.ts`](./data/query.zod.ts) — Sort Node

skills/objectstack-schema/references/data/filter.zod.ts renamed to skills/objectstack-query/references/data/filter.zod.ts

File renamed without changes.

skills/objectstack-schema/references/data/query.zod.ts renamed to skills/objectstack-query/references/data/query.zod.ts

File renamed without changes.

skills/objectstack-quickstart/references/kernel/manifest.zod.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,25 @@ export const ManifestSchema = z.object({
5454
.regex(/^[a-z][a-z0-9_]{1,19}$/, 'Namespace must be 2-20 chars, lowercase alphanumeric + underscore')
5555
.optional()
5656
.describe('Short namespace identifier for metadata scoping (e.g. "crm", "todo")'),
57-
58-
/**
57+
58+
/**
59+
* Default datasource for all objects in this package.
60+
*
61+
* When set, all objects defined in this package will use this datasource
62+
* by default unless they explicitly override it with their own `datasource` field.
63+
*
64+
* This provides package-level datasource configuration without needing to
65+
* specify it on every individual object.
66+
*
67+
* @example "memory" // Use in-memory driver for all package objects
68+
* @example "turso" // Use Turso/LibSQL for all package objects
69+
*/
70+
defaultDatasource: z.string().optional().default('default')
71+
.describe('Default datasource for all objects in this package'),
72+
73+
/**
5974
* Package version following semantic versioning (major.minor.patch).
60-
*
75+
*
6176
* @example "1.0.0"
6277
* @example "2.1.0-beta.1"
6378
*/
@@ -77,17 +92,27 @@ export const ManifestSchema = z.object({
7792
* - adapter: Host adapter (Express, Fastify)
7893
*/
7994
type: z.enum([
80-
'plugin',
95+
'plugin',
8196
...CORE_PLUGIN_TYPES,
82-
'module',
97+
'module',
8398
'gateway', // Deprecated: use 'server'
8499
'adapter'
85100
]).describe('Type of package'),
86-
87-
/**
101+
102+
/**
103+
* Deployment scope of this package.
104+
* - `platform`: Provided by the runtime layer (auth, identity, i18n, etc.).
105+
* Never installed per-environment; always globally available.
106+
* - `environment`: A business solution (CRM, project management, custom app).
107+
* Installed independently into each environment (Power Apps "solution" model).
108+
*/
109+
scope: z.enum(['platform', 'environment']).default('environment')
110+
.describe('Deployment scope: platform (runtime-global) or environment (per-env install)'),
111+
112+
/**
88113
* Human-readable name of the package.
89114
* Displayed in the UI for users.
90-
*
115+
*
91116
* @example "Project Management"
92117
*/
93118
name: z.string().describe('Human-readable package name'),

skills/objectstack-schema/SKILL.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ database table and exposes automatic CRUD APIs.
7272
| `datasource` | `'default'` | Target datasource ID for virtualized data |
7373
| `displayNameField` | `'name'` | Field used as record display name |
7474
| `enable` || Capability flags (trackHistory, searchable, apiEnabled, etc.) |
75+
| `fieldGroups` || Ordered list of logical field groups for forms/detail pages (see [Field Groups](#field-groups-mvp)) |
7576

7677
### Object Capabilities (`enable`)
7778

@@ -92,6 +93,48 @@ Toggle system behaviours per object:
9293

9394
---
9495

96+
## Field Groups (MVP)
97+
98+
Organize fields into logical groups (e.g., "Contact Information", "Billing",
99+
"System") for forms, detail pages, and editors.
100+
101+
- Declare groups on `ObjectSchema.fieldGroups`**array order is the display order**.
102+
- Assign each field to a group via `Field.group`, which references an
103+
`ObjectFieldGroup.key`. In-group display order equals the traversal order
104+
of `fields`.
105+
- Group keys must be `snake_case`; group labels are human-readable.
106+
107+
```typescript
108+
import { ObjectSchema } from '@objectstack/spec';
109+
110+
export default ObjectSchema.create({
111+
name: 'account',
112+
label: 'Account',
113+
114+
fieldGroups: [
115+
{ key: 'contact_info', label: 'Contact Information', icon: 'user' },
116+
{ key: 'billing', label: 'Billing', defaultExpanded: false },
117+
{ key: 'system', label: 'System', visibleOn: '$user.isAdmin' },
118+
],
119+
120+
fields: {
121+
name: { type: 'text', required: true, group: 'contact_info' },
122+
email: { type: 'email', group: 'contact_info' },
123+
phone: { type: 'phone', group: 'contact_info' },
124+
vat_id: { type: 'text', group: 'billing' },
125+
billing_address: { type: 'address', group: 'billing' },
126+
created_at: { type: 'datetime', readonly: true, group: 'system' },
127+
created_by: { type: 'lookup', reference: 'user', readonly: true, group: 'system' },
128+
},
129+
});
130+
```
131+
132+
**Supported migrations at this layer:** add / rename / delete / reorder groups
133+
(edit the `fieldGroups` array), assign a field to a group (edit `Field.group`).
134+
Explicit per-field in-group ordering is deferred to a future iteration.
135+
136+
---
137+
95138
## Quick Reference — Detailed Rules
96139

97140
For comprehensive documentation with incorrect/correct examples:
@@ -299,7 +342,7 @@ When extending an object you do not own:
299342
- [rules/indexing.md](./rules/indexing.md) — Index types, composite/partial strategies
300343
- [rules/hooks.md](./rules/hooks.md) — Data lifecycle hooks quick reference (→ [objectstack-hooks](../objectstack-hooks/references/data-hooks.md))
301344
- [references/data/field.zod.ts](./references/data/field.zod.ts) — FieldType enum, FieldSchema
302-
- [references/data/object.zod.ts](./references/data/object.zod.ts) — ObjectSchema, capabilities
345+
- [references/data/object.zod.ts](./references/data/object.zod.ts) — ObjectSchema, capabilities, `ObjectFieldGroupSchema`
303346
- [references/data/validation.zod.ts](./references/data/validation.zod.ts) — Validation rule types
304347
- [references/data/hook.zod.ts](./references/data/hook.zod.ts) — Hook schema, HookContext
305348
- [Schema index](./references/_index.md) — All bundled schemas with dependency tree

skills/objectstack-schema/references/_index.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88

99
- [`data/datasource.zod.ts`](./data/datasource.zod.ts) — Driver Identifier
1010
- [`data/field.zod.ts`](./data/field.zod.ts) — Field Type Enum
11-
- [`data/filter.zod.ts`](./data/filter.zod.ts) — Unified Query DSL Specification
1211
- [`data/hook.zod.ts`](./data/hook.zod.ts) — Hook Lifecycle Events
1312
- [`data/object.zod.ts`](./data/object.zod.ts) — API Operations Enum
14-
- [`data/query.zod.ts`](./data/query.zod.ts) — Sort Node
1513
- [`data/validation.zod.ts`](./data/validation.zod.ts) — ObjectStack Validation Protocol
1614
- [`security/permission.zod.ts`](./security/permission.zod.ts) — Entity (Object) Level Permissions
1715

skills/objectstack-schema/references/data/object.zod.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,68 @@ export const CDCConfigSchema = z.object({
211211
destination: z.string().describe('Destination endpoint (e.g., "kafka://topic", "webhook://url")'),
212212
});
213213

214+
/**
215+
* Object Field Group Schema — MVP (data-layer protocol)
216+
*
217+
* Declares the set of logical field groups for an object. A group bundles
218+
* related fields together for presentation in forms, detail pages, and
219+
* editors (e.g., "Contact Info", "Billing", "System").
220+
*
221+
* Design rules (MVP):
222+
* - Group **order** is the declaration order of this array — no `order` property.
223+
* - Field → group mapping is derived automatically from `Field.group`
224+
* matching `ObjectFieldGroup.key`; the **in-group display order** equals
225+
* the traversal order of `ObjectSchema.fields`.
226+
* - Fields whose `group` is unset (or references an undeclared key) are
227+
* considered ungrouped and must be rendered by consumers in a default
228+
* bucket after the declared groups, preserving their field declaration order.
229+
* - Extension packages and runtime code use `Field.group` to assign fields
230+
* to an existing group — no per-field order property is introduced at this
231+
* layer.
232+
*
233+
* Migration operations supported by this MVP:
234+
* - add / rename / delete / reorder groups (via the array)
235+
* - assign an existing field to a group (via `Field.group`)
236+
*
237+
* Deferred (not part of MVP):
238+
* - explicit per-field in-group ordering
239+
* - nested groups / sub-groups
240+
* - permission-scoped group visibility beyond `visibleOn`
241+
*
242+
* @example
243+
* ```ts
244+
* fieldGroups: [
245+
* { key: 'contact_info', label: 'Contact Information', icon: 'user' },
246+
* { key: 'billing', label: 'Billing', defaultExpanded: false },
247+
* { key: 'system', label: 'System', visibleOn: '$user.isAdmin' },
248+
* ]
249+
* ```
250+
*/
251+
export const ObjectFieldGroupSchema = z.object({
252+
/** Group key — referenced by `Field.group` to assign a field to this group. Must be snake_case. */
253+
key: z.string().regex(/^[a-z_][a-z0-9_]*$/, {
254+
message: 'Field group key must be lowercase snake_case (e.g., "contact_info", "billing", "system")',
255+
}).describe('Group machine key (snake_case). Referenced by Field.group.'),
256+
257+
/** Human-readable label displayed as the group header. */
258+
label: z.string().describe('Group display label'),
259+
260+
/** Optional Lucide/Material icon name for the group header. */
261+
icon: z.string().optional().describe('Icon name (Lucide/Material) for the group header'),
262+
263+
/** Optional description / help text shown under the group header. */
264+
description: z.string().optional().describe('Optional description shown under the group header'),
265+
266+
/** Whether the group is expanded by default. Defaults to `true`. */
267+
defaultExpanded: z.boolean().optional().default(true).describe('Whether the group is expanded by default'),
268+
269+
/** Optional visibility expression — when false, the entire group is hidden (e.g., "$user.isAdmin", "status == \'closed\'"). */
270+
visibleOn: z.string().optional().describe('Visibility condition expression; when false the group is hidden'),
271+
});
272+
273+
export type ObjectFieldGroup = z.infer<typeof ObjectFieldGroupSchema>;
274+
export type ObjectFieldGroupInput = z.input<typeof ObjectFieldGroupSchema>;
275+
214276
/**
215277
* Base Object Schema Definition
216278
*
@@ -282,6 +344,23 @@ const ObjectSchemaBase = z.object({
282344
message: 'Field names must be lowercase snake_case (e.g., "first_name", "company", "annual_revenue")',
283345
}), FieldSchema).describe('Field definitions map. Keys must be snake_case identifiers.'),
284346
indexes: z.array(IndexSchema).optional().describe('Database performance indexes'),
347+
348+
/**
349+
* Field Groups (MVP)
350+
*
351+
* Declares logical groups for presenting fields in forms and detail
352+
* pages. The **array order is the group display order**. Each field's
353+
* `Field.group` references an entry's `key` to assign it to a group;
354+
* within a group, fields are displayed in their `ObjectSchema.fields`
355+
* declaration order.
356+
*
357+
* See {@link ObjectFieldGroupSchema} for the full MVP contract and
358+
* deferred features.
359+
*/
360+
fieldGroups: z.array(ObjectFieldGroupSchema).refine(
361+
(groups) => new Set(groups.map(g => g.key)).size === groups.length,
362+
{ message: 'fieldGroups[].key must be unique within an object' },
363+
).optional().describe('Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema.'),
285364

286365
/**
287366
* Advanced Data Management

skills/objectstack-ui/references/ui/dashboard.zod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ export const DashboardSchema = z.object({
253253
/** Collection of widgets */
254254
widgets: z.array(DashboardWidgetSchema).describe('Widgets to display'),
255255

256+
/** Grid column count — defaults to 12 for a standard 12-column grid */
257+
columns: z.number().int().min(1).max(24).optional().describe('Number of grid columns (default 12)'),
258+
259+
/** Grid gap in Tailwind spacing units (e.g. 4 = 1rem) */
260+
gap: z.number().int().min(0).optional().describe('Grid gap in Tailwind spacing units'),
261+
256262
/** Auto-refresh */
257263
refreshInterval: z.number().optional().describe('Auto-refresh interval in seconds'),
258264

0 commit comments

Comments
 (0)