Skip to content

Commit a3442f9

Browse files
os-zhuangclaude
andauthored
docs: clone endpoint, required-FK delete, Field.time, owner-scoped writes (#2012)
Reflect runtime behavior shipped this cycle in the hand-written docs: - api-reference + implementation-status: POST /data/:object/:id/clone (gated by enable.clone; overrides; 201 {object,id,sourceId,record}). - protocol/objectql/types: a required lookup FK escalates a defaulted set_null to restrict → 409 DELETE_RESTRICTED; Field.time accepts HH:MM/HH:MM:SS. - guides/security: member_default writes are owner-scoped (created_by) and now enforced on by-id update/delete via a pre-image authorization check; admins / modifyAllRecords bypass; audit-opt-out objects fail closed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0d4e3f3 commit a3442f9

5 files changed

Lines changed: 46 additions & 2 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
docs: document the record-clone endpoint, required-FK delete escalation (DELETE_RESTRICTED), Field.time input format, and owner-scoped member writes — reflecting the runtime behavior shipped this cycle. Docs-only.

content/docs/concepts/implementation-status.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ The data (`/data`), metadata (`/meta`), and batch endpoints are implemented in `
182182
| `/data/{object}/createMany` | POST | Batch create ||
183183
| `/data/{object}/updateMany` | POST | Batch update ||
184184
| `/data/{object}/deleteMany` | POST | Batch delete ||
185+
| `/data/{object}/{id}/clone` | POST | Clone a record (gated by `enable.clone`) ||
185186
| `/data/{object}/batch` | POST | Atomic batch operations ||
186187

187188
### Advanced Protocols

content/docs/guides/api-reference.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,24 @@ Batch delete records by ID list.
251251

252252
---
253253

254+
### `POST /data/:object/:id/clone`
255+
256+
Clone a record. Reads the source, drops engine-owned columns (`id`, the audit
257+
fields, autonumbers, and computed `formula`/`summary` values) so they are
258+
re-derived, applies any caller `overrides`, and inserts the copy. Shallow by
259+
design — it duplicates the record's own fields, not its child records.
260+
261+
Gated by the object's `enable.clone` capability (default `true`); an object with
262+
`enable.clone: false` returns `403 CLONE_DISABLED`.
263+
264+
**Body** (optional): `{ "overrides": { "name": "Acme (Copy)" } }` — applied on
265+
top of the copied values (a bare field map is also accepted). The natural place
266+
to set a new name or clear a unique field.
267+
268+
**Response** `201`: `{ object, id, sourceId, record }`
269+
270+
---
271+
254272
## Analytics
255273

256274
Semantic BI queries using a cube-style API. Available when the analytics service is enabled.

content/docs/guides/security.mdx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,22 @@ than redefining the basics.
231231
|:--|:--|:--|:--|
232232
| `admin_full_access` | Platform (no RLS) | All objects, all system permissions, Studio + Setup, cross-org reads | Reserved for platform operators. Auto-granted to the bootstrap admin (`bootstrapPlatformAdmin`) — the **first registered human**. The non-loginable seed-data identity `usr_system` (role `system`, provisioned before the first sign-up to own seeded rows) is deliberately skipped, so the real admin wins the promotion. |
233233
| `organization_admin` | Per-org (`tenant_isolation` RLS, only active when `@objectstack/plugin-org-scoping` is loaded) | Wildcard CRUD inside the org, `manage_org_users`, Setup app shell (only org-scoped entries visible), invite members, manage roles assignment | **Read-only** on `sys_role`, `sys_permission_set`, `sys_role_permission_set`, `sys_user_permission_set`, `sys_user_role` to prevent self-elevation. Does **not** see Studio. |
234-
| `member_default` | Per-org | Standard end-user CRUD on org records | Default profile for invited members. |
234+
| `member_default` | Per-org | Standard end-user CRUD on org records; **writes are owner-scoped** | Default profile for invited members. |
235235
| `viewer_readonly` | Per-org | Read access only | For auditors / read-only stakeholders. |
236236

237+
> **Owner-scoped writes.** `member_default` grants read/create on org records but
238+
> restricts **edit and delete to the records the user created** — via the
239+
> `owner_only_writes` / `owner_only_deletes` RLS policies, keyed on `created_by`
240+
> (the ownership column the engine stamps on every row). These are enforced on
241+
> single-id `update`/`delete` by a **pre-image authorization check**: before the
242+
> mutation the middleware re-reads the target row through the write-operation RLS
243+
> filter, and a member attempting to modify another user's record gets `403`.
244+
> A permission set with `modifyAllRecords` (or no RLS, e.g. `admin_full_access`)
245+
> bypasses this. An object that opts out of audit fields (`systemFields.audit:
246+
> false`, no `created_by`) fails **closed** for member writes — grant
247+
> `modifyAllRecords` or a per-object policy, or model transferable ownership with
248+
> a dedicated owner field + custom policy.
249+
237250
> **Single-org vs. multi-org runtimes.** The wildcard `tenant_isolation`
238251
> RLS row above only takes effect when `@objectstack/plugin-org-scoping`
239252
> is registered (typically via `OS_MULTI_TENANT=true`). In single-org

content/docs/protocol/objectql/types.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ business_hours_start:
353353
defaultValue: "09:00:00"
354354
```
355355

356-
**Storage format:** `HH:MM:SS`
356+
**Storage format:** `HH:MM:SS` — input accepts `HH:MM` or `HH:MM:SS` (with an optional fractional part and `Z`/offset). A `time` is a wall-clock value, not an instant: it is validated as a time-of-day, not parsed as a date.
357357

358358
**Use cases:**
359359
- Business hours
@@ -519,6 +519,14 @@ const opportunities = await ObjectQL.query({
519519
- `restrict`: Prevent deletion if references exist
520520
- `cascade`: Delete this record when referenced record is deleted
521521

522+
> **Required foreign keys.** A `required: true` lookup cannot be nulled, so the
523+
> *default* `set_null` automatically escalates to `restrict` on such a field —
524+
> deleting the parent is refused with `409 DELETE_RESTRICTED` (the response
525+
> carries `dependentObject` and `dependentCount`) instead of a confusing
526+
> "&lt;field&gt; is required" validation error. To delete the children along with
527+
> the parent, set `deleteBehavior: cascade` explicitly. An explicit `set_null`
528+
> or `cascade` is always honored as written.
529+
522530
**Multiple lookups:**
523531
```yaml
524532
contacts:

0 commit comments

Comments
 (0)