Collections
This page shows how to use collections: named sets of JSON records, each stored under a key, that the server can filter and sort. A collection suits structured data you look up or query: contacts, bookmarks, settings per device.
Collection paths follow the document rules: letters, digits, -, _ and /, at most 256 characters.
Keys are at most wire.MaxCollectionKeyLength (256) bytes.
Create a collection
Section titled “Create a collection”err := cl.CreateCollection(ctx, "contacts", "Contacts")CreateCollection(ctx, path, name, opts...) creates a collection you own. It is private by default; pass
client.WithCollectionVisibility(wire.VisibilityShared) or wire.VisibilityPublic to open it.
Put and get items
Section titled “Put and get items”Content must be valid JSON; anything else is refused with status 400.
res, err := cl.PutCollectionItem(ctx, "contacts", "ann", []byte(`{"name":"Ann","age":31,"tags":["work"]}`))fmt.Println(res.Created, res.ETag, res.Version) // true sha256:… 1
item, err := cl.GetCollectionItem(ctx, ownerID, "contacts", "ann")if item != nil { fmt.Println(string(item.Content), item.Hash, item.Version, item.UpdatedAt)}PutCollectionItem creates or replaces the item and returns a *client.CollectionItemResult with the
new ETag and version. GetCollectionItem returns nil, nil for a key that does not exist, and
client.ErrForbidden for a collection you may not read. Content is a json.RawMessage, ready to
unmarshal.
For optimistic locking, pass the ETag you last saw. A mismatch is a *client.ConflictError:
_, err = cl.PutCollectionItem(ctx, "contacts", "ann", updated, client.WithCollectionIfMatch(item.Hash))if errors.Is(err, client.ErrConflict) { // re-read, merge, retry}Delete one item, or the whole collection:
deleted, err := cl.DeleteCollectionItem(ctx, "contacts", "ann") // false, nil if absentdeleted, err = cl.DeleteCollection(ctx, "contacts")QueryCollection filters items on their JSON content:
page, err := cl.QueryCollection(ctx, ownerID, "contacts", map[string]any{"age": map[string]any{"$gte": 30}}, client.WithQuerySort("name", true), client.WithQueryLimit(20),)for _, it := range page.Items { fmt.Println(it.Key, string(it.Content))}| Filter | Matches |
|---|---|
{"field": value} |
Items whose field equals value (JSON containment). |
{"field": {"$lt": n}}, $lte, $gt, $gte |
Numeric comparison. |
{"field": {"$ilike": "%ann%"}} |
Case-insensitive pattern on a string field. |
{"field": {"$contains": v}} |
field contains v, such as an element of an array. |
{"$and": [f1, f2]}, {"$or": [f1, f2]} |
Combinations. |
A nil or empty filter matches every item. Field names may contain letters, digits, _, . and -.
| Option | Effect |
|---|---|
WithQuerySort(field, asc) |
Sort by a top-level field, then by key. Default: key order. |
WithQueryLimit(n) |
Page size. Server default 50. |
WithQueryCursor(c) |
Continue from the previous page’s NextCursor. |
WithQueryOffset(n) |
Skip n items. Ignored when a cursor is given. |
WithQueryTotal() |
Report the number of matches in TotalCount. Costs a scan, so it is off by default. |
WithQueryServer(id) |
Send this request to one server only. |
Page through results
Section titled “Page through results”A *client.CollectionQueryResponse has Items, HasMore and NextCursor. Follow the cursor rather than
an offset, so items added between pages do not shift the results:
var cursor stringfor { opts := []client.CollectionQueryOption{client.WithQueryLimit(100)} if cursor != "" { opts = append(opts, client.WithQueryCursor(cursor)) } page, err := cl.QueryCollection(ctx, ownerID, "contacts", nil, opts...) if err != nil { return err } handle(page.Items) if page.NextCursor == "" { break } cursor = page.NextCursor}TotalCount is -1 when the server did not count: on a page fetched by cursor, and on a filtered query
without WithQueryTotal.
List keys
Section titled “List keys”ListCollectionKeys returns one page of keys and the total; ListCollectionKeysPage adds the cursor:
keys, total, err := cl.ListCollectionKeys(ctx, ownerID, "contacts")
kp, err := cl.ListCollectionKeysPage(ctx, ownerID, "contacts", client.WithQueryLimit(500))fmt.Println(kp.Keys, kp.HasMore, kp.NextCursor)Collections and visibility
Section titled “Collections and visibility”info, err := cl.GetCollection(ctx, ownerID, "contacts") // nil, nil if absentfmt.Println(info.Name, info.RecordCount, info.Visibility)
all, err := cl.ListCollections(ctx, ownerID) // the ones you may read
_, err = cl.SetCollectionVisibility(ctx, "contacts", wire.VisibilityShared)access, err := cl.GrantCollectionReader(ctx, "contacts", friendID)_, err = cl.RevokeCollectionReader(ctx, "contacts", friendID)access, err = cl.CollectionAccess(ctx, "contacts")Visibility covers every item and every query. Only the owner writes, whatever the visibility.