Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/30847-api-skill-recycle-bin-doors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@objectstack/skills': patch
---

Clause-②: no
Metadata-protocol: none

`objectstack-api` now teaches the recycle bin's doors as they shipped: a by-id
delete answers `recycleBinEntryId`; `POST /api/v1/data/{object}/recycle-bin/:entryId/restore`
restores one entry (`allowRestoreOwnDeleted` for the caller's own deletes,
`manageRecycleBin` for anyone else's); `GET` lists, `DELETE …/:entryId` purges
and `DELETE …/recycle-bin` empties (all `manageRecycleBin`); a refused restore
answers `409` with one of the four `RESTORE_*` codes. `restore` / `purge` are
taught as derived from `delete` instead of "never derive". The api skill's
"undelete invoices" eval grades the new teaching.
36 changes: 18 additions & 18 deletions skills/objectstack-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,10 @@ POST /api/v1/batch # Cross-object atomic batch
```

There is no `/bulk` route and no `GET .../aggregate` route — batch writes go
through the `batch` endpoints, aggregation through the `query` one.
through the `batch` endpoints.

> **Key rule:** If your object defines `apiMethods`, only those operations (and
> what derives from them) are exposed; `[]` means deny-all. The authorable
> values are the six primitives — see **API Methods (Operations)**.
> what derives from them) are exposed; `[]` means deny-all.

### Metadata API (`/meta`)

Expand Down Expand Up @@ -237,7 +236,6 @@ for `authRequired: false`.
- Always use **plural nouns** for collection paths (`accounts`, not `account`).
- Use **snake_case** for multi-word paths (`project_tasks`, not `projectTasks`).
- Use **verbs** only for actions, not for CRUD (`/close`, `/approve`).
- Always prefix with `/api/v1/` for versioning.

---

Expand Down Expand Up @@ -268,8 +266,8 @@ eight extra verbs are never declared in `apiMethods`:
| `import` | `create` ∨ `update` (writeMode-precise) | `POST /data/{object}/import` |
| `export` | `list` | `GET /data/{object}/export` |

`history` (from `get` ∧ `trackHistory`) gates only. `restore` / `purge` never
derive: the recycle bin has no restore or purge door yet. Declaring any
`history` (from `get` ∧ `trackHistory`) gates only. `restore` / `purge` derive
from `delete` and open the recycle-bin doors (pitfall 2). Declaring any
derived verb in `apiMethods` is stripped at parse with a FROM → TO warning.

`POST /data/{object}/import` takes `mapping` as `{ Column: field }` or as
Expand Down Expand Up @@ -305,7 +303,7 @@ The **HttpDispatcher** is the central request router; it answers:

## Realtime Subscriptions

Realtime contracts are pointer-style — read the spec source for exact shapes:
Realtime contracts live in the spec source:

- `node_modules/@objectstack/spec/src/api/realtime.zod.ts` — `TransportProtocol`
(`websocket` | `sse` | `polling`), `SubscriptionSchema` (`id`, `events[]`,
Expand All @@ -327,10 +325,8 @@ own auth keys.
There is no nested `auth` block on an endpoint, and no `name` / `request` /
`response` field: auth is the flat `public` + `permissions` pair on
`RestApiEndpointSchema`, and declarative `ApiEndpointSchema` endpoints (and the
dispatcher) use `authRequired: boolean` (default `true`) — setting it `false`
obliges you to arm a `rateLimit` (ADR-0121 D6 — see **Declarative Endpoints →
`authRequired` and the D6 pairing** above). Rate-limit policies themselves are
shaped by `RateLimitConfigSchema`:
dispatcher) use `authRequired: boolean` (default `true`; `false` owes an armed
`rateLimit`, D6 above). Rate-limit policies are shaped by `RateLimitConfigSchema`:

<!-- os:check -->
```typescript
Expand Down Expand Up @@ -390,9 +386,7 @@ keys mean the run produced none — never invent an empty.
return flat `{ error, code }` bodies instead (e.g. `CONCURRENT_UPDATE` →
409, `VALIDATION_FAILED` → 400) — do not assume every error arrives in the
`success: false` envelope.
3. **Apply least-privilege auth.** Every endpoint should declare its required
permissions explicitly.
4. **Design idempotent writes deliberately.** No upsert route is generated, so
3. **Design idempotent writes deliberately.** No upsert route is generated, so
an external integration queries by its unique external ID and branches to
create or update — group those writes through
`POST /api/v1/data/{object}/batch`.
Expand All @@ -408,10 +402,16 @@ keys mean the run produced none — never invent an empty.
read. Sending neither skips the check; a mismatch answers **409
`CONCURRENT_UPDATE`**; the quoted-empty entity-tag (`""`) is refused **400
`VALIDATION_FAILED`**, not treated as omitted.
2. **Treating `DELETE` as final or as restorable.** A by-id delete keeps a
business object's rows in the recycle bin (`sys_recycle_bin`, 30 days by
default; not `multi: true` deletes, platform rows or rows deleted before the
bin existed); nothing lists or restores it over the API yet. No soft-delete.
2. **Treating `DELETE` as final.** A by-id delete keeps a business object's
rows in the recycle bin (`sys_recycle_bin`, 30 days by default; not
`multi: true` deletes, platform rows or rows deleted before the bin existed)
and answers `recycleBinEntryId`. Under `/api/v1/data/{object}/recycle-bin`:
`POST /:entryId/restore` (`allowRestoreOwnDeleted` for your own deletes,
`manageRecycleBin` for others'); `GET` lists, `DELETE /:entryId` purges,
`DELETE` empties (`manageRecycleBin`). A refused restore answers `409`
`RESTORE_UNIQUE_VALUE_TAKEN`, `RESTORE_PARENT_PURGED`,
`RESTORE_PARENT_IN_RECYCLE_BIN` (restore that parent's entry first) or
`RESTORE_PACKAGE_UNINSTALLED`, and keeps the entry. No soft-delete.

---

Expand Down
6 changes: 3 additions & 3 deletions skills/objectstack-api/evals/endpoints-auth-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@
{
"id": 5,
"prompt": "Lock the `invoice` object's API down to read, list, search and export; add a `/bulk` route for imports and a `restore` operation so deleted invoices can be undeleted.",
"expected_output": "Authors only the primitives: `apiMethods: ['get', 'list']` — `search` and `export` derive from `list` automatically and are never declared (a declared derived verb is stripped with a warning). Explains there is no `/bulk` route (batch writes go through `POST /api/v1/data/{object}/batch`) and that `restore` / `purge` never derive: a by-id delete of a business object already keeps its rows in the recycle bin (`sys_recycle_bin`), but nothing lists or restores them over the API yet, so there is no restore operation to add and no custom soft-delete to build.",
"expected_output": "Authors only the primitives: `apiMethods: ['get', 'list']` — `search` and `export` derive from `list` automatically and are never declared (a declared derived verb is stripped with a warning). Explains there is no `/bulk` route (batch writes go through `POST /api/v1/data/{object}/batch`) and that `restore` is never declared either: it derives from `delete`, and a by-id delete of a business object already keeps its rows in the recycle bin (`sys_recycle_bin`). Points out the conflict: with `delete` left out, the bin's doors are closed too, so undeleting over the API needs `delete` in `apiMethods`; a caller then restores with `POST /api/v1/data/invoice/recycle-bin/:entryId/restore` (the `recycleBinEntryId` the delete answered), under `allowRestoreOwnDeleted` for its own deletes or `manageRecycleBin` for anyone's. No custom soft-delete to build.",
"files": [],
"assertions": {
"must_contain": ["apiMethods", "/batch", "sys_recycle_bin", "recycle bin"],
"must_not_contain": ["'search'", "'restore'"]
"must_contain": ["apiMethods", "/batch", "sys_recycle_bin", "recycle-bin"],
"must_not_contain": ["'search'", "'restore'", "nothing lists or restores"]
}
},
{
Expand Down
Loading