Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/rocicorp/rails
Replicache on Rails
https://github.com/rocicorp/rails
Last synced: about 2 months ago
JSON representation
Replicache on Rails
- Host: GitHub
- URL: https://github.com/rocicorp/rails
- Owner: rocicorp
- License: other
- Created: 2022-05-29T23:19:25.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-07-19T01:36:14.000Z (2 months ago)
- Last Synced: 2024-07-24T00:17:46.644Z (about 2 months ago)
- Language: TypeScript
- Size: 1.09 MB
- Stars: 33
- Watchers: 5
- Forks: 3
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Replicache on Rails
Generates a CRUD-style interface for Replicache and Reflect, with optional schema validation.
## Install
```bash
npm install --save-dev @rocicorp/rails
```## Usage
### 1. Define Entities
```ts
// All entities must include at least id:string.
export type Todo = {
id: string;
text: string;
complete: boolean;
sort: number;
};
```### 2. Generate Helpers
```ts
import {generate} from '@rocicorp/rails';export const {
put: putTodo,
get: getTodo,
list: listTodos,
// ...
} = generate('todo');
```### 3. Build Mutators
The generated functions all have the same signatures as mutators, so they can be used as mutators directly:
```ts
import {putTodo, updateTodo, deleteTodo} from './todo';export const mutators = {
putTodo,
updateTodo,
deleteTodo,
};
```You can also compose them to make more advanced mutators:
```ts
import {listTodos, updateTodo} from './todo';async function markAllComplete(tx: WriteTransaction) {
const todos = await listTodos(tx);
for (const t of todos) {
// Mutators are transactional, so this is safe. The entire function will
// run atomically and `t.complete` cannot change while it is running.
if (!t.complete) {
await updateTodo(tx, {id: todo.id, complete: true});
}
}
}export const mutators = {
// ...
markAllComplete,
};
```### 4. Build Subscriptions
The generated functions that are read-only (`get`, `has`, `list`, etc) have the correct signature to be used as subscriptions. So you can use them directly:
```ts
// subscribe to a query
const todos = useSubscribe(rep, listTodos, []);
```But as with mutators, you can also compose them to make more interesting subscriptions:
```ts
async function listIncompleteTodos(tx: WriteTransaction) {
const todos = await listTodos(tx);
return todos.filter(t => !t.complete);
}const incompleteTodos = useSubscribe(rep, listIncompleteTodos, []);
```## Validation
You can optionally pass `generate` a validation function as a second parameter. For example, to use Zod as your schema validator:
```ts
import * as z from 'zod';
import {generate} from '@rocicorp/rails';const todoSchema = {
id: z.string(),
text: z.string(),
complete: z.boolean(),
sort: z.number(),
};// In this case, the template parameter to generate can be omitted because it
// is inferred from return type of todoSchema.parse().
export const {
put: putTodo,
get: getTodo,
update: updateTodo,
delete: deleteTodo,
list: listTodos,
} = generate('todo', todoSchema.parse);
```## Reference
### `init(tx: WriteTransaction, value: T): Promise`
Writes `value` if it is not already present. If `value` is already present, does nothing. Returns `true` if the value was written or false otherwise.
### `put(tx: WriteTransaction, value: T): Promise`
Deprecated. Use `set` instead.
Writes `value`. If not present, creates, otherwise overwrites.
### `set(tx: WriteTransaction, value: T): Promise`
Writes `value`. If not present, creates, otherwise overwrites.
### `update(tx: WriteTransaction, value: Update) => Promise`
Updates `value`. Value can specify any of the fields of `T` and must contain `id`. Fields not included in `value` are left unaffected.
All fields in an update are applied together atomically. If the entity does not exist a debug message is printed to the console and the update is skipped.
### `delete(tx: WriteTransaction, id: string) => Promise`
Delete any existing value or do nothing if none exist.
### `has(tx: ReadTransaction, id: string) => Promise`
Return true if specified value exists, false otherwise.
### `get(tx: ReadTransaction, id: string) => Promise`
Get value by ID, or return undefined if none exists.
### `mustGet(tx: ReadTransaction, id: string) => Promise`
Get value by ID, or throw if none exists.
### `list(tx: ReadTransaction, options?: {startAtID?: string, limit:? number}) => Promise`
List values matching criteria.
### `listIDs(tx: ReadTransaction, options?: {startAtID?: string, limit:? number}) => Promise`
List ids matching criteria.
### `listEntries(tx: ReadTransaction, options?: {startAtID?: string, limit:? number}) => Promise<[string, T][]>`
List `[id, value]` entries matching criteria.
## Presence State
Presence state is a special kind of state that is tied to a certain `clientID`.
It is designed to be used with Reflect's
[presence](https://hello.reflect.net/concepts/presence) feature.The entity type for presence state is slightly different from the `Entity` type:
```ts
type PresenceEntity = {
clientID: string;
id: string;
};
```### Generate Presence State Helpers
The function `generatePresence` is similar to the `generate` function but it
generates functions that are to be used with presence state.```ts
type Cursor {
clientID: string;
x: number;
y: number;
};const {
set: setCursor,
get: getCursor,
delete: deleteCursor,
} = generatePresence('cursor');
```For presence entities there are two common cases:
1. The entity does not have an `id` field. Then there can only be one entity per
client. This case is useful for keeping track of things like the cursor
position.
2. The entity has an `id` field. Then there can be multiple entities per client.
This case is useful for keeping track of things like multiple selection or
multiple cursors (aka multi touch).### Lookup Presence State
The `clientID` field (and `id` if used) is significant when reading presence
state. However, for convenience, you can omit the `clientID` and it will default
to the `clientID` of current client. You may not omit the `id` if your entity
type requires an `id` field. When reading presence state you may also omit the
lookup argument completely.### Mutating Presence State
When writing you may only change the presence state entities for the current
client. If you pass in a `clientID` that is different from the `clientID` of the
current client a runtime error will be thrown.When writing you may also omit the `clientID` it will default to the `clientID`
of the current client.```ts
await setCursor(tx, {x: 10, y: 20});
expect(await getCursor(tx)).toEqual({
clientID: tx.clientID,
x: 10,
y: 20,
});
```## Reference for Presence State
### `set: (tx: WriteTransaction, value: OptionalClientID) => Promise`
Write `value`, overwriting any previous version of same value.
### `init: (tx: WriteTransaction, value: OptionalClientID) => Promise`
Write `value` only if no previous version of this value exists.
### `update: (tx: WriteTransaction, value: PresenceUpdate) => Promise`
Update existing value with new fields.
### `delete: (tx: WriteTransaction, id?: PresenceID) => Promise`
Delete any existing value or do nothing if none exist.
### `has: (tx: ReadTransaction, id?: PresenceID) => Promise`
Return true if specified value exists, false otherwise.
### `get: (tx: ReadTransaction, id?: PresenceID) => Promise`
Get value by ID, or return undefined if none exists.
### `mustGet: (tx: ReadTransaction, id?: PresenceID) => Promise`
Get value by ID, or throw if none exists.
### `list: (tx: ReadTransaction, options?: PresenceListOptions) => Promise`
List values matching criteria.
### `listIDs: (tx: ReadTransaction, options?: PresenceListOptions) => Promise[]>`
List IDs matching criteria. The returned ID is `{clientID: string}` if the entry
has no `id` field, otherwise it is `{clientID: string, id: string}`.### `listClientIDs: (tx: ReadTransaction, options?: PresenceListOptions) => Promise`
List `clientID`s matching criteria. Unlike `listIDs` this returns an array of
strings consisting of the `clientID`s.### `listEntries: (tx: ReadTransaction, options?: PresenceListOptions) => Promise<[ListID, T][]>`
List [id, value] entries matching criteria.
## Upgrade from 0.6
### Pluggable Schema
Rails 0.7 made the schema validator pluggable. Instead of passing an instance of zod, pass the parse function.
Before:
```ts
export const {
put: putTodo,
// ...
} = generate('todo', todoSchema);
```Now:
```ts
export const {
put: putTodo,
// ...
} = generate('todo', todoSchema.parse);
```### EntitySchema no longer part of Rails
Because the validator is pluggable it no longer makes sense for Rails to provide `entitySchema`. So either define it yourself:
```ts
const entitySchema = z.object({
id: z.string(),
});
```... or simply add `id: z.string()` to each of your entity definitions.
### Parse called in Debug mode
In 0.6.0, zod was only used if `process.env.NODE_ENV !== 'production'`. Now that the validator is pluggable, it makes more sense for the app to do this.
Before:
```ts
export const {
put: putTodo,
// ...
} = generate('todo', todoSchema);
```Now:
```ts
export const {
put: putTodo,
// ...
} = generate(
'todo',
process.env.NODE_ENV !== 'production' ? todoSchema.parse : undefined,
);
```