Skip to content

Commit 40aaa50

Browse files
committed
feat: createMany, updateMany, deleteMany
1 parent f4bb64f commit 40aaa50

File tree

27 files changed

+2196
-39
lines changed

27 files changed

+2196
-39
lines changed

docs/guide/data/cache.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ You can also use the `store.<collectionName>.clearItem` method:
8989
store.User.clearItem('abc')
9090
```
9191

92-
## Layers
92+
## Layers <Badge text="New in v0.7" />
9393

9494
A cache layer is a way to create a temporary state modification that can be easily reverted. This is how [optimistic updates](./mutation.md#optimistic-updates) are implemented.
9595

docs/guide/data/form.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Form Objects
22

3-
## Create Form
3+
## Create
44

55
Instead of creating a new item directly, you can create a *form object* that is very useful to handle the data of a form with the `createForm` method.
66

docs/guide/data/live.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22

33
rstore provides a set of methods to help build realtime applications. Plugins can use [the relevant hooks](../plugin/hooks.md#subscriptions) to connect to WebSockets, Server-Sent Events, or any other realtime protocol.
44

5-
## Subscribe
5+
## Subscribe <Badge text="Changed in v0.7" type="warning" />
66

77
Subscriptions are a way to listen for changes in the data store. You can subscribe to a specific collection and plugins will update the store in realtime. You can pass the same parameters than in the [queries](./query.md).
88

99
```ts
1010
const store = useStore()
1111

12-
store.ChatMessage.subscribe({
12+
store.ChatMessage.subscribe(q => q({
1313
params: {
1414
filter: {
1515
roomId: 'room1',
1616
},
1717
},
18-
})
18+
}))
1919
```
2020

2121
## Unsubscribe
@@ -39,21 +39,21 @@ const { meta } = store.ChatMessage.subscribe()
3939
console.log(meta.value)
4040
```
4141

42-
## Live Query
42+
## Live Query <Badge text="Changed in v0.7" type="warning" />
4343

4444
You can use the `liveQueryFirst` and `liveQueryMany` methods to do both a query and a subscription at the same time.
4545

4646
```ts
47-
const { data: user } = store.User.liveQueryFirst('some-id')
47+
const { data: user } = store.User.liveQuery(q => q.first('some-id'))
4848
```
4949

5050
```ts
51-
const { data: messages } = store.ChatMessage.liveQueryMany({
51+
const { data: messages } = store.ChatMessage.liveQuery(q => q.many({
5252
filter: item => item.roomId === 'room1',
5353
params: {
5454
filter: {
5555
roomId: 'room1',
5656
},
5757
},
58-
})
58+
}))
5959
```

docs/guide/data/module.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Module
1+
# Module <Badge text="Changed in v0.7" type="warning" />
22

33
In most application, there are cases where some specific logic or state is needed. For example, you may want to handle the current user with a specific key and also have special mutations for login or logout.
44

@@ -180,7 +180,7 @@ const { data: currentUser } = auth.currentUser
180180
Awaiting a module is always optional. You can use the module without awaiting it, but all necessary code might not have run yet. This is a valid use case if you don't use async component setup for example.
181181
:::
182182

183-
## Mutations
183+
## Mutations <Badge text="Changed in v0.7" type="warning" />
184184

185185
You can define mutations using the `defineMutation` function from the module setup function. This is useful for defining actions that modify the state of the module or the store in general.
186186

docs/guide/data/mutation.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@ const newTodo = await store.todos.create({
1111
})
1212
```
1313

14+
::: tip
15+
You can also use the `createForm` method to create a new record. This method returns a form object that you can use to manage the state of the form and submit it when you're ready. See [Forms](./form.md#create-form) for more details.
16+
:::
17+
18+
## Create Many <Badge text="New in v0.7.3" />
19+
20+
You can create many items at once by using the `createMany` method on the store. This method takes an array of objects with the properties of the records you want to create.
21+
22+
```ts
23+
const newTodos = await store.todos.createMany([
24+
{ title: 'New Todo 1', completed: false },
25+
{ title: 'New Todo 2', completed: false },
26+
])
27+
```
28+
29+
It can be useful to batch the create operations into a single fetch request to your backend. See `createMany` in the [plugin hooks](../plugin/hooks.md#createmany).
30+
1431
## Update
1532

1633
To update an existing record, you can use the `update` method on the store. This method takes an object with the properties you want to update and the key of the record you want to update.
@@ -58,6 +75,23 @@ async function toggle() {
5875
}
5976
```
6077

78+
::: tip
79+
You can also use the `updateForm` method to update an existing record. This method returns a form object that you can use to manage the state of the form and submit it when you're ready. See [Forms](./form.md#update-form) for more details.
80+
:::
81+
82+
## Update Many <Badge text="New in v0.7.3" />
83+
84+
You can update many items at once by using the `updateMany` method on the store. This method takes an array of objects with the properties you want to update. Each object must contain the key (or the properties used to [compute the key](../schema/collection.md#item-key)) of the record you want to update.
85+
86+
```ts
87+
const updatedTodos = await store.todos.updateMany([
88+
{ id: 'id-1', completed: true },
89+
{ id: 'id-2', completed: true },
90+
])
91+
```
92+
93+
It can be useful to batch the update operations into a single fetch request to your backend. See `updateMany` in the [plugin hooks](../plugin/hooks.md#updatemany).
94+
6195
## Delete
6296

6397
To delete a record, you can use the `delete` method on the store. This method takes the key of the record you want to delete.
@@ -94,7 +128,17 @@ if (todo) {
94128
}
95129
```
96130

97-
## Optimistic Updates
131+
## Delete Many <Badge text="New in v0.7.3" />
132+
133+
You can delete many items at once by using the `deleteMany` method on the store. This method takes an array of keys or objects that contain the keys of the records you want to delete.
134+
135+
```ts
136+
await store.todos.deleteMany(['id-1', 'id-2'])
137+
```
138+
139+
It can be useful to batch the delete operations into a single fetch request to your backend. See `deleteMany` in the [plugin hooks](../plugin/hooks.md#deletemany).
140+
141+
## Optimistic Updates <Badge text="New in v0.7" />
98142

99143
By default, rstore will try to perform optimistic updates when you create, update or delete a record. This means that the record will be updated in the store immediately, without waiting for the server to confirm the change. If an error is thrown during the mutation, the change will be automatically reverted.
100144

docs/guide/data/query.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const { data: todos } = store.Todo.query(q => q.many())
1616
const { data: users } = store.users.query(q => q.many())
1717
```
1818

19-
## Query composables
19+
## Query composables <Badge text="Changed in v0.7" type="warning" />
2020

2121
The query composables are the recommended way to fetch data from the server. They are designed to be used in a Vue component and return a reactive result to be used in the components.
2222

@@ -379,7 +379,7 @@ The cache will automatically resolve the relations as soon as the data is availa
379379

380380
Plugins hooked on the `fetchRelations` hook will also be called to potentially fetch the data of the relations. See [Plugin hooks](../plugin/hooks.md#fetching-relations) for more details.
381381

382-
## Customizing Find Options Types
382+
## Customizing Find Options Types <Badge text="Changed in v0.7" type="warning" />
383383

384384
You can customize the `FindOptions` type used in the `first` and `many` query builder methods and in the `peek*`/`find*` methods by declaring a module augmentation for `@rstore/vue` and extending the `FindOptions` interface.
385385

docs/guide/plugin/hooks.md

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,27 @@ hook('createItem', async (payload) => {
210210

211211
:::
212212

213+
### createMany <Badge text="New in v0.7.3" />
214+
215+
This hook is called when rstore needs to create many new items at once when [`createMany()`](../data/mutation.md#create-many) is used.
216+
217+
::: tip
218+
If no `createMany` hook is defined, rstore will fallback to calling the [`createItem`](#createitem) hook for each item in the array. If hooks for `createMany` are defined but none of them [aborts](#aborting), rstore will also fallback to calling the `createItem` hook for each item. Calling `abort()` or `setResult(value)` with a non-empty array in the `createMany` hook will prevent this behavior.
219+
:::
220+
221+
```ts
222+
hook('createMany', async (payload) => {
223+
const result = await fetch(`/api/${payload.collection.name}/batch`, {
224+
method: 'POST',
225+
headers: {
226+
'Content-Type': 'application/json',
227+
},
228+
body: JSON.stringify(payload.items),
229+
}).then(r => r.json())
230+
payload.setResult(result)
231+
})
232+
```
233+
213234
### updateItem
214235

215236
This hook is called when rstore needs to update an existing item.
@@ -266,6 +287,40 @@ hook('updateItem', async (payload) => {
266287
```
267288
:::
268289

290+
### updateMany <Badge text="New in v0.7.3" />
291+
292+
This hook is called when rstore needs to update many existing items at once when [`updateMany()`](../data/mutation.md#update-many) is used.
293+
294+
::: tip
295+
If no `updateMany` hook is defined, rstore will fallback to calling the [`updateItem`](#updateitem) hook for each item in the array. If hooks for `updateMany` are defined but none of them [aborts](#aborting), rstore will also fallback to calling the `updateItem` hook for each item. Calling `abort()` or `setResult(value)` with a non-empty array in the `updateMany` hook will prevent this behavior.
296+
:::
297+
298+
::: danger
299+
300+
Contrary to `createMany` hook, the `updateMany` hook receives an array of items that already contain their keys:
301+
302+
```ts
303+
interface Payload {
304+
items: Array<{ key: string | number, item: any }>
305+
/// ...
306+
}
307+
```
308+
309+
:::
310+
311+
```ts
312+
hook('updateMany', async (payload) => {
313+
const result = await fetch(`/api/${payload.collection.name}/batch`, {
314+
method: 'PATCH',
315+
headers: {
316+
'Content-Type': 'application/json',
317+
},
318+
body: JSON.stringify(payload.items.map(i => i.item)),
319+
}).then(r => r.json())
320+
payload.setResult(result)
321+
})
322+
```
323+
269324
### deleteItem
270325

271326
This hook is called when rstore needs to delete an existing item.
@@ -302,7 +357,25 @@ hook('deleteItem', async (payload) => {
302357
```
303358
:::
304359

305-
### Aborting
360+
### deleteMany <Badge text="New in v0.7.3" />
361+
362+
This hook is called when rstore needs to delete many existing items at once when [`deleteMany()`](../data/mutation.md#delete-many) is used.
363+
364+
It receives an array of keys to delete:
365+
366+
```ts
367+
hook('deleteMany', async (payload) => {
368+
await fetch(`/api/${payload.collection.name}/batch`, {
369+
method: 'DELETE',
370+
headers: {
371+
'Content-Type': 'application/json',
372+
},
373+
body: JSON.stringify(payload.keys),
374+
})
375+
})
376+
```
377+
378+
### Aborting <Badge text="New in v0.7" />
306379

307380
If you have multiple plugins that can handle the same collections, you can abort the remaining callbacks for a *Data handling* hook by calling `abort()` on the payload.
308381

@@ -318,7 +391,25 @@ hook('deleteItem', (payload) => {
318391
```
319392

320393
::: info
321-
For `fetchFirst`, `fetchMany`, `createItem` and `updateItem`, the remaining callbacks are automatically aborted when a non-null result is set with `setResult`. You can override this behavior by passing `{ abort: false }` as the second argument to `setResult`.
394+
For `fetchFirst`, `fetchMany`, `createItem` and `updateItem`, the remaining callbacks are automatically aborted when a non-null result is set with `setResult`.
395+
396+
```ts
397+
pluginApi.hook('fetchFirst', async (payload) => {
398+
// If the item is non-null,
399+
// remaining `fetchFirst` hooks will not be called
400+
payload.setResult(cache.get(payload.key))
401+
// If `cache.get(payload.key)` is null,
402+
// remaining `fetchFirst` hooks will be called
403+
})
404+
```
405+
406+
Note that in the above example, adding a condition to call `setResult` is not necessary because it checks if the value is null or empty (for arrays) before aborting the remaining callbacks.
407+
408+
You can prevent this behavior by setting `abort: false` to the second argument of `setResult()`.
409+
410+
```ts
411+
payload.setResult(cache.get(payload.key), { abort: false })
412+
```
322413
:::
323414

324415
## Fetching relations

docs/guide/plugin/setup.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ app/
6363
You can also add an `app/rstore` folder in Nuxt layers! rstore will automatically add those files too.
6464
:::
6565

66-
## Category
66+
## Category <Badge text="New in v0.7" />
6767

6868
Plugins can be categorized to define their role in the data flow. The available categories are:
6969

@@ -129,6 +129,40 @@ export default definePlugin({
129129

130130
Explore the [Plugin hooks](./hooks.md) for a complete list of available hooks.
131131

132+
### Aborting hook <Badge text="New in v0.7" />
133+
134+
You can abort most the hooks by calling either `setResult()` with a non-null/non-empty value, or `abort()`. This will prevent the remaining plugins from running the same hook. This is useful when you want to short-circuit the data flow, for example when you have a cache plugin that can return the data without needing to call a remote API.
135+
136+
```ts
137+
pluginApi.hook('fetchFirst', async (payload) => {
138+
// If the item is non-null,
139+
// remaining `fetchFirst` hooks will not be called
140+
payload.setResult(cachedmyCache.get(payload.key))
141+
})
142+
```
143+
144+
::: tip
145+
You can prevent this behavior by setting `abort: false` to the second argument of `setResult()`.
146+
147+
```ts
148+
payload.setResult(cachedmyCache.get(payload.key), { abort: false })
149+
```
150+
151+
:::
152+
153+
```ts
154+
pluginApi.hook('fetchFirst', async (payload) => {
155+
if (payload.collection.name === 'SomeSpecialCollection') {
156+
// Do something special here
157+
await doSomethingSpecial()
158+
// Remaining `fetchFirst` hooks will not be called
159+
payload.abort()
160+
}
161+
})
162+
```
163+
164+
See also [Category](#category) and [Sorting plugins](#sorting-plugins). Learn more in [Hooks](./hooks.md#aborting).
165+
132166
## Scope ID
133167

134168
The scope ID allows filtering which plugins will handle the collection. For example, if a collection has a scope A, only plugins with the scope A will be able to handle it by default. This is very useful to handle multiple data sources.
@@ -190,7 +224,7 @@ export default definePlugin({
190224
})
191225
```
192226

193-
## Sorting plugins
227+
## Sorting plugins <Badge text="New in v0.7" />
194228

195229
Plugins are sorted based on their dependencies and category. You can specify that a plugin should be loaded before or after another plugin or category using the `before` and `after` options:
196230

docs/guide/schema/collection.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const store = await createStore({
4040

4141
:::
4242

43-
## Defining a Collection
43+
## Defining a Collection <Badge text="Changed in v0.7" type="warning" />
4444

4545
For JavaScript, you can use the `defineCollection` utility function to define a collection with auto-completion in your IDE:
4646

@@ -88,7 +88,7 @@ const store = await createStore({
8888
The [currying](https://en.wikipedia.org/wiki/Currying) is necessary to specify the type of the item while still letting TypeScript infer the type of the collection. This is a limitation of TypeScript, and [it might improve in the future](https://github.com/microsoft/TypeScript/issues/26242).
8989
:::
9090

91-
## Collection hooks
91+
## Collection hooks <Badge text="New in v0.7" />
9292

9393
You can define hooks on the collection that will be called at different stages of the item lifecycle in the `hooks` option. The available hooks are:
9494

0 commit comments

Comments
 (0)