Skip to content
Merged
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
35 changes: 21 additions & 14 deletions node.js/app-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,27 +159,34 @@ cds.ApplicationService.handle_log_events = cds.service.impl (function(){

## Results of Generic CRUD Handlers

Custom `.on` handlers can return any value. When no custom handler provides a result — that is, when CAP's built-in generic handler runs the CRUD operationthe result follows a consistent shape:
When CAP's generic handlers run a CRUD operation, the result follows a consistent shape (custom `.on` handlers may return any value):

| Operation | Return value |
|-----------|-------------|
| **READ** | `object[]` — the matching records, or a single `object \| null` for singleton requests |
| **INSERT** / **CREATE** | An array of primary-key objects of the inserted rows, with `.affected` set to the number of rows written |
| **UPDATE** / **UPSERT** / **DELETE** | An empty array with `.affected` set to the number of rows changed or deleted |
| Operation | Return value |
|-----------------------|-----------------------------------------------------------------------------------------------|
| `READ` | Array of matching records, or a single record / `null` when read by key |
| `INSERT` / `CREATE` | Array with `.affected` (rows written); iterate to access the inserted rows' primary keys |
| `UPDATE` / `UPSERT` | Array with `.affected` (rows changed); populated with rows from a `RETURNING` clause |
| `DELETE` | Array with `.affected` (rows deleted); populated with rows from a `RETURNING` clause |

The `.affected` count is a property directly on the returned array:
For `INSERT`s, the result is a lazy array: iterating it (`[...result]`, `for…of`, `JSON.stringify`) materializes the generated primary keys of the inserted rows. Direct index access works after the first iteration.

```js
const inserted = await srv.create(Books).entries({title:'Catweazle'})
inserted[0] // { ID: '...' } — primary key of the inserted row
inserted.affected // 1
inserted.affected // 1
const [row] = [...inserted] // materializes — row holds the generated key
inserted[0] // same row (materialized above)
```

For `UPDATE`, `UPSERT`, and `DELETE`, the array is reserved for rows returned by a SQL `RETURNING` clause. But `RETURNING` is not yet supported, so the array currently is always empty:

```js
const updated = await srv.update(Books).set({discount:'10%'}).where({stock:{'>':111}})
updated.affected // number of rows updated
updated.affected // number of rows updated
```

When a write targets a **specific subject** (for example, `srv.update(Books, 201)` or `srv.delete(Books, '1')`) and no row is matched, the handler throws a `404` error. A query using only a `where` clause with zero matches returns `{ affected: 0 }` without throwing.
When a write targets a single row by key (for example, `srv.update(Books, 201)` or `srv.delete(Books, '1')`) and no row matches, the handler throws a 404 error. A `where` clause that matches zero rows returns an array with `affected: 0` without throwing.

> [!tip] Consistent Results Across Local and Remote Services
> This shape was introduced in cds 10 so that local services, HCQL-proxied remote services, and database services return the same thing. To restore the previous behavior, set <Config>cds.features.legacy_srv_results: true</Config>.

::: tip Consistent results across local and remote services
This return shape was introduced in **cds 10** to make results from local services, HCQL-proxied remote services, and database services consistent. To restore the previous behavior, set `{ "features": { "legacy_srv_results": true } }` in your project configuration.
:::
[See the migration guide for opt-out options.](../releases/migration/cds10#fixed-service-results){.learn-more}
Loading