Using Items

This section assumes you have already read the overview and understand what collections and items are and what each are for.

Items are very similar to collections in how you interact with them, so this section may look familiar, especially if you just finished the previous one. However, there are some differences, especially when uploading data.

Prerequisite: have a collection

This section assumes you already have a collection created and uploaded. We already covered it in the previous section, but as a reminder:

const etebase = await Etebase.Account.login("username", "password");
const collectionManager = etebase.getCollectionManager();
const collection = await collectionManager.create("cyberdyne.files",
{
name: "My files",
description: "A collection of files of different types",
},
""
);
await collectionManager.upload(collection);

Creating items

// We can reuse the collection and manager from above
const collectionManager = ...;
const collection = ...;
// Similar to how we have collection manager
const itemManager = collectionManager.getItemManager(collection);
// Create, encrypt and upload a new item
const item = await itemManager.create(
{
type: "file",
name: "note.txt",
mtime: (new Date()).getTime(),
},
"My secret note",
);
await itemManager.batch([item]);

Fetching items

Fetching items is very similar to fetching collections:

Simple fetch

const itemManager = collectionManager.getItemManager(collection);
const items = await itemManager.list();
/*
Items:
{
data: Etebase.Item[], // Returned array of items
stoken: string, // The sync token for this fetch
... // More fields we'll cover later
}
*/

The number of returned items is limited by default, and you can control this limit by passing a different limit parameter as we'll see in the next example:

const items = await itemManager.list({ limit: 50 });

Only fetch recent changes

We can use the stoken we have gotten in previous fetches to only return changed items.

const stoken = "..."; // An stoken we got previously (items.stoken)
const items = await itemManager.list({ stoken });

Fetch in chunks

We can use a combination of limit and stoken to fetch the changes in chunks rather than all at once. This is more resistant to spotty internet connections, and means we can show data to users faster.

let stoken = null;
while (true) {
const items = await itemManager.list({ stoken, limit: 30 });
stoken = items.stoken;
processNewItems(items.data);
if (items.done) {
break;
}
}

Fetch by uid

Sometimes we don't care about getting the whole list of items, and we are just interested in fetching one item based on its uid. We can do it like this:

const item = await itemManager.fetch(itemUid);
// Can optionally pass stoken to only return the item if changed:
const item = await itemManager.fetch(itemUid, { stoken });

Fetch a group of items

In addition to fetching all of the changes in a collection, you can also limit the fetching to only a specific subset of items. This is useful, for example, if your data is structured hierarchically (e.g. a directory tree), and you are only interested in refreshing the currently viewed directory.

const toFetch = [item1, item2, item3];
const items = await itemManager.fetchUpdates(toFetch);
// Can optionally pass stoken to limit to new changes
const items = await itemManager.fetchUpdates(toFetch, { stoken });

Modifying and deleting items

Modifying items is easy, it's just a matter of changing them and uploading them.

await item.setContent("new secret content");
await itemManager.batch([item]);

Deleting is even easier:

await item.delete();
await itemManager.batch([item]);

Uploading multiple items

As you saw in the previous examples, unlike the collection's upload, batch accepts an array of items. This can be used for uploading multiple items at once:

await itemManager.batch([item1, item2, item3, ...]);

Advanced uploads and transactions

In the examples above we always uploaded the items in a way that overwrote whatever is on the server, regardless if it has changed since we last fetched it, or not. While this is fine in many cases, in some cases you want to prevent that in order to ensure the consistency of data.

Transactions

The easiest way to ensure consistency is just to use transactions. Transactions make sure that what we think is the most recent version, really is, and will fail otherwise. This consistency check is done across all of the items, and if one item fails the check, the whole transaction fails.

// -> On device A:
const item1 = await.itemManager.fetch(itemUid1);
const item2 = await.itemManager.fetch(itemUid2);
// -> On device B:
const item1 = await.itemManager.fetch(itemUid1);
await item1.setContent("something else for item 1");
await itemManager.batch([item1]);
// -> On device A (using the previously saved item)
await item1.setContent("new content for item 1");
await item2.setContent("new content for item 2");
// Will fail because item1 changed on device B
await itemManager.transaction([item1, item2]);
// Will succeed
await itemManager.batch([item1, item2]);
// Will succeed because item2 hasn't changed on device B
await itemManager.transaction([item2]);

Using stoken

Like with collections, transactions will only fail if the items themselves have changed, but will not fail if another item of the collection has changed. In some cases we want to have collection-wide consistency and want to make sure nothing has changed.

// -> On device A:
const stoken = collection.stoken;
const item = await.itemManager.fetch(itemUid);
// -> On device B:
const anotherItem = await.itemManager.fetch(anotherItemUid);
await anotherItem.setContent("content for another item");
await itemManager.batch([anotherItem]);
// -> On device A (using the previously saved items and stoken)
await item.setContent("new secret content");
// Will both fail
await itemManager.transaction([item], null, { stoken });
await itemManager.batch([item], null, { stoken });
// Will both succeed
await itemManager.transaction([item]);
await itemManager.batch([item]);

Additional dependencies

Sometimes we may want a transaction or batch upload to fail if some items have changed but not upload them. These are called dependencies and can be passed to both transactions and batches.

// -> On device A:
const item1 = await.itemManager.fetch(itemUid1);
const item2 = await.itemManager.fetch(itemUid2);
// -> On device B:
const item1 = await.itemManager.fetch(itemUid1);
await item1.setContent("something else for item 1");
await itemManager.batch([item1]);
// -> On device A (using the previously saved items and stoken)
await item2.setContent("new secret content");
// Will both fail because item1 changed
await itemManager.transaction([item2], [item1]);
await itemManager.batch([item2], [item1]);
// Can even use the item in both the list and deps in batch
// Will fail because item1 changed on device B
await itemManager.batch([item1, item2], [item1]);

Treating collections as items

In the examples above we only covered consistency of items, but what happens if we want to ensure the collection itself is consistent with its items? One case where this is useful, is if your data is ordered hierarchically (e.g. as a tree) with the collection as the root. In this case, you will want to be able to create a child and have it added to the root node in the same transaction.

In Etebase, collections are essentially just items with some extra data, so you can use collections directly as items with just a small difference.

// A pre-existing collection and a few items:
const collection = ...;
const item1 = ...;
const item2 = ...;
// Get the item out of the collection
const colItem = collection.item;
// The collection item can then be used like any other item:
await itemManager.transaction([colItem, item1], [item2]);
await itemManager.transaction([item1, item2], [colItem]);
await itemManager.batch([colItem, item1]);
// In addition, these are true:
assert((await collection.getMeta()) === (await colItem.getMeta()));
assert((await collection.getContent()) === (await colItem.getContent()));

You can also fetch the collection as an item from all of the item API functions:

const itemManager = collectionManager.getItemManager(collection);
// Will return the collection item as part of the list:
const items = await itemManager.list({ withCollection: true });
// Assuming the collection is the first item returned:
const colItem = items.data[0];
assert(colItem.uid === collection.uid);
// You can also fetch collection items based on UID:
const colItem = await itemManager.fetch(collection.uid);
// Or fetch updates of the collection along with other items:
const items = await itemManager.fetchUpdates([collection.item, item1]);

Binary content

In the examples above content was always a string. However, content is actually a binary blob of data, not a string. Using it as a string is just a convenience.

Here is how you can control the formatting of the data:

// default, returns a Uint8Array
item.getContent();
// tries to convert the binary data to a string and returns that
item.getContent(Etebase.OutputFormat.String);
// Sets the content to a binary blob
item.setContent(Uint8Array.from([72, 101, 108, 108, 111]));
// Sets the content to a string
item.setContent("Hello");