From 0fcee6288469f1c7fe8a696358167341b829b9ee Mon Sep 17 00:00:00 2001 From: Reynaldi Chernando <12949382+reynaldichernando@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:23:39 +0700 Subject: [PATCH] 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> --- src/docs/src/recipes/kv-append-to-list.md | 114 ---------------------- src/docs/src/recipes/store-items-by-id.md | 81 +++++++++++++++ src/docs/src/recipes/store-small-list.md | 92 +++++++++-------- 3 files changed, 135 insertions(+), 152 deletions(-) delete mode 100644 src/docs/src/recipes/kv-append-to-list.md create mode 100644 src/docs/src/recipes/store-items-by-id.md diff --git a/src/docs/src/recipes/kv-append-to-list.md b/src/docs/src/recipes/kv-append-to-list.md deleted file mode 100644 index 86a644c9e..000000000 --- a/src/docs/src/recipes/kv-append-to-list.md +++ /dev/null @@ -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. diff --git a/src/docs/src/recipes/store-items-by-id.md b/src/docs/src/recipes/store-items-by-id.md new file mode 100644 index 000000000..633c887a4 --- /dev/null +++ b/src/docs/src/recipes/store-items-by-id.md @@ -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 diff --git a/src/docs/src/recipes/store-small-list.md b/src/docs/src/recipes/store-small-list.md index ac162ee27..8d1b53391 100644 --- a/src/docs/src/recipes/store-small-list.md +++ b/src/docs/src/recipes/store-small-list.md @@ -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