diff --git a/content/docs/concepts/architecture.mdx b/content/docs/concepts/architecture.mdx index 9d765b0e3b..914025574a 100644 --- a/content/docs/concepts/architecture.mdx +++ b/content/docs/concepts/architecture.mdx @@ -95,6 +95,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Customer = ObjectSchema.create({ name: 'customer', + sharingModel: 'private', label: 'Customer', icon: 'building', @@ -401,6 +402,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Opportunity = ObjectSchema.create({ name: 'opportunity', + sharingModel: 'private', label: 'Opportunity', icon: 'target', diff --git a/content/docs/concepts/metadata-driven.mdx b/content/docs/concepts/metadata-driven.mdx index 74aa5cfb2e..3e313bd46c 100644 --- a/content/docs/concepts/metadata-driven.mdx +++ b/content/docs/concepts/metadata-driven.mdx @@ -79,6 +79,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const User = ObjectSchema.create({ name: 'user', + sharingModel: 'private', label: 'User', icon: 'user', @@ -243,6 +244,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Task = ObjectSchema.create({ name: 'task', + sharingModel: 'private', label: 'Task', icon: 'check-square', @@ -341,6 +343,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Account = ObjectSchema.create({ name: 'account', + sharingModel: 'private', label: 'Account', fields: { name: Field.text({ required: true }), @@ -387,6 +390,7 @@ Follow these strict naming conventions for consistency: // ✅ Correct naming export const TodoTask = ObjectSchema.create({ name: 'todo_task', // snake_case machine name + sharingModel: 'private', // OWD — required on custom objects label: 'Todo Task', fields: { @@ -463,6 +467,7 @@ annual_revenue: Field.currency({ ```typescript export const Account = ObjectSchema.create({ name: 'account', + sharingModel: 'private', label: 'Account', fields: { /* ... */ }, @@ -470,7 +475,7 @@ export const Account = ObjectSchema.create({ enable: { trackHistory: true, // Enable field history tracking searchable: true, // Include in global search - apiEnabled: true, // Expose via REST/GraphQL + apiEnabled: true, // Expose object via automatic APIs (REST) files: true, // Enable file attachments feeds: true, // Enable activity feed activities: true, // Enable tasks and events @@ -486,6 +491,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const ExampleObject = ObjectSchema.create({ name: 'example_object', // Required: snake_case + sharingModel: 'private', // Required on custom objects: the OWD baseline label: 'Example Object', // Required: Human-readable pluralLabel: 'Example Objects', // Optional icon: 'box', // Optional: Lucide icon name diff --git a/content/docs/data-modeling/drivers.mdx b/content/docs/data-modeling/drivers.mdx index 93ceb39953..1d9153446f 100644 --- a/content/docs/data-modeling/drivers.mdx +++ b/content/docs/data-modeling/drivers.mdx @@ -905,6 +905,7 @@ Then in your object definition: ```typescript export const AuditLog = ObjectSchema.create({ name: 'audit_log', + sharingModel: 'private', datasource: 'analytics', // Routes to the analytics database fields: { /* ... */ }, }); diff --git a/content/docs/data-modeling/external-datasources.mdx b/content/docs/data-modeling/external-datasources.mdx index 1b950f860d..bb879d9ffd 100644 --- a/content/docs/data-modeling/external-datasources.mdx +++ b/content/docs/data-modeling/external-datasources.mdx @@ -72,6 +72,7 @@ and `external.columnMap`. ```typescript export const Customer = ObjectSchema.create({ name: 'ext_customer', + sharingModel: 'private', datasource: 'warehouse', external: { remoteName: 'customers', // remote TABLE name (object name may differ) @@ -255,6 +256,10 @@ ObjectSchema.create({ /* ... */ external: { remoteName: 'orders', writable: true With either gate off, insert/update/delete on the federated object is rejected. +The `/* ... */` above still has to carry a `sharingModel` — a custom (non-`sys_`) object +that declares no OWD is an **`os validate` error** (`security-owd-unset`), federated or +not. See the full object above. + **This gate is federation-only — it does nothing on a managed datasource.** `allowWrites` answers *who owns this external database*, not *is this connection diff --git a/content/docs/data-modeling/field-types.mdx b/content/docs/data-modeling/field-types.mdx index a3da1c1714..a0b9c3c18a 100644 --- a/content/docs/data-modeling/field-types.mdx +++ b/content/docs/data-modeling/field-types.mdx @@ -314,7 +314,7 @@ Reference to a record in another object (foreign key). | Property | Type | Default | Description | |:---|:---|:---|:---| | `reference` | `string` | **required** | Target object name (snake_case) | -| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property (unknown keys are stripped by the schema). Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) | +| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) | | `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'set_null'` | Behavior when referenced record is deleted. On a *required* lookup `set_null` is escalated to `restrict`, since a NOT NULL foreign key cannot be cleared — **whether the `set_null` was defaulted or written out explicitly**. On a `multiple: true` required lookup the escalation is judged per referencing row: only a row the member removal would leave EMPTY is refused. `cascade` and `restrict` are the values honored as written. Where `set_null` does run, a `multiple: true` lookup loses only the deleted **member** — the other members are kept, and a set emptied that way is stored as `[]`, never `null` | ```typescript @@ -336,7 +336,7 @@ Parent-child relationship (cascading delete by default). | Property | Type | Default | Description | |:---|:---|:---|:---| | `reference` | `string` | **required** | Target (master) object name | -| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property (unknown keys are stripped by the schema). Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) | +| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) | | `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'cascade'` | Behavior when parent is deleted. `restrict` is the only value that deviates: master-detail cascades on everything else, so an explicit `set_null` here is **not** honored — the child is deleted with the parent | | `inlineEdit` | `boolean \| 'grid' \| 'form'` | — | Edit child records inline on the parent create/edit form (`true` = auto-pick, `'grid'`, or `'form'`) | | `inlineColumns` | `array` | — | Optional explicit inline grid columns | @@ -530,7 +530,7 @@ Name-keyed map of embedded sub-objects (`Record`). Insertion ## Enhanced Types ### `location` -Geographic coordinates. Stored as `{ latitude, longitude, altitude?, accuracy? }` (latitude −90..90, longitude −180..180). No per-type config properties. +Geographic coordinates. Stored as `{ lat, lng, altitude?, accuracy? }` (`lat` −90..90, `lng` −180..180). No per-type config properties. The key names are `lat`/`lng`, not `latitude`/`longitude` — see `LocationValueSchema` in `field-value.zod.ts` (ADR-0104 D1). ```typescript { name: 'headquarters', label: 'Location', type: 'location' } diff --git a/content/docs/data-modeling/fields.mdx b/content/docs/data-modeling/fields.mdx index d14374b6b8..6fa5b294d2 100644 --- a/content/docs/data-modeling/fields.mdx +++ b/content/docs/data-modeling/fields.mdx @@ -17,6 +17,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Contact = ObjectSchema.create({ name: 'contact', + sharingModel: 'private', label: 'Contact', fields: { first_name: Field.text({ label: 'First Name', required: true }), diff --git a/content/docs/data-modeling/formulas.mdx b/content/docs/data-modeling/formulas.mdx index 4d526d1190..43c0cc7126 100644 --- a/content/docs/data-modeling/formulas.mdx +++ b/content/docs/data-modeling/formulas.mdx @@ -93,6 +93,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Invoice = ObjectSchema.create({ name: 'invoice', + sharingModel: 'private', nameField: 'display_title', // ADR-0079 — the record title is a designated field; composite titles migrate off the deprecated `titleFormat` to a text formula fields: { // Composite record title as a text formula, surfaced via `nameField` above. diff --git a/content/docs/data-modeling/index.mdx b/content/docs/data-modeling/index.mdx index 8e8782127f..65309177e9 100644 --- a/content/docs/data-modeling/index.mdx +++ b/content/docs/data-modeling/index.mdx @@ -15,6 +15,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Lead = ObjectSchema.create({ name: 'crm_lead', + sharingModel: 'public_read_write', // OWD — required on every custom object label: 'Lead', pluralLabel: 'Leads', icon: 'funnel', @@ -35,8 +36,8 @@ That one definition is enough to get a persisted table, CRUD + query endpoints, - **A full spectrum of field types** — from `text`, `currency`, and `lookup`/`master_detail` relationships to `formula`, `summary`, `signature`, and `vector` (with `dimensions` config for AI embeddings). See the [Field Types gallery](/docs/data-modeling/field-types). - **Validation as metadata** — required/format rules, CEL script validation with access to `previous.`, uniqueness via indexes, and severity levels — enforced identically in API, UI, and automation. - **CEL expressions** — formula fields and computed defaults share one expression language ([Expressions](/docs/data-modeling/formulas)). -- **A compiled query AST** — queries are JSON documents validated against the protocol, then compiled by a driver into native queries with joins, aggregations, window functions, HAVING, and subqueries. The [query cheat sheet](/docs/data-modeling/queries) covers the syntax; the [spec](/docs/protocol/objectql/query-syntax) is normative. -- **Four database drivers in this repo** — `driver-sql` (PostgreSQL / MySQL / SQLite via Knex), `driver-mongodb`, `driver-memory` (in-memory, for tests and demos), and `driver-sqlite-wasm` (SQLite in the browser / WebContainers). The same model runs unchanged on any of them. +- **A compiled query AST** — queries are JSON documents validated against the protocol, then compiled by a driver into native queries with aggregations, `groupBy`, `HAVING`, and subqueries. `joins` and request-surface `windowFunctions` were **retired in protocol 17** (#4286, ADR-0049) and are now rejected at parse; window functions survive only as a SQL-driver door (`SqlDriver.findWithWindowFunctions()`), not on the `IDataDriver` contract. The [query cheat sheet](/docs/data-modeling/queries) covers the syntax; the [spec](/docs/protocol/objectql/query-syntax) is normative. +- **Five database drivers in this repo** — `driver-sql` (PostgreSQL / MySQL / SQLite via Knex), `driver-mongodb`, `driver-memory` (in-memory, for tests and demos), `driver-sqlite-wasm` (SQLite in the browser / WebContainers), and `driver-turso` (Turso / libSQL — an **optional** install, because it pulls `@libsql/client` plus native bindings; see [Database Drivers](/docs/data-modeling/drivers)). The same model runs unchanged on any of them. - **External datasource federation** — introspect an existing external database, import selected tables into the catalog, and query them alongside native objects ([External Datasources](/docs/data-modeling/external-datasources)). - **Composable ownership** — one package owns an object, and any other package can merge fields, validation rules, and indexes into it without forking the definition ([Object Extensions](/docs/data-modeling/object-extensions)). - **Repeatable bulk import** — a named mapping projects someone else's column headers and codes onto your fields, and the import endpoint applies it by name ([Import Mappings](/docs/data-modeling/import-mappings)). diff --git a/content/docs/data-modeling/objects.mdx b/content/docs/data-modeling/objects.mdx index ed08813c1e..06ea4cbaaf 100644 --- a/content/docs/data-modeling/objects.mdx +++ b/content/docs/data-modeling/objects.mdx @@ -15,6 +15,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Account = ObjectSchema.create({ name: 'account', + sharingModel: 'private', label: 'Account', pluralLabel: 'Accounts', icon: 'building', @@ -79,7 +80,7 @@ Control which platform features are active for this object: enable: { trackHistory: true, // Field history tracking for audit searchable: true, // Include in global search index - apiEnabled: true, // Expose via REST/GraphQL APIs + apiEnabled: true, // Expose object via automatic APIs (REST) apiMethods: ['get', 'list', 'create', 'update', 'delete'], files: true, // File attachments feeds: true, // Activity feed and comments @@ -373,6 +374,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const ProjectTask = ObjectSchema.create({ name: 'project_task', + sharingModel: 'private', label: 'Project Task', pluralLabel: 'Project Tasks', icon: 'check-square', diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx index 15b333f210..c2bed2ca48 100644 --- a/content/docs/data-modeling/schema-design.mdx +++ b/content/docs/data-modeling/schema-design.mdx @@ -21,6 +21,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const MyObject = ObjectSchema.create({ // Metadata name: 'my_object', // Machine name (snake_case) + sharingModel: 'private', // OWD baseline — REQUIRED on custom objects label: 'My Object', // Display name pluralLabel: 'My Objects', // Plural form icon: 'briefcase', // Icon identifier @@ -67,13 +68,14 @@ Control which features are available for an object: enable: { trackHistory: true, // Track field changes over time searchable: true, // Include in global search - apiEnabled: true, // Expose via REST/GraphQL + apiEnabled: true, // Expose object via automatic APIs (REST) apiMethods: [ // Whitelist API operations — six primitives only; 'get', // search/export/… DERIVE from them (list grants 'list', // aggregate/search/export; create+update grants 'create', // upsert/import). undefined = all, [] = none. 'update', - 'delete' + 'delete', + 'bulk' // omitting `bulk` DENIES batch operations ], files: true, // Allow file attachments feeds: true, // Enable activity feed (Chatter-like) @@ -284,6 +286,7 @@ import { ObjectSchema } from '@objectstack/spec/data'; export const Account = ObjectSchema.create({ name: 'account', + sharingModel: 'private', label: 'Account', fieldGroups: [ @@ -403,6 +406,7 @@ here (ADR-0085). ```typescript export const Account = ObjectSchema.create({ name: 'account', + sharingModel: 'private', label: 'Account', pluralLabel: 'Accounts', icon: 'building', diff --git a/content/docs/data-modeling/seed-data.mdx b/content/docs/data-modeling/seed-data.mdx index 41b00eb40c..6a08f1b002 100644 --- a/content/docs/data-modeling/seed-data.mdx +++ b/content/docs/data-modeling/seed-data.mdx @@ -246,6 +246,7 @@ seed value is an **array of natural keys**. Each element resolves independently. ```typescript const Book = ObjectSchema.create({ name: 'book', + sharingModel: 'private', fields: { name: Field.text({ label: 'Title', required: true }), authors: Field.lookup('author', { label: 'Authors', multiple: true }), diff --git a/content/docs/data-modeling/validation-rules.mdx b/content/docs/data-modeling/validation-rules.mdx index 4bbb72f7d9..ef13e81639 100644 --- a/content/docs/data-modeling/validation-rules.mdx +++ b/content/docs/data-modeling/validation-rules.mdx @@ -91,6 +91,15 @@ These properties apply to **all** field types and are validated by the base `Fie **Default constraints:** Validated the same as `text` (`maxLength`/`minLength` only). On a generic (non-`better-auth`) object a `password`-typed value is stored **plaintext at rest** but **masked to `••••••••` on read** through the normal query path (ADR-0100) — the engine does not hash or encrypt it. One-way hashing and verification of real login credentials are owned entirely by the auth subsystem (better-auth); its own credential column (`sys_account.password`) is a hashed `Field.text()` column that is exempt from this masking. For a reversible, encrypted-at-rest value on your own objects, use the `secret` type instead. +### `secret` + +| Property | Type | Default | Validation Behavior | +|:---|:---|:---|:---| +| `maxLength` | `number` | — | Maximum length of the cleartext value | +| `minLength` | `number` | — | Minimum length of the cleartext value | + +**Default constraints:** Reversible, encrypted-at-rest value (DB password, API key, token) — ADR-0100. **Fail-closed:** with no `ICryptoProvider` registered, a write **throws** rather than persisting cleartext. The value is encrypted on write into a `sys_secret` row, only an opaque handle is persisted on the record, and reads are masked. Distinct from `password`, which is plaintext at rest (or one-way hashed inside the auth subsystem). + --- ## Rich Content Types @@ -144,7 +153,7 @@ These properties apply to **all** field types and are validated by the base `Fie | `currencyConfig.currencyMode` | `enum` | `dynamic` | `dynamic` (user-selectable) or `fixed` (single currency) | | `currencyConfig.defaultCurrency` | `string` | `CNY` | 3-character currency code (ISO 4217 or crypto) | -**Default constraints:** Stored as `{ value, currency }` pair. Precision defaults to 2 decimal places. +**Default constraints:** Stored as a **bare number** (a finite numeric scalar — `valueSchemaFor` routes `currency` to `z.number().finite()`); there is no `{ value, currency }` envelope on the value path. The per-record currency **code** is a separate concern, carried by `currencyConfig` above. Precision defaults to 2 decimal places. ### `percent` @@ -264,6 +273,15 @@ update that omits the field never fails this check, so legacy rows stay editable application-level emptiness check is needed. +### `user` + +| Property | Type | Default | Validation Behavior | +|:---|:---|:---|:---| +| `multiple` | `boolean` | `false` | `true` stores a JSON array of user ids | +| `defaultValue` | `'current_user'` | — | Stamps the acting user's id on insert | + +**Default constraints:** Person picker — a `lookup` specialized to the built-in `sys_user` object, so `reference` is implied and must not be authored. Stored as a foreign key to `sys_user.id` and resolved through the same `$expand` machinery as `lookup`; with `multiple: true` the stored value is a JSON array of ids. + ### `master_detail` | Property | Type | Default | Validation Behavior | @@ -366,11 +384,32 @@ application-level emptiness check is needed. --- +## Embedded Structured Types + +These types store structured values as JSON on the parent row — no separate table and no +foreign key. Sub-field shapes are declared on the field, and the stored value is validated +as an open object map (`valueSchemaFor`), so sub-keys are not constrained by the field type +itself. + +### `composite` + +**Default constraints:** Single embedded sub-object. Stored as a JSON object map (`Record`). No per-type config properties. + +### `repeater` + +**Default constraints:** Repeating embedded sub-object array. Stored as a JSON **array** of object maps (`Array>`); a non-array value is rejected. No per-type config properties. + +### `record` + +**Default constraints:** Name-keyed map of embedded sub-objects (`Record`) — ADR-0007. Stored as a JSON object map; insertion order is display order. No per-type config properties. + +--- + ## Enhanced Types ### `location` -**Default constraints:** Stored as `{ latitude, longitude, altitude?, accuracy? }`. Latitude: -90 to 90. Longitude: -180 to 180. No per-type config properties. +**Default constraints:** Stored as `{ lat, lng, altitude?, accuracy? }` — the keys are `lat`/`lng`, not `latitude`/`longitude` (`LocationValueSchema`, ADR-0104 D1). `lat`: -90 to 90. `lng`: -180 to 180. No per-type config properties. ### `address` @@ -483,6 +522,7 @@ section above). See the | `url` | — | Valid URL with protocol | | `phone` | — | Permissive character set, not strict E.164 | | `password` | — | Validated like `text`; masked on read but plaintext at rest (no hashing/encryption) | +| `secret` | — | Encrypted at rest via `sys_secret`, masked on read; **fail-closed** — writes throw with no `ICryptoProvider` | | `markdown` | — | `maxLength` | | `html` | — | Sanitized, `maxLength` | | `richtext` | — | Sanitized, `maxLength` | @@ -499,6 +539,7 @@ section above). See the | `radio` | `options` | Single value from options | | `checkboxes` | `options` | Array of valid option values | | `lookup` | `reference` | Foreign key integrity | +| `user` | — | Lookup specialized to `sys_user`; `multiple: true` stores an id array | | `master_detail` | `reference` | Cascade delete, ownership | | `tree` | `reference` | Self-referencing; no automatic cycle check | | `image` | — | Common image MIME types; `multiple` for many | @@ -509,6 +550,9 @@ section above). See the | `formula` | `expression` | Read-only, computed at runtime | | `summary` | `summaryOperations` | Read-only, roll-up from children | | `autonumber` | — | Read-only, auto-incremented | +| `composite` | — | Single embedded sub-object; stored as a JSON object map | +| `repeater` | — | Embedded sub-object array; stored as a JSON array of object maps | +| `record` | — | Name-keyed map of embedded sub-objects (ADR-0007) | | `location` | — | Lat: -90–90, Lng: -180–180 | | `address` | — | Structured object (street, city, …) | | `code` | — | Plain text, `language` for highlighting | diff --git a/content/docs/data-modeling/validation.mdx b/content/docs/data-modeling/validation.mdx index 52c7e26a59..5b81813359 100644 --- a/content/docs/data-modeling/validation.mdx +++ b/content/docs/data-modeling/validation.mdx @@ -1,6 +1,6 @@ --- title: Validation Metadata -description: Define data integrity rules — formula conditions, uniqueness, format, state machine transitions, and more +description: Define data integrity rules — formula conditions, format, cross-field checks, state machine transitions, and more --- # Validation Metadata @@ -10,7 +10,7 @@ description: Define data integrity rules — formula conditions, uniqueness, for Three patterns that look like validation rules are deliberately **not** rule types, because each needs I/O or is a client-side concern. Use the layer that already does each one correctly: -- **Uniqueness** → a unique index (`ObjectSchema.indexes`, `{ fields, unique: 'organization' | 'global' }` — state the scope, [ADR-0120](/docs/data-modeling/indexing); `partial` for a scoped constraint) or field-level `unique`. A SELECT-then-INSERT rule is inherently racy (TOCTOU); a DB unique constraint is not. +- **Uniqueness** → a unique index (`ObjectSchema.indexes`, `{ fields, unique: 'organization' | 'global' }` — state the scope, [ADR-0120](/docs/data-modeling/indexing)) or field-level `unique`. A SELECT-then-INSERT rule is inherently racy (TOCTOU); a DB unique constraint is not. - **Async / remote validation** → a client-form concern, and an SSRF/latency hazard on the server write path. Keep it in the form layer, or enforce the invariant with a `unique` index / lifecycle hook. - **Custom handler** → a `beforeInsert` / `beforeUpdate` lifecycle hook, the supported extension point for arbitrary validation code. - **Delete-time guards** → a `beforeDelete` lifecycle hook. Validation rules run only on insert/update (a delete carries no record payload to validate), so there is no `'delete'` validation event — block or gate deletions from a `beforeDelete` hook. @@ -26,6 +26,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Order = ObjectSchema.create({ name: 'order', + sharingModel: 'private', label: 'Order', fields: { amount: Field.currency({ label: 'Amount', required: true }), @@ -132,10 +133,15 @@ email: Field.email({ label: 'Contact Email', unique: 'organization' }), // Composite / scoped uniqueness via ObjectSchema.indexes indexes: [ { fields: ['code'], unique: 'organization' }, - // `partial` expresses a scoped/conditional constraint ] ``` +`type` and `partial` were **retired at protocol 17** (#5248, #4943) — `IndexSchema` is a +strict object declaring exactly `name` / `fields` / `unique`, so authoring either is now a +rejection with guidance, not a silent strip. A **partial** index is built at the database +layer (a runtime migration issuing `CREATE UNIQUE INDEX … WHERE`), not on the declaration +surface; see [Objects](/docs/data-modeling/objects). + ### Format Validation Validate against a regex pattern or standard format: diff --git a/content/docs/getting-started/examples.mdx b/content/docs/getting-started/examples.mdx index 47bcb0878d..5688ebd21f 100644 --- a/content/docs/getting-started/examples.mdx +++ b/content/docs/getting-started/examples.mdx @@ -51,12 +51,24 @@ The fastest way to explore all examples at once: ```bash git clone https://github.com/objectstack-ai/objectstack.git cd objectstack -pnpm install +pnpm install # Node 22+, pnpm 8+ (corepack enable) +pnpm build # REQUIRED — nothing builds the workspace implicitly pnpm dev:showcase # or: pnpm dev:todo / pnpm dev:crm ``` Each script starts one example's dev server. `pnpm dev` is an alias for `pnpm dev:showcase`. + + **`pnpm build` is not optional.** The root `dev:*` scripts begin with + `node scripts/check-dev-prereqs.mjs`, which refuses to boot an unbuilt workspace — + every package's `dist/` entry point has to be on disk. There is no `postinstall`, so + `pnpm install` alone leaves the workspace unbuilt. This is the same sequence + `README.md` documents. + + `pnpm build` still does not produce the Console SPA at `/_console/` — that is built + separately by `pnpm objectui:build`. + + --- ## app-todo — Your First App @@ -105,6 +117,9 @@ examples/app-todo/ ```bash +# From a freshly cloned monorepo, build it once first — `objectstack dev` runs +# against the workspace's dist/ output and the root prereq gate does not guard +# this path: pnpm install && pnpm build cd examples/app-todo pnpm dev ``` @@ -127,6 +142,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; export const Task = ObjectSchema.create({ name: 'todo_task', + sharingModel: 'public_read_write', // OWD — required on every custom object label: 'Task', pluralLabel: 'Tasks', icon: 'check-square', @@ -281,6 +297,9 @@ profiles, translations, themes, webhooks, and more. Use it as a living reference when you want to see how a particular metadata type is authored. ```bash +# From a freshly cloned monorepo, build it once first — `objectstack dev` runs +# against the workspace's dist/ output and the root prereq gate does not guard +# this path: pnpm install && pnpm build cd examples/app-showcase pnpm dev ``` diff --git a/content/docs/getting-started/index.mdx b/content/docs/getting-started/index.mdx index 6c5b3dc396..c8ceaadfe7 100644 --- a/content/docs/getting-started/index.mdx +++ b/content/docs/getting-started/index.mdx @@ -96,7 +96,7 @@ Think of ObjectStack as: - **Kubernetes** for business applications - Declarative configuration over imperative code - **Terraform** for data modeling - Infrastructure as code, but for data -- **GraphQL + React Server Components** - Schema-driven data + UI rendering combined (REST ships today; GraphQL is exposed via the `IGraphQLService` contract) +- **GraphQL + React Server Components** - Schema-driven data + UI rendering combined — as an analogy for the shape, not the transport. The generated data surface is **REST**; GraphQL is not in the product plan and `/graphql` was removed from the dispatcher. - **MCP for business systems** - Structured, permission-aware tools generated from metadata ## Key Features @@ -130,7 +130,7 @@ Think of ObjectStack as: | Traditional Approach | ObjectStack Approach | | :--- | :--- | | Write SQL migrations manually | Schema changes sync automatically | -| Build CRUD APIs by hand | REST generated from schema (GraphQL via the `IGraphQLService` contract) | +| Build CRUD APIs by hand | REST generated from schema | | Manually define agent tools | MCP/tool surfaces generated from metadata | | Duplicate validation logic 3x | Define once, enforce everywhere | | Lock into one database vendor | Swap databases without code changes | diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index 766d7e5b44..d3246d49f8 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -193,11 +193,16 @@ is collected, then the body runs with those values as its `input`. Surfaces can also reference actions **by name**: ```typescript -// List views — row and bulk menus +// List views — row and bulk menus. These are LIST-VIEW keys, so they nest +// under `list` (or a `listViews` entry) — never at the container top level: +// `ViewSchema` is a strict object and rejects them there. defineView({ // ... - rowActions: ['complete_task'], - bulkActions: ['showcase_bulk_reassign'], + list: { + // ... + rowActions: ['complete_task'], + bulkActions: ['showcase_bulk_reassign'], + }, }); // Record pages — a quick-actions bar diff --git a/content/docs/ui/create-vs-edit-form.mdx b/content/docs/ui/create-vs-edit-form.mdx index a77b8244bc..3e6314731a 100644 --- a/content/docs/ui/create-vs-edit-form.mdx +++ b/content/docs/ui/create-vs-edit-form.mdx @@ -79,7 +79,7 @@ export const ContactViews = defineView({ create: { type: 'simple', data, title: 'New contact', sections: [ - { label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] }, + { name: 'who', label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] }, ], }, }, diff --git a/content/docs/ui/public-data-collection.mdx b/content/docs/ui/public-data-collection.mdx index 14fddf5e3a..0d9e91a411 100644 --- a/content/docs/ui/public-data-collection.mdx +++ b/content/docs/ui/public-data-collection.mdx @@ -21,7 +21,7 @@ formViews: { type: 'simple', data: { provider: 'object', object: 'showcase_inquiry' }, sections: [ - { label: 'Tell us about yourself', columns: 1, fields: [ + { name: 'about_you', label: 'Tell us about yourself', columns: 1, fields: [ { field: 'name', required: true }, { field: 'email', required: true }, { field: 'company' }, diff --git a/content/docs/ui/views.mdx b/content/docs/ui/views.mdx index 2a845fbe1b..168ba44b20 100644 --- a/content/docs/ui/views.mdx +++ b/content/docs/ui/views.mdx @@ -50,7 +50,7 @@ export const TaskViews = defineView({ type: 'simple', data, sections: [ - { label: 'Task', columns: 2, fields: ['title', 'status', 'assignee', 'due_date'] }, + { name: 'task', label: 'Task', columns: 2, fields: ['title', 'status', 'assignee', 'due_date'] }, ], }, }, @@ -388,7 +388,7 @@ formViews: { | Property | Type | Description | | :--- | :--- | :--- | -| `name` | `string` | Stable identifier (snake_case) for i18n lookup | +| `name` | `string` | Stable identifier (snake_case) for i18n lookup — resolves `objects.{object}._sections.{name}.label`. **A nameless section renders its authored label in every locale**, so give every section a `name` if the app is localized | | `label` | `string` | Section header | | `columns` | `1-4` | Grid column count | | `collapsible` | `boolean` | Can section be collapsed |