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:
Reynaldi ChernandoandCopilot Autofix powered by AI authored and GitHub committed 2026-10-02 19:23:39 +07:00
1 parent 384524a027
commit 0fcee62884
3 files changed
+135 -152

No files matched your search

-114
View File
@@ -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.
+81
View File
@@ -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
+54 -38
View File
@@ -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