Skip to content

Commit 1ba9d14

Browse files
committed
Merge branch 'main' into showcase-examples
2 parents 0774833 + 2a01821 commit 1ba9d14

253 files changed

Lines changed: 5294 additions & 3285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
feat(spec): model action-param translations in TranslationData (`_actions.params`) so action param label/helpText/placeholder/options can be localized via the keys+bundles path. Additive and optional — existing bundles unaffected.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
auth: expose `isPlatformAdmin` on the customSession user payload
6+
7+
The session already derives a coarse `admin` role for platform admins or
8+
active-org admins, but never surfaced the underlying platform-admin signal.
9+
Console action `visible` CEL predicates need it to gate platform-admin-only
10+
object actions (e.g. `sys_environment.change_plan`) without hiding org-admin
11+
actions. Both `customSession` return paths now carry the boolean; org-admins
12+
who are not platform admins correctly get `isPlatformAdmin: false`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/rest": minor
5+
"@objectstack/runtime": patch
6+
---
7+
8+
feat(book): documentation navigation as a `book` element — spine + derived membership (ADR-0046 §6)
9+
10+
Adds the `book` metadata element: a navigation **spine** (ordered groups + `audience` + identity) whose membership is **derived** by rule (`include` glob/tag) plus optional per-doc `order`/`group`, never a central array. This keeps AI authoring create-and-forget (no central-array read-modify-write) and runtime overlay merge-safe (RFC 7396 treats arrays atomically).
11+
12+
- `BookSchema` + `resolveBookTree()` derived-membership resolver + `defineBook()` + additive `doc.order`/`doc.group`.
13+
- Register `book` as a render-time metadata type (`allowOrgOverride: true`); wire it through the runtime type enumerations (PLURAL_TO_SINGULAR, engine registration, artifact field map, type-schema map).
14+
- REST `GET /meta/book/:name/tree` resolves the tree; read-layer `audience` gating (`public` ≡ anonymous; `org`/`{profile}` require sign-in).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): drop stale `ownership` key from the `os init` scaffold object template
6+
7+
The `app` and `plugin` scaffold templates emitted `ownership: 'own'` on the starter object. `ownership` is no longer a valid `ObjectSchema` field (it's not in `ObjectSchemaBase`, and `ObjectSchema.create()` rejects unknown top-level keys per ADR-0032 / #1535), so a user migrating the scaffolded object into `ObjectSchema.create({...})` would hit a validation error. Removed the key from both templates; the rest of the scaffold output is unchanged.

.changeset/studio-action-body-composite.md

Lines changed: 0 additions & 7 deletions
This file was deleted.

.claude/launch.json

Lines changed: 0 additions & 52 deletions
This file was deleted.

content/docs/concepts/architecture.mdx

Lines changed: 117 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -143,65 +143,65 @@ That's the job of the other layers.
143143
### Example: Permission Rules
144144

145145
```typescript
146-
// packages/crm/src/permissions/customer.permission.ts
147-
import { Permission } from '@objectstack/spec';
148-
149-
export const CustomerPermission = Permission({
150-
object: 'customer',
151-
rules: [
152-
{
153-
profile: 'sales_rep',
154-
crud: {
155-
create: true,
156-
read: true,
157-
update: true,
158-
delete: false, // Only managers can delete
159-
},
160-
fieldPermissions: {
161-
annual_revenue: { read: true, edit: false }, // Read-only
162-
},
146+
// packages/crm/src/permissions/sales_rep.permission.ts
147+
import type { PermissionSet } from '@objectstack/spec/security';
148+
149+
// A permission set is keyed by object name. Each object grants the four
150+
// CRUD verbs (allowCreate / allowRead / allowEdit / allowDelete), and
151+
// optional field-level security keyed by "object.field".
152+
export const SalesRep: PermissionSet = {
153+
name: 'sales_rep',
154+
isProfile: true,
155+
objects: {
156+
customer: {
157+
allowCreate: true,
158+
allowRead: true,
159+
allowEdit: true,
160+
allowDelete: false, // Only managers can delete
163161
},
164-
{
165-
profile: 'sales_manager',
166-
crud: {
167-
create: true,
168-
read: true,
169-
update: true,
170-
delete: true,
171-
},
172-
},
173-
],
174-
});
162+
},
163+
fields: {
164+
'customer.annual_revenue': { readable: true, editable: false }, // Read-only
165+
},
166+
};
175167
```
176168

177169
### Example: Workflow Automation
178170

179-
```typescript
180-
// packages/crm/src/workflows/customer.workflow.ts
181-
import { Workflow } from '@objectstack/spec';
171+
Automations are authored with `defineFlow`. A flow is a **node graph**
172+
a typed `start`/`end` plus decision and action nodes wired together by
173+
`edges` — driven by a trigger `type` (here `record_change`):
182174

183-
export const CustomerWorkflow = Workflow({
184-
object: 'customer',
185-
trigger: 'after_create',
186-
conditions: [
187-
{ field: 'annual_revenue', operator: 'greaterThan', value: 1000000 },
175+
```typescript
176+
// packages/crm/src/flows/high_value_customer.flow.ts
177+
import { defineFlow } from '@objectstack/spec';
178+
179+
export const HighValueCustomer = defineFlow({
180+
name: 'high_value_customer',
181+
label: 'High-Value Customer Routing',
182+
type: 'record_change', // runs when a customer record changes
183+
nodes: [
184+
{ id: 'start', type: 'start', label: 'Customer created' },
185+
{ id: 'assign', type: 'action', label: 'Assign to enterprise team' },
186+
{ id: 'notify', type: 'action', label: 'Alert sales leadership' },
187+
{ id: 'end', type: 'end', label: 'End' },
188188
],
189-
actions: [
190-
{
191-
type: 'assign_owner',
192-
params: { owner: 'enterprise_sales_team' },
193-
},
194-
{
195-
type: 'send_email',
196-
params: {
197-
template: 'high_value_customer_alert',
198-
to: 'sales-leadership@company.com',
199-
},
200-
},
189+
edges: [
190+
// Only branch when annual_revenue > 1,000,000 (CEL condition)
191+
{ id: 'e1', source: 'start', target: 'assign', condition: 'record.annual_revenue > 1000000' },
192+
{ id: 'e2', source: 'assign', target: 'notify' },
193+
{ id: 'e3', source: 'notify', target: 'end' },
201194
],
202195
});
203196
```
204197

198+
<Callout type="info">
199+
The node/edge wiring above is illustrative. For the full set of node types,
200+
trigger semantics, and condition syntax, see the
201+
[automation skill](/docs/protocol/objectos) and the `defineFlow` schema in
202+
`@objectstack/spec/automation`.
203+
</Callout>
204+
205205
ObjectOS **orchestrates** these rules at runtime, independent of the data structure or UI.
206206

207207
## Layer 3: ObjectUI (View Protocol)
@@ -220,63 +220,47 @@ ObjectOS **orchestrates** these rules at runtime, independent of the data struct
220220
### Example: List View
221221

222222
```typescript
223-
// packages/crm/src/views/customer_list.view.ts
224-
import { ListView } from '@objectstack/spec';
223+
// packages/crm/src/views/customer.view.ts
224+
import { defineView } from '@objectstack/spec';
225225

226-
export const CustomerListView = ListView({
226+
// A view is a container bound to an object; its `list` slot holds the
227+
// list-view config. Columns are field names; sort is { field, order }.
228+
export const CustomerView = defineView({
227229
object: 'customer',
228-
label: 'All Customers',
229-
type: 'grid',
230-
columns: [
231-
{ field: 'name', width: 200 },
232-
{ field: 'industry', width: 150 },
233-
{ field: 'annual_revenue', width: 150 },
234-
{ field: 'primary_contact', width: 180 },
235-
],
236-
filters: [
237-
{ field: 'industry', operator: 'equals' },
238-
{ field: 'annual_revenue', operator: 'greaterThan' },
239-
],
240-
defaultSort: { field: 'name', direction: 'asc' },
230+
list: {
231+
label: 'All Customers',
232+
type: 'grid',
233+
columns: ['name', 'industry', 'annual_revenue', 'primary_contact'],
234+
sort: [{ field: 'name', order: 'asc' }],
235+
},
241236
});
242237
```
243238

244239
### Example: Form View
245240

246241
```typescript
247-
// packages/crm/src/views/customer_form.view.ts
248-
import { FormView } from '@objectstack/spec';
242+
// packages/crm/src/views/customer.view.ts
243+
import { defineView } from '@objectstack/spec';
249244

250-
export const CustomerFormView = FormView({
245+
// The same view container can also carry a `form` slot. A form is laid out
246+
// with `sections`, each section listing the fields it renders.
247+
export const CustomerView = defineView({
251248
object: 'customer',
252-
label: 'Customer Details',
253-
type: 'tabbed',
254-
tabs: [
255-
{
256-
label: 'Overview',
257-
sections: [
258-
{
259-
label: 'Company Information',
260-
fields: ['name', 'industry', 'annual_revenue'],
261-
},
262-
{
263-
label: 'Contact',
264-
fields: ['primary_contact'],
265-
},
266-
],
267-
},
268-
{
269-
label: 'Related Records',
270-
sections: [
271-
{
272-
label: 'Opportunities',
273-
component: 'related_list',
274-
object: 'opportunity',
275-
filter: { customer: '$recordId' },
276-
},
277-
],
278-
},
279-
],
249+
form: {
250+
label: 'Customer Details',
251+
type: 'tabbed',
252+
sections: [
253+
{
254+
label: 'Company Information',
255+
columns: 2,
256+
fields: ['name', 'industry', 'annual_revenue'],
257+
},
258+
{
259+
label: 'Contact',
260+
fields: ['primary_contact'],
261+
},
262+
],
263+
},
280264
});
281265
```
282266

@@ -290,6 +274,13 @@ The UI doesn't "know" the field types. It asks ObjectQL for the schema and rende
290274

291275
Let's trace a **real-world scenario**: A sales rep creates a new high-value customer.
292276

277+
<Callout type="info">
278+
The snippets in this walkthrough (`Auth.getCurrentUser`, `Permission.check`,
279+
`ObjectQL.getSchema`, `driver.insert`, `Workflow.getTriggersFor`, …) are
280+
**conceptual pseudo-code** that illustrate the control flow between layers.
281+
They are not literal exported APIs — don't search for these exact symbols.
282+
</Callout>
283+
293284
### Step 1: User Action (ObjectUI)
294285

295286
```
@@ -431,32 +422,45 @@ export const Opportunity = ObjectSchema.create({
431422
### 2. ObjectOS: Define Business Rules
432423

433424
```typescript
434-
export const OpportunityWorkflow = Workflow({
435-
object: 'opportunity',
436-
trigger: 'field_update',
437-
conditions: [
438-
{ field: 'stage', operator: 'equals', value: 'closed_won' },
425+
import { defineFlow } from '@objectstack/spec';
426+
427+
export const OpportunityWon = defineFlow({
428+
name: 'opportunity_won',
429+
label: 'On Opportunity Won',
430+
type: 'record_change',
431+
nodes: [
432+
{ id: 'start', type: 'start', label: 'Stage changed' },
433+
{ id: 'invoice', type: 'action', label: 'Create invoice' },
434+
{ id: 'notify', type: 'action', label: 'Notify sales team' },
435+
{ id: 'end', type: 'end', label: 'End' },
439436
],
440-
actions: [
441-
{ type: 'create_invoice', params: { object: 'invoice' } },
442-
{ type: 'send_notification', params: { to: 'sales_team' } },
437+
edges: [
438+
{ id: 'e1', source: 'start', target: 'invoice', condition: "record.stage == 'closed_won'" },
439+
{ id: 'e2', source: 'invoice', target: 'notify' },
440+
{ id: 'e3', source: 'notify', target: 'end' },
443441
],
444442
});
445443
```
446444

447445
### 3. ObjectUI: Define the Kanban View
448446

449447
```typescript
450-
export const OpportunityKanban = ListView({
448+
import { defineView } from '@objectstack/spec';
449+
450+
// For a kanban list, set type: 'kanban' and put the board config under
451+
// `kanban` (groupByField selects the column axis). `columns` is the list
452+
// of card fields. Drag-and-drop between columns is built in.
453+
export const OpportunityBoard = defineView({
451454
object: 'opportunity',
452-
type: 'kanban',
453-
groupBy: 'stage',
454-
columns: [
455-
{ field: 'title' },
456-
{ field: 'amount' },
457-
{ field: 'customer' },
458-
],
459-
enableDragDrop: true,
455+
list: {
456+
type: 'kanban',
457+
columns: ['title', 'amount', 'customer'],
458+
kanban: {
459+
groupByField: 'stage',
460+
summarizeField: 'amount',
461+
columns: ['title', 'amount'],
462+
},
463+
},
460464
});
461465
```
462466

@@ -546,7 +550,7 @@ The three protocols are **loosely coupled** but **tightly integrated**:
546550

547551
## Next Steps
548552

549-
- [ObjectQL: Data Protocol]((/docs/protocol/objectql)) - Full data protocol specification
553+
- [ObjectQL: Data Protocol](/docs/protocol/objectql) - Full data protocol specification
550554
- [ObjectUI: UI Protocol](/docs/protocol/objectui) - Full view protocol specification
551555
- [ObjectOS: System Protocol](/docs/protocol/objectos) - Full control protocol specification
552556
- [Developer Guide](/docs/getting-started/quick-start) - Build your first ObjectStack application

0 commit comments

Comments
 (0)