Keel plugin
Keel pairs especially well with Legend-State because it’s designed for strong typing and developer experience, and because they’ve worked with us to make Legend-State and Keel pair perfectly together. All you need to do is provide the actions in the generated keelClient.ts
and the observables will be fully typed and handle calling the correct action functions for you.
As a basic example, if you have a Keel model that that looks like this:
Then you can pass the functions from the generated keelClient.ts into syncedKeel
to create a fully typed observable:
Then you can just get and modify the observable to two-way sync your data with Keel.
Install
Follow Keel’s instructions to get everything setup with Keel. Then install the ksuid
library, which the Keel plugin uses to generate IDs locally in the same way that Keel’s backend generates IDs.
Full Example
We’ll start with a full example to see what a full setup looks like, then go into specific details.
Configure globals
The first step to using the Keel plugin is to set some global configuration options. The suggested options are:
- client: It needs the client in order to enable the Keel realtime plugins.
- waitFor: An observable that you set to true after signing in
TODO: Other config options
get and list
The Keel plugin has two slightly different patterns depending on whether you’re using a get
or a list
action.
The behavior when using get
or as: 'value'
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
- create: Setting the value to null or undefined, or calling
delete()
, will delete
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 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
The shape of the observable object can be changed with the as
parameter, which supports three options:
object
: The default, an object keyed by the row’sid
field.array
: Treat the result of a query as an arrayMap
: A Map, which can be more efficient for accessing rows by keyvalue
: Treat the result of a query as a single value like aget
where
When using a list
function you may want to provide more options to the where
query. You can do that by customizing actions, but it is most easily done with the where
parameter.
In this example of using a lookup table by room, we can pass the roomId
into the query:
Action functions
Using Legend-State with Keel puts some requirements on your model structure:
1. id parameter in create actions
Because Legend-State generate ids locally, id
needs to be include in create functions in your Keel models. You can make it optional if you may sometimes not create with an id.
2. Include all possibly changeable fields as optional in create/update actions
This plugin sends updates with only the changed fields, so having some fields as required in update could cause the update action to fail. And if it changes any field that’s not included in the action, that will also fail.
Additionally, using the debounceSet option may result in the create
action being delayed until after your code has added more fields to the initial value.
So we suggest:
- create actions should have required fields required and include all other fields as optional
- update actions should include all changeable fields as optional
3. Include updatedAt? in list actions
This is only needed if you’re using changesSince: 'last-sync'
. See sync only diffs.
Example model structure
Customizing actions
In the previous examples we provided the Keel function directly, but you can also provide your own function which calls the Keel action. That can be useful for adding extra query or creation options, such as with a lookup table.
Sync only diffs
An optional but very useful feature is the changesSince: 'last-sync'
option. This can massively reduce badwidth 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:
- Save the maximum updatedAt to the local persistence
- In subsequent syncs or after refresh it will list by
updatedAt: lastSync + 1
to get only recent changes - The new changes will be merged into the observable
To enable this on the Keel side, just include updatedAt?
in the list parameters to enable querying by updatedAt.
And to enable this feature in Legend-State, use the changesSince
option in combination with list
. It can not work with get, but you can emulate a get with a list by creating a list
action with an id
parameter and the as: 'value'
option in syncedKeel
.
Soft deletes
The delete parameter does not need to be an actual delete
action in Keel. You could also implement it as a soft delete if you prefer, just setting a deleted
field to true. To do that you can have a deleted
field on your model, or provide a fieldDeleted
with a custom field name.
Then when you delete an element it will internally call the update action with { deleted: true }
and the list action will remove deleted elements from the observable.
List deletes from audit table
We have a helper function that we use in Keel code to get deleted rows from Keel’s built-in audit log. If the query has an updatedAt
timestamp, this will get all values updated since updatedAt
as well as get all rows deleted since updatedAt
and include them as { id, deleted: true }
. The plugin will internally remove those deleted rows from the observable for you.
Then you can use listTableWithDeletes
in your beforeQuery
hooks. You will need to add this to any beforeQuery
hooks that you want to list with deletes.
Usage
Add new element to table with id
To add a new element to an observable and use it locally before it has been created remotely, you can create it with a local id, and then it will be updated with createdAt
and updatedAt
after it’s created in Keel.
Note that since createdAt
and updatedAt
are defined as required in the types they should to be set to undefined when creating.
Wait for remote load
Because Keel automatically adds a createdAt
field after it creates, you can know that data has been successfully saved to Keel if it has a createdAt
field. Just make sure that you don’t set createdAt
yourself as it’s automatically created by Keel.
waitFor another table
If you have a table dependant on another table, it needs to wait for the dependant table to be created, otherwise it will fail because the relationship doesn’t exist. For example you can’t create messages in a chat room before that chat room exists. You can ensure the related table is created first using waitForSet
and createdAt
:
TODO
Realtime
Keel does not have realtime built in, but it’s very easy to build a realtime system on top of it.
More details coming soon.
Other todo
- options
- transforms
- Persist in full example