> Upsert, bulk upsert, and delete entities from the server client and hooks

# Mutations

Create, update, and delete entities from the server client (`createPyloServer` / `createPyloNode`)
or from Client Components with the hooks.

## Server

### Upsert

Create or update with the `_search_value` pattern. Pylo updates the record that matches the search,
or creates one when nothing matches.

```typescript filename="example.ts"
const result = await pylo.invoice.upsert({
  _search_value: {
    field: 'invoice_number',
    value: 'INV-001',
    not_found_behavior: 'create',
  },
  invoice_number: 'INV-001',
  amount_net: 1000,
});

// result: { id: string }
```

### With relations

Use `_set` to connect related entities during upsert:

```typescript filename="example.ts"
await pylo.invoice.upsert({
  _search_value: {
    field: 'invoice_number',
    value: 'INV-001',
    not_found_behavior: 'create',
  },
  invoice_number: 'INV-001',
  amount_net: 1500,
  customer_set: {
    _search_value: {
      field: 'customer_number',
      value: 'C-001',
      not_found_behavior: 'create',
    },
    name: 'Acme Corp',
  },
});
```

### Search values

`_search_value` targets the record by a field instead of an `id`. It works at the top level and
inside nested relation upserts (as in the example above). The older spelling `__search_value` is
deprecated and still accepted.

| Option                         | Default    | Description                                                                                       |
| ------------------------------ | ---------- | ------------------------------------------------------------------------------------------------- |
| `field`                        | required   | Field to search by                                                                                |
| `value`                        |            | Value to match                                                                                    |
| `not_found_behavior`           | `'create'` | `'create'` inserts a new record, `'error'` fails, `'ignore'` skips (nested relation upserts only) |
| `search_in_all_field_variants` | `false`    | Search across all variants of the field instead of only the requested ones                        |
| `multiple_results_allowed`     | `false`    | Allow more than one match; without it, multiple matches fail the mutation                         |
| `multiple_results_use_latest`  | `true`     | With multiple matches allowed, use the newest record (`false` uses the oldest)                    |

### Bulk upsert

Upserts many rows in one all-or-nothing transaction. Rows without an `id` or `_search_value` are
created. The ids come back in input order.

```typescript filename="example.ts"
const results = await pylo.invoice.bulkUpsert([
  { invoice_number: 'INV-002', amount_net: 500 },
  { id: 'invoice-uuid', amount_net: 750 },
]);

// results: { id: string }[]
```

### Delete

```typescript filename="example.ts"
const result = await pylo.invoice.delete(['invoice-uuid-1', 'invoice-uuid-2']);
// result: { success: boolean }
```

## Client hooks

### usePyloUpsert

Invalidates the entity's list queries on success.

```typescript filename="example.ts"
'use client';

import { pylo } from "@/lib/pylo-hooks";

const { mutateAsync } = pylo.usePyloUpsert('invoice');

await mutateAsync({
  _search_value: {
    field: 'invoice_number',
    value: 'INV-001',
    not_found_behavior: 'create',
  },
  invoice_number: 'INV-001',
  amount_net: 1000,
});
```

`usePyloBulkUpsert` works the same way with an array input.

### usePyloDelete

```typescript filename="example.ts"
const { mutateAsync } = pylo.usePyloDelete('invoice');

await mutateAsync(['invoice-uuid-1']);
```

## Query invalidation

Mutation hooks invalidate all queries for the same entity, so list and detail views refetch. Query
key structure:

- Lists: `['pylo', entityName, 'list', options]`
- Infinite lists: `['pylo', entityName, 'infiniteList', options]`
- By ID: `['pylo', entityName, 'byId', id, options]`

Mutations invalidate the prefix `['pylo', entityName]`, which covers all of the above.

Next: [Import & Export](/data/import-and-export)
