# Crud plugin

Crud plugin — Legend State documentation.

Source: https://legend.so/state/v3/sync/crud/

Legend State v3 documentation. Current documentation version.

Legend-State includes a `syncedCrud` plugin that runs on top of `synced` and encapsulates a lot of the behavior you'd use to sync with a CRUD backend.

You can use `syncedCrud` directly or you can build a plugin for your backend on top of it. See the source of the [Keel](https://legend.so/state/v3/sync/keel) and [Supabase](https://legend.so/state/v3/sync/supabase) plugins for examples of plugins built on top of `syncedCrud`.

## [get and list](https://legend.so/state/v3/sync/crud/#get-and-list)

The crud plugin has two slightly different patterns depending on whether you're using a `get` or a `list` action.

The behavior when using `get` is:

-   **get**: Observable value is the value returned from get
-   **create**: If get returned null, then setting any value on the observable will create
-   **update**: If get returned a value, then updating any value on the observable will update
-   **delete**: Setting the value to null or undefined, or calling `delete()`, will delete

```ts
import { observable } from "@legendapp/state";
import { syncedCrud } from '@legendapp/state/sync-plugins/crud'

const profile$ = observable(syncedCrud({
    get: getProfile,
    create: createProfile,
    update: updateProfile,
    delete: deleteProfile,
}))

profile$.get() // => Profile
```

The behavior when using `list` is:

-   **list**: Observable value is an object containing the listed values keyed by id
-   **create**: Adding a new value to the object will create
-   **update**: Updating a child value will update it with the changed fields
-   **delete**: Setting a child value to null or undefined, or calling `delete()`, will delete

```ts
import { observable } from "@legendapp/state";
import { syncedCrud } from "@legendapp/state/sync-plugins/crud";

const profiles$ = observable(syncedCrud({
    list: listProfiles,
    create: createProfile,
    update: updateProfile,
    delete: deleteProfile,
}))

profiles$.get() // => Record<string, Profile>
```

The `list` function expects an array of rows to be returned from your API.

The shape of the observable object returned from a `list` can be changed with the `as` parameter, which supports four options:

1.  `object`: The default, an object keyed by the row's `id` field.
2.  `array`: Treat the result of a query as an array
3.  `Map`: A Map, which can be more efficient for accessing rows by key
4.  `value`: Treat the result of a query as a single value like a `get`

## [create](https://legend.so/state/v3/sync/crud/#create)

The `create` function is called whenever a new object is added to the observable. If you provide a `fieldCreatedAt` then this is determined by whether the object has a value at that field. Otherwise it's determined by whether the new value was previously undefined.

The returned value will be merged into the local value, applying any server defaults or created/updated times from the server value. See [onSaved](https://legend.so/state/v3/sync/crud/#onsaved) for more details.

```ts
import { observable } from "@legendapp/state";
import { syncedCrud } from "@legendapp/state/sync-plugins/crud";

const profile$ = observable(syncedCrud({
    // ...
    create: async (value, options) => {
        const { data, error } = await serverCreateProfile(value);
        if (error) {
            // Handle error, throw an Error to trigger a retry
        } else if (data) {
            return data;
        }
    },
    fieldCreatedAt: 'created_at'
}))
```

## [update](https://legend.so/state/v3/sync/crud/#update)

If an element in the observable is updated it will call the `update` function with the changed value. If you've enabled the `updatePartial` option then the value will include only the changed fields and the `id`. Otherwise it will be the full changed object.

The returned value will be merged into the local value, applying any server defaults or created/updated times from the server value. See [onSaved](https://legend.so/state/v3/sync/crud/#onsaved) for more details.

```ts
import { observable } from "@legendapp/state";
import { syncedCrud } from "@legendapp/state/sync-plugins/crud";

const profile$ = observable(syncedCrud({
    // ...
    update: async (value, options) => {
        const { data, error } = await serverUpdateProfile(value);
        if (error) {
            // Handle error, throw an Error to trigger a retry
        } else if (data) {
            return data;
        }
    },
    fieldUpdatedAt: 'updated_at',
    updatePartial: true // Update with only changed fields
}))
```

## [delete](https://legend.so/state/v3/sync/crud/#delete)

When an element is deleted from the observable, it will call the `delete` function with the previous row value, including its `id`.

```ts
import { observable } from "@legendapp/state";
import { syncedCrud } from "@legendapp/state/sync-plugins/crud";

const profile$ = observable(syncedCrud({
    // ...
    delete: async ({ id }, options) => {
        const { data, error } = await serverDeleteProfile(id);
        if (error) {
            // Handle error, throw an Error to trigger a retry
        } else if (data) {
            return data;
        }
    }
}))
```

Alternatively if you do soft deletes, you can provide a `fieldDeleted` option instead of `delete`, and then it will call the `update` function with that field set to true.

```ts
import { observable } from "@legendapp/state";
import { syncedCrud } from "@legendapp/state/sync-plugins/crud";

const profile$ = observable(syncedCrud({
    // ...
    update: () => {/* ... */},
    fieldDeleted: 'deleted'
}))
```

## [onSaved](https://legend.so/state/v3/sync/crud/#onsaved)

Rows returned by `create` and `update` are automatically merged into the local observable, excluding fields changed locally while the save was in flight.

If you want more control over what fields are updated in your object you can use `onSaved`. Return an object with the fields you want merged into the observable. Returning nothing preserves the automatic merge, so you can also use this for side effects.

```ts
const profile$ = observable(syncedCrud({
    // ...
    create: () => {/* ... */},
    update: () => {/* ... */},
    onSaved: ({ saved, input, currentValue, isCreate }) => {
        return {
            serverValue: saved.serverValue
        }
    }
}))
```

## [subscribe](https://legend.so/state/v3/sync/crud/#subscribe)

If your backend has a realtime feature, or if you want to poll periodically for changes, use `subscribe` to set that up. It starts before the first `get` and may restart when the observable is observed again.

This can be used in two ways depending on how your backend works, updating with incoming data or simply triggering a refresh.

When the observable is no longer being observed it will call the returned unsubscribe function.

```ts
const profile$ = observable(syncedCrud({
    // ...
    list: () => {/* ... */},
    subscribe: ({ refresh, update }) => {
        const unsubscribe = pusher.subscribe({ /*...*/ }, (data) => {
            // Either update with the received data
            update({ value: data })
            // Or trigger a refresh of the get function
            refresh()
        })
        // return unsubscribe function
        return unsubscribe
    }
}))
```

## [Sync only diffs](https://legend.so/state/v3/sync/crud/#sync-only-diffs)

An optional but very useful feature is the `changesSince: 'last-sync'` option. This can massively reduce bandwidth usage when you're persisting list results since it only needs to list changes since the last query. The way this works internally is basically:

1.  Save the maximum updatedAt to the local persistence
2.  In subsequent syncs or after refresh, your `list({ lastSync })` function uses that timestamp to query only recent changes
3.  The new changes will be merged into the observable

This has a few requirements to work correctly:

1.  Set the `fieldUpdatedAt` with a field that is automatically updated by your backend on save. It should not be set on the frontend because inaccurate user clocks might cause data to be missed.
2.  Use soft deletes instead of deleting rows or include deleted rows in your list function. If the list function does not include rows deleted since the last update, the frontend will not know to delete them. You can enable this by adding a `deleted` field in your backend and setting the `fieldDeleted` option.

## [All options](https://legend.so/state/v3/sync/crud/#all-options)

-   `get`: Get a single value from the backend
-   `list`: List an array of values from the backend
-   `create`: Create a single value on the backend
-   `update`: Update a single value on the backend
-   `delete`: Delete a single value on the backend
-   `onSaved`: Update local value with remote data
-   `fieldId`: Row identifier field, defaults to `id`
-   `fieldCreatedAt`: Set the field your backend uses for created times
-   `fieldUpdatedAt`: Set the field your backend uses for updated times
-   `fieldDeleted`: Set the field your backend uses for soft deletes
-   `fieldDeletedList`: Recognize a separate deletion marker in list results without enabling soft-delete writes
-   `updatePartial`: Send only changed fields into update function
-   `changesSince`: 'all' | 'last-sync'. Defaults to 'all'. 'last-sync' syncs only diffs
-   `generateId`: Provide a function for creating row ids.
-   `subscribe`: Set up a realtime connection or polling
-   `retry`: Options for retrying in case of error. Applies to both get and set.
-   `persist`: Options for persisting locally. See [Persist and sync](https://legend.so/state/v3/sync/persist-sync).
-   `debounceSet`: Debounce saved changes to reduce the number of updates
-   `mode`: 'set' | 'assign' | 'merge' | 'append' | 'prepend'. How to apply incoming changes.
-   `transform`: Transform data as it loads in from the remote source or out as it saves to the remote source. You could use this to encrypt the data or transform it into some other format.
-   `waitFor`: A Promise or Observable to wait for before getting from remote
-   `waitForSet`: A Promise or Observable to wait for before setting to remote

[View Markdown](https://legend.so/state/v3/sync/crud.md)[Report a documentation correction](https://github.com/LegendApp/legend-docs/issues/new?title=Docs%3A+Crud+plugin&body=Page%3A+https%3A%2F%2Flegend.so%2Fstate%2Fv3%2Fsync%2Fcrud%0A%0AWhat+is+incorrect+or+missing%3F%0A%0AExpected+behavior+or+supporting+source%3A%0A)
