Purpose of UserId in storage entries

Confused about the role of UserID in:

type StorageRead struct {
	Collection string
	Key        string
	UserID     string
}

and also in StorageWrite, of course. Does it create a namespace? There doesn’t seem to be any server side docs for the storage engine except a pretty lacking function reference, so I haven’t found any real docs for this stuff. The client side docs have this example:

var writeObjects = new [] {
    new WriteStorageObject
    {
        Collection = "saves",
        Key = "savegame",
        Value = saveGame
    },
};

await client.WriteStorageObjectsAsync(session, writeObjects);

Based on this I assume that the user id is used internally to provide a namespace so that every user gets an own saveGame entry in the saves collection, as otherwise everyone would be overwriting the same file. Is this the case? In my case I want to store a limited history of some stuff the user has received, so should I use:

runtime.StorageRead{
    Collection: "history",
    Key:        storageRequest.userId,
    UserID:     "",
}

or

runtime.StorageRead{
    Collection: "history",
    Key:        "purchases",
    UserID:     storageRequest.userId,
}

in order to have an entry per user? Assuming the entry is written already.

Also, what does:

StorageRead(ctx context.Context, reads []*StorageRead) ([]*api.StorageObject, error)`

do if one entry in the array is missing? In my case I’d always have just one requested entry. Does it give an error or return an empty array? Should all reads be first checked with StorageList? These things would be nice to have in the function reference.

Hello @chakie,

The UserID serves to establish ownership of the object - depending on the read/write permissions on the object its userID may be checked against the session’s userID (if the call is made from the storage client facing APIs, for example) to determine whether the user is authorized to perform the operation.

Internally it’s also used as part of the composite key to uniquely identify the storage object in the DB, as collection and key wouldn’t be unique by themselves.

For the case you describe using the second approach is more appropriate:

runtime.StorageRead{
    Collection: "history",
    Key:        "purchases",
    UserID:     storageRequest.userId,
}

An empty UserID will store the object under the system user (nil UUID). Your alternative example would still work since the composite key would still be unique, but it wouldn’t be very ergonomic.

If StorageRead does not find any of the requested entries it’ll return an empty slice.
@gabriel can we please add a note on the above in the docs?

I don’t think you should check reads beforehand with storageList, but rather surface an error if storageRead doesn’t return the expected number of results, or handle it by creating the object, if appropriate.

Hope this clarifies.

Thank you for the quick reply.

Ok, so the user id is a part of the key. This could probably be mentioned somewhere. The page Heroic Labs Documentation | Collections shows the user id in the little ASCII art illustration, but doesn’t say what makes up a “key”.

When testing confirmed that a missing entry just returns no records which is perfect. I of course check that there is indeed a record before starting to unmarshal the contents.