mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-10 22:01:40 +00:00
Update recipes for storing a small list and store per id (#4028)
* Update recipes for storing a small list and store per id * Potential fix for pull request finding 'Clarify that IDs must be path-safe or properly escaped' Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
384524a027
commit
0fcee62884
3 files changed
+135
-152
No files matched your search
@@ -1,114 +0,0 @@
|
||||
---
|
||||
title: Append to a List
|
||||
description: "Learn how to add items to a list that only grows, such as a log, a chat transcript or an activity feed, with one Puter.js key-value call."
|
||||
tags: [kv, data-modeling]
|
||||
order: 10
|
||||
draft: true
|
||||
---
|
||||
|
||||
Some lists only ever grow: an activity log, a chat transcript, a history feed.
|
||||
Items are written once and never changed, and the list is read whole with one
|
||||
[`puter.kv.get()`](/KV/get/). The [key-value store](/KV/) appends to a stored
|
||||
array for you, so you never read the list just to add to it.
|
||||
[Store data](/recipes/store-data/) covers the basic reads and writes.
|
||||
|
||||
If items get edited or deleted later, keep them in an object keyed by id
|
||||
instead, as in [Store a Small List](/recipes/store-small-list/).
|
||||
|
||||
## Append an Item
|
||||
|
||||
To add an item to the end of the list, use the
|
||||
[`puter.kv.add()`](/KV/add/) method and wrap the item in an array:
|
||||
|
||||
```js
|
||||
await puter.kv.add('log', [{ at: Date.now(), event: 'opened' }]);
|
||||
```
|
||||
|
||||
A key that doesn't exist yet is created as an array, so there is nothing to set
|
||||
up first. The call returns the whole updated list.
|
||||
|
||||
## Why Not Use get() and set()
|
||||
|
||||
Reading the list, pushing an item and writing it back loses items. Two tabs can
|
||||
both read the same list, and whichever writes second drops the other's item:
|
||||
|
||||
```js
|
||||
// Don't do this
|
||||
const log = await puter.kv.get('log') ?? [];
|
||||
log.push({ at: Date.now(), event: 'opened' });
|
||||
await puter.kv.set('log', log);
|
||||
```
|
||||
|
||||
[`puter.kv.add()`](/KV/add/) appends inside the database in a single write, so
|
||||
two calls at the same moment always add both items. It is also one call
|
||||
instead of two.
|
||||
|
||||
## Append Several Items
|
||||
|
||||
An array argument is spread: each element is appended on its own. This adds
|
||||
two entries, not one nested array:
|
||||
|
||||
```js
|
||||
await puter.kv.add('log', [
|
||||
{ at: Date.now(), event: 'edited' },
|
||||
{ at: Date.now(), event: 'saved' },
|
||||
]);
|
||||
```
|
||||
|
||||
## Always Wrap Objects in an Array
|
||||
|
||||
A bare object is not an item. [`puter.kv.add()`](/KV/add/) reads each of its
|
||||
keys as a path inside the stored value:
|
||||
|
||||
```js
|
||||
// Wrong: reads `at` and `event` as paths, and a list has neither
|
||||
await puter.kv.add('log', { at: Date.now(), event: 'closed' });
|
||||
|
||||
// Right: one item, appended to the list
|
||||
await puter.kv.add('log', [{ at: Date.now(), event: 'closed' }]);
|
||||
```
|
||||
|
||||
The wrong form rejects with `invalid_path` and leaves the list as it
|
||||
was. Wrapping every item in an array is the one rule that always works.
|
||||
|
||||
## Append to a List Inside an Object
|
||||
|
||||
The object form is useful when the list sits inside a record. Name the path to
|
||||
the list, and pass the items to append there:
|
||||
|
||||
```js
|
||||
await puter.kv.set('profile', { name: 'Ada', tags: ['alpha'] });
|
||||
|
||||
await puter.kv.add('profile', { tags: ['beta', 'gamma'] });
|
||||
// { name: 'Ada', tags: ['alpha', 'beta', 'gamma'] }
|
||||
```
|
||||
|
||||
Paths use dot notation, so `{ 'settings.labels': ['urgent'] }` appends to
|
||||
`settings.labels`. Missing objects along the path are created for you, and the
|
||||
rest of the record is left alone.
|
||||
|
||||
## Read the List
|
||||
|
||||
To show the list, read it with [`puter.kv.get()`](/KV/get/). A list that was
|
||||
never written comes back as `null`, so default it to an empty array:
|
||||
|
||||
```js
|
||||
const log = await puter.kv.get('log') ?? [];
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- A value is capped at **400 KB**, and a list that grows forever reaches it.
|
||||
Put the period in the key, such as `log:2026-09`, so each month starts a new
|
||||
list, or give each item its own key as in [Store a Large
|
||||
Collection](/recipes/store-large-collection/).
|
||||
- [`puter.kv.add()`](/KV/add/) returns the whole updated list, so appends to a
|
||||
long list get slower as it grows. Another reason to roll over to a new key.
|
||||
- Each call counts toward the key-value rate limit of **400 calls per 10
|
||||
seconds** (200 for guest accounts). See [Rate Limits and
|
||||
Quotas](/rate-limits-and-quotas/).
|
||||
- A list whose TTL ran out starts fresh: the next
|
||||
[`puter.kv.add()`](/KV/add/) creates a new list.
|
||||
[Store Temporary Data](/recipes/store-temporary-data/) covers TTLs.
|
||||
- To count things, use a counter instead. [Add
|
||||
Counters](/recipes/add-counters/) shows how.
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: Store Items by ID
|
||||
description: "Learn how to keep a list in one Puter.js key-value entry with each item stored under a unique ID, so you can edit or delete any item by its ID. It fits a few thousand small items."
|
||||
tags: [kv, data-modeling]
|
||||
order: 20
|
||||
---
|
||||
|
||||
[Store a small list](/recipes/store-small-list/) keeps items in an array and
|
||||
edits them by index. When the same user edits the list from two tabs or devices
|
||||
at once, a delete in one tab shifts the indexes, and the other tab can edit or
|
||||
delete the wrong item.
|
||||
|
||||
Storing the list as an object keyed by item ID avoids that. Each item has a
|
||||
fixed ID, so an edit or delete always targets the correct item. The list is
|
||||
still one key-value entry, and you read all of it with a single
|
||||
[`puter.kv.get()`](/KV/get/) call.
|
||||
|
||||
## Add an Item
|
||||
|
||||
To add an item, use the [`puter.kv.update()`](/KV/update/) method with the id as
|
||||
the path:
|
||||
|
||||
```js
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
await puter.kv.update('todos', {
|
||||
[id]: { text: 'Buy milk', done: false, at: Date.now() },
|
||||
});
|
||||
```
|
||||
|
||||
The id becomes the key you reference later to update or delete that item. Because it is used as a KV path, use a path-safe unique string; [`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID) is a safe default.
|
||||
|
||||
## Show the List
|
||||
|
||||
To load the list, use the [`puter.kv.get()`](/KV/get/) method. One read returns
|
||||
every item:
|
||||
|
||||
```js
|
||||
const todos = await puter.kv.get('todos') ?? {};
|
||||
|
||||
const items = Object.entries(todos)
|
||||
.map(([id, todo]) => ({ id, ...todo }))
|
||||
.sort((a, b) => a.at - b.at);
|
||||
```
|
||||
|
||||
An object has no inherent order, and the stored field order is not preserved on
|
||||
read, so carry an `at` or `order` field on each item and sort when you render.
|
||||
At sizes that fit in one entry, sorting in memory costs nothing measurable.
|
||||
|
||||
## Edit an Item
|
||||
|
||||
To change one field of one item, use the [`puter.kv.update()`](/KV/update/)
|
||||
method with the item's id and the field you are changing:
|
||||
|
||||
```js
|
||||
await puter.kv.update('todos', { [`${ id }.done`]: true });
|
||||
```
|
||||
|
||||
This changes that one item and leaves the rest of the list as it was.
|
||||
|
||||
## Delete an Item
|
||||
|
||||
To remove an item, use the [`puter.kv.remove()`](/KV/remove/) method with its
|
||||
id:
|
||||
|
||||
```js
|
||||
await puter.kv.remove('todos', id);
|
||||
```
|
||||
|
||||
It also takes several paths in one call, so `remove('todos', idA, idB)` deletes
|
||||
two items at once.
|
||||
|
||||
## When to Switch
|
||||
|
||||
One entry holds up to [400 KB](/KV/MAX_VALUE_SIZE/), which is a few thousand
|
||||
small items. If your list holds more than that, or you expect it to, [store a
|
||||
large collection](/recipes/store-large-collection/) instead, which gives you:
|
||||
|
||||
- no ceiling on how many records you keep
|
||||
- an expiry per record, instead of one for the whole list
|
||||
- reads a page at a time, instead of the whole list on every render
|
||||
@@ -1,76 +1,92 @@
|
||||
---
|
||||
title: Store a Small List
|
||||
description: "Learn how to keep a list of items, such as todos or notes, inside one key-value entry so you can retrieve data in a single read. It fits a few thousand small items."
|
||||
description: "Learn how to keep a list in one Puter.js key-value entry, so your app loads it with a single read and edits any item in place. It fits a few thousand small items."
|
||||
tags: [kv, data-modeling]
|
||||
order: 20
|
||||
order: 10
|
||||
---
|
||||
|
||||
Most applications keep a list the user edits later, such as todos, notes, saved
|
||||
records or a task board. The whole list can live in one key-value entry, so a
|
||||
screen loads with a single [`puter.kv.get()`](/KV/get/) and there is nothing to
|
||||
page through.
|
||||
|
||||
You can store the list as an object with an item id key. This lets you add,
|
||||
edit, and delete each item in one call without manually reading the entire list.
|
||||
Most applications keep a list the user adds to and edits later, such as todos,
|
||||
notes, saved records or an activity log. The whole list can live in one
|
||||
key-value entry as an array, so you read all of it with a single
|
||||
[`puter.kv.get()`](/KV/get/) call, without pagination.
|
||||
|
||||
## Add an Item
|
||||
|
||||
To add an item, use the [`puter.kv.update()`](/KV/update/) method with the id as
|
||||
the path:
|
||||
To add an item to the end of the list, use the [`puter.kv.add()`](/KV/add/)
|
||||
method and wrap the item in an array:
|
||||
|
||||
```js
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
await puter.kv.update('todos', {
|
||||
[id]: { text: 'Buy milk', done: false, at: Date.now() },
|
||||
});
|
||||
await puter.kv.add('todos', [{ text: 'Buy milk', done: false }]);
|
||||
```
|
||||
|
||||
The id becomes the key you reference later to update or delete that item. Any
|
||||
unique string works, and
|
||||
[`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID)
|
||||
is a safe default.
|
||||
If the key does not exist yet, it is created as an array. The append is atomic,
|
||||
so two concurrent calls both add their item.
|
||||
|
||||
To add several items, put them all in the array. Each element is appended as a
|
||||
separate item:
|
||||
|
||||
```js
|
||||
await puter.kv.add('todos', [
|
||||
{ text: 'Walk the dog', done: false },
|
||||
{ text: 'Call mom', done: false },
|
||||
]);
|
||||
```
|
||||
|
||||
## Show the List
|
||||
|
||||
To load the list, use the [`puter.kv.get()`](/KV/get/) method. One read returns
|
||||
every item:
|
||||
every item in the order it was added, so you don't need an additional field for
|
||||
sorting. A list that was never written comes back as `null`, so default it to an
|
||||
empty array:
|
||||
|
||||
```js
|
||||
const todos = await puter.kv.get('todos') ?? {};
|
||||
|
||||
const items = Object.entries(todos)
|
||||
.map(([id, todo]) => ({ id, ...todo }))
|
||||
.sort((a, b) => a.at - b.at);
|
||||
const todos = await puter.kv.get('todos') ?? [];
|
||||
```
|
||||
|
||||
An object has no inherent order, and the stored field order is not preserved on
|
||||
read, so carry an `at` or `order` field on each item and sort when you render.
|
||||
At sizes that fit in one entry, sorting in memory costs nothing measurable.
|
||||
|
||||
## Edit an Item
|
||||
|
||||
To change one field of one item, use the [`puter.kv.update()`](/KV/update/)
|
||||
method with the item's id and the field you are changing:
|
||||
method with a path made of the item's index in brackets, then the field name.
|
||||
This marks the first todo as done:
|
||||
|
||||
```js
|
||||
await puter.kv.update('todos', { [`${ id }.done`]: true });
|
||||
await puter.kv.update('todos', { '[0].done': true });
|
||||
```
|
||||
|
||||
This updates the specific property of the object with that id, without you
|
||||
having to manually iterate the whole list and update it.
|
||||
The index is the item's position in the array you read with
|
||||
[`puter.kv.get()`](/KV/get/). To edit an item you know by a field value, find
|
||||
its index first:
|
||||
|
||||
```js
|
||||
const todos = await puter.kv.get('todos') ?? [];
|
||||
const index = todos.findIndex((todo) => todo.text === 'Buy milk');
|
||||
|
||||
await puter.kv.update('todos', { [`[${ index }].done`]: true });
|
||||
```
|
||||
|
||||
To replace the whole item, use the index on its own:
|
||||
|
||||
```js
|
||||
await puter.kv.update('todos', { [`[${ index }]`]: { text: 'Buy oat milk', done: false } });
|
||||
```
|
||||
|
||||
## Delete an Item
|
||||
|
||||
To remove an item, use the [`puter.kv.remove()`](/KV/remove/) method with its
|
||||
id:
|
||||
index in brackets:
|
||||
|
||||
```js
|
||||
await puter.kv.remove('todos', id);
|
||||
await puter.kv.remove('todos', `[${ index }]`);
|
||||
```
|
||||
|
||||
It also takes several paths in one call, so `remove('todos', idA, idB)` deletes
|
||||
two items at once.
|
||||
Every item after it moves down by one index. To delete several items, pass all
|
||||
their indexes in one call, such as `remove('todos', '[0]', '[3]')`. The indexes
|
||||
in one call refer to the list as it was before the call.
|
||||
|
||||
Because indexes shift, a list edited from two tabs or devices at once can go
|
||||
wrong. After one tab deletes an item, the other tab still has the old indexes
|
||||
and can edit or delete the wrong item. In that case, [store items by
|
||||
ID](/recipes/store-items-by-id/) instead, where each item has a fixed ID.
|
||||
|
||||
## When to Switch
|
||||
|
||||
|
||||
Reference in new issue
Block a user