You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: contents/docs/custom-mutators.mdx
+75-26Lines changed: 75 additions & 26 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ title: Custom Mutators
4
4
5
5
_Custom Mutators_ are a new way to write data in Zero that is much more powerful than the original ["CRUD" mutator API](./writing-data).
6
6
7
-
Instead of having only the few built-in `insert`/`update`/`delete` write operations for each table, custom mutators _allow you to create your own write operations_ using arbitrary code. This makes it possible to do things that are impossible or awkward with other sync engines.
7
+
Instead of having only the few built-in `insert`/`update`/`delete` write operations for each table, custom mutators allow you to _create your own write operations_ using arbitrary code. This makes it possible to do things that are impossible or awkward with other sync engines.
8
8
9
9
For example, you can create custom mutators that:
10
10
@@ -47,7 +47,7 @@ Zero's custom mutators are based on [_server reconciliation_](https://www.gabrie
47
47
Our previous sync engine, [Replicache](https://replicache.dev/), also used
48
48
server reconciliation. The ability to implement arbitrary mutators was one of
49
49
Replicache's most popular features. Custom mutators bring this same power to
50
-
Zero, but with a *much* better developer experience.
50
+
Zero, but with a much better developer experience.
51
51
</Note>
52
52
53
53
A custom mutator is just a function that runs within a database transaction, and which can read and write to the database. Here's an example of a very simple custom mutator written in TypeScript:
@@ -102,7 +102,7 @@ async function updateIssueOnServer(
102
102
```
103
103
104
104
<Noteheading="Code sharing in mutators is optional">
105
-
Even in TypeScript, you can do as little or as much code sharing as you like. In your server mutator, you can [use raw SQL](TODO), any data access libraries you prefer, or add as much extra server-specific logic as you need.
105
+
Even in TypeScript, you can do as little or as much code sharing as you like. In your server mutator, you can [use raw SQL](#dropping-down-to-raw-sql), any data access libraries you prefer, or add as much extra server-specific logic as you need.
106
106
107
107
Reusing ZQL on the server is a handy – and we expect frequently used – option, but not a requirement.
108
108
@@ -141,7 +141,7 @@ Now that we understand what client and server mutations are, let's walk through
141
141
142
142
1. When you call a custom mutator on the client, Zero runs your client-side mutator immediately on the local device, updating all active queries instantly.
143
143
2. In the background, Zero then sends a _mutation_ (a record of the mutator having run with certain arguments) to your server's push endpoint.
144
-
3. Your push endpoint runs the [push protocol](#custom-push-implementation), executing the server-side mutator in a transaction against your database and recording the fact that the mutation ran.
144
+
3. Your push endpoint runs the [push protocol](#custom-push-implementation), executing the server-side mutator in a transaction against your database and recording the fact that the mutation ran. Optionally, you use our `PushProcessor` class to handle this for you, but you can also implement it yourself.
145
145
4. The changes to the database are replicated to `zero-cache` as normal.
146
146
5.`zero-cache` calculates the updates to active queries and sends rows that have changed to each client. It also sends information about the mutations that have been applied to the database.
147
147
6. Clients receive row updates and apply them to their local cache. Any pending mutations which have been applied to the server have their local effects rolled back.
@@ -234,7 +234,7 @@ export function createMutators() {
// Validate title length. Legacy issues are exempt.
240
240
if (!prev.isLegacy&&title.length>100) {
@@ -252,6 +252,16 @@ You have the full power of ZQL at your disposal, including relationships, filter
252
252
253
253
Reads and writes within a mutator are transactional, meaning that the datastore is guaranteed to not change while your mutator is running. And if the mutator throws, the entire mutation is rolled back.
254
254
255
+
<Notetype="note"heading="No server reads in optimistic mutators">
256
+
Outside of mutators, the `run()` method has a [`type` parameter](reading-data#running-queries-once) that can be used to wait for server results.
257
+
258
+
This parameter isn't supported within mutators, because waiting for server results makes no sense in an optimistic mutation – it defeats the purpose of running optimistically to begin with.
259
+
260
+
When a mutator runs on the client (`tx.location === "client"`), ZQL reads only return data already cached on the client. When mutators run on the server (`tx.location === "server"`), ZQL reads always return all data.
261
+
262
+
You can use `run()` within custom mutators, but the `type` argument does nothing. In the future, passing `type` in this situation will throw an error.
263
+
</Note>
264
+
255
265
### Invoking Client Mutators
256
266
257
267
Once you have registered your client mutators, you can call them from your client-side application:
@@ -263,27 +273,60 @@ zero.mutate.issue.update({
263
273
});
264
274
```
265
275
266
-
Mutations execute instantly on the client, but it is sometimes useful to know when the server has applied the mutation (or experienced an error doing so).
276
+
The result of a call to a mutator is a `Promise`. You do not usually need to `await` this promise as Zero mutators run very fast, usually completing in a tiny fraction of one frame.
277
+
278
+
However because mutators ocassionally need to access browser storage, they are technically `async`. Reading a row that was written by a mutator immediately after it is written may not return the new data, because the mutator may not have completed writing to storage yet.
279
+
280
+
### Waiting for Mutator Result
281
+
282
+
We typically recommend that you "fire and forget" mutators.
267
283
268
-
You can get the server result of a mutation with the `server` property of a mutator's return value:
284
+
Optimistic mutations make sense when the common case is that a mutation succeeds. If a mutation frequently fails, then showing the user an optimistic result doesn't make sense, because it will likely be wrong.
285
+
286
+
That said there are cases where it is useful to know when a write succeeded on either the client or server.
287
+
288
+
One example is if you need to read a row directly after writing it. Zero's local writes are very fast (almost always < 1 frame), but because Zero is backed by IndexedDB, writes are still *technically* asynchronous and reads directly after a write may not return the new data.
289
+
290
+
You can use the `.client` promise in this case to wait for a write to complete on the client side:
269
291
270
292
```ts
271
-
const { server } =awaitzero.mutate.issue.update({
272
-
id: 'issue-123',
273
-
title: 'New title',
274
-
}).server;
293
+
try {
294
+
const write =zero.mutate.issue.update({
295
+
id: 'issue-123',
296
+
title: 'New title',
297
+
});
275
298
276
-
if (server.error) {
277
-
console.error('Server mutation went boom', server.error);
278
-
} else {
279
-
console.log('Server mutation complete');
299
+
// issue-123 not guaranteed to be present here. read1 may be undefined.
You can also wait for the server write to succeed:
314
+
315
+
```ts
316
+
try {
317
+
awaitzero.mutate.issue.update({
318
+
id: 'issue-123',
319
+
title: 'New title',
320
+
}).server;
321
+
322
+
// issue-123 is written to server
323
+
} catch (e) {
324
+
console.error("Mutator failed on server", e);
280
325
}
281
326
```
282
327
283
328
<Noteheading="Returning data from mutators">
284
-
You will soon be able to use `server.data` to return data from mutators in the
285
-
success case. [Let us know](https://discord.rocicorp.dev/) if you need this
286
-
soon.
329
+
There is not yet a way to return data from mutators in the success case – the type of `.clent` and `.server` is always `Promise<void>`. [Let us know](https://discord.rocicorp.dev/) if you need this.
287
330
</Note>
288
331
289
332
### Setting Up the Server
@@ -299,33 +342,40 @@ If you are implementing your server in TypeScript, you can use the `PushProcesso
The `connectionProvider` argument allows `PushProcessor` to create a connection and run transactions against your database. We provide an implementation for the excellent [`postgres.js`](https://www.npmjs.com/package/postgres) library, but you can [implement an adapter](https://github.com/rocicorp/mono/blob/bb9ba1bde0b11689ed3be8814c2acabb145b951b/packages/zql/src/mutate/custom.ts#L67) for a different Postgres library if you prefer.
374
+
`PushProcessor` depends on an abstract `Database`. This allows it to implement the push algorithm against any database.
375
+
376
+
`@rocicorp/zero/pg` includes a `ZQLDatabase` implementation of this interface backed by Postgres. The implementation allows the same mutator functions to run on client and server, by providing an implementation of the ZQL APIs that custom mutators run on the client.
377
+
378
+
`ZQLDatabase` in turn relies on an abstract `DBConnection` that provides raw access to a Postgres database. This allows you to use any Postgres library you like, as long as you provide a `DBConnection` implementation for it. The `PostgresJSConnection` class implements `DBConnection` for the excellent [`postgres.js`](https://www.npmjs.com/package/postgres) library to connect to Postgres.
329
379
330
380
To reuse the client mutators exactly as-is on the server just pass the result of the same `createMutators` function to `PushProcessor`.
Copy file name to clipboardExpand all lines: contents/docs/debug/permissions.mdx
+20-8Lines changed: 20 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,18 +6,30 @@ Given that permissions are defined in their own file and internally applied to q
6
6
7
7
## Read Permissions
8
8
9
-
The `transform-query` utility is provided to transform a query by applying permissions to it. As of today you'll need to provide the hash of the query you want to transform. You can find this in server logs, websocket network inspector, or in the CVR database. In a future release, you'll be able to write ZQL directly.
You can use the `analyze-query` utility with the `--apply-permissions` flag to see the complete query Zero runs, including read permissions.
10
+
11
+
```bash
12
+
npx analyze-query
13
+
--schema='./shared/schema.ts'
14
+
--query='issue.related("comments")'
15
+
--apply-permissions
16
+
--auth-data='{"userId":"user-123"}'
13
17
```
14
18
15
-
The output will be the ZQL query with permissions applied as well as the AST of that query.
19
+
If the result looks right, the problem may be that Zero is not receiving the `AuthData` that you think it is. You can retrieve a query hash from websocket or server logs, then ask Zero for the details on that specific query.
20
+
21
+
Run this command with the same environment you run `zero-cache` with. It will use your `upstream` or `cvr` configuration to look up the query hash in the cvr database.
22
+
23
+
```bash
24
+
npx analyze-query
25
+
--schema='./shared/schema.ts'
26
+
--hash='3rhuw19xt9vry'
27
+
--apply-permissions
28
+
--auth-data='{"userId":"user-123"}'
29
+
```
16
30
17
31
<Notetype="note">
18
-
The printed AST is slightly different than the source ZQL string as it
19
-
leverages internal APIs to prevent syncing rows that are used strictly for
20
-
permission checks.
32
+
The printed query can be different than the source ZQL string, because it is rebuilt from the query AST. But it should be logically equivalent to the query you wrote.
Copy file name to clipboardExpand all lines: contents/docs/debug/slow-queries.mdx
+3-5Lines changed: 3 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -67,10 +67,8 @@ npx analyze-query \
67
67
68
68
This will output the query plan and time to execute each phase of that plan.
69
69
70
-
If you're unsure which query is slow, or you do not know what it looks like after permissions have been applied, you can obtain the query from the hash that is output in the server logs (e.g., `hash=3rhuw19xt9vry`):
70
+
Note that query performance can also be affected by read permissions. See [Debugging Permissions](./permissions) for information on how to analyze queries with read permissions applied.
This will find the query with that hash in the CVR DB and output the ZQL for that query, with all read permissions applied. You can then feed this output into `analyze-query`.
74
+
`zero-cache` makes some internal health statistics available via the `/statz` endpoint of `zero-cache`. In order to access this, you must configure an [admin password](/docs/zero-cache-config#admin-password).
Errors from mutators and queries are thrown in response to method calls where possible, but many Zero errors occur asynchronously, during sync.
6
+
7
+
You can catch these errors with the `onError` constructor parameter:
8
+
9
+
```ts
10
+
const z =newZero({
11
+
upstream: 'https://my-upstream-db.com',
12
+
onError: (msg, ...rest) => {
13
+
reportToSentry('Zero error:', msg, ...rest);
14
+
},
15
+
});
16
+
```
17
+
18
+
You can use this to send errors to Sentry, show custom UI, etc.
19
+
20
+
The first parameter to `onError` is a descriptive message. Additional parameters provide more detail, for example an `Error` object (with a stack), or a JSON object.
21
+
22
+
<Note>
23
+
If you implement `onError`, errors will no longer be sent to devtools by default. If you also want errors sent to the devtools console, you must call `console.error()` inside your `onError` handler.
Copy file name to clipboardExpand all lines: contents/docs/reading-data.mdx
+19-6Lines changed: 19 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -432,16 +432,29 @@ z.query.issue
432
432
433
433
## Running Queries Once
434
434
435
-
Usually subscribing to a query is what you want in a reactive UI but every so often running a query once is all that’s needed.
435
+
Usually subscribing to a query is what you want in a reactive UI, but every so often you'll need to run a query just once. To do this, use the `run()` method:
The `run()` method does not yet expose [`resultType`](#completeness) in any
443
-
way, so you can't use this method to wait for an authoritative result from the
444
-
server. See [bug 3243](https://bugs.rocicorp.dev/issue/3243) for a workaround.
441
+
By default, `run()` only returns results that are currently available on the client. That is, it returns the data that would be given for [`result.type === 'unknown'`](#completeness).
442
+
443
+
If you want to wait for the server to return results, pass `{type: 'complete'}` to `run`:
0 commit comments