Register

Version 11 Document Hierarchy Changes

With the move to the LevelDB database architecture, several limitations imposed on Document hierarchies have been lifted. Previously, it was only feasible to embed child Documents one level deep, and for the vast majority of use cases within the Foundry Virtual Tabletop core software, this was all that was necessary. In the case of Actors, Items, and ActiveEffects, however, this limitation did cause some friction. With the limitation removed, it may pave the way for several other useful hierarchies, but for Version 11, the most immediate impact is with ActiveEffects.

The below diagram illustrates the current Actor hierarchy, with terms that will be used labelled.

          Actor
          /   \
       Item  ActiveEffect <-- Children
         |
   ActiveEffect           <-- Grandchildren

Other terms that may be referred to are:

The Hierarchy Property

All Document classes now have a hierarchy static property. This hierarchy is constructed by inspecting the Document's DataModel. EmbeddedCollectionFields and EmbeddedDocumentFields are considered hierarchical fields and contribute to a Document's hierarchy. The metadata.embedded property is therefore being slowly phased out in favor of hierarchy.

The hierarchy property is an object whose keys are the collection names, and whose values are the DataField instances that define those collections. It is essentially a subset of the Document's schema that contains only the hierarchical fields. This is different to the structure of metadata.embedded, which maps document names to the collection name. In order to facilitate this change, the getCollectionName static method has been added.

Using the getCollectionName helper, Document#getEmbeddedCollection will work when passed either the document name, or the collection name. It is preferable to use this method, rather than accessing the collection directly, in order to make your code agnostic of whether it has been passed the document name or the collection name.

// Don't do this:
const collection = actor[collectionName];

// Do this:
const collection = actor.getEmbeddedCollection(collectionName);

// If you know the name of the collection you want to access, you can still do so directly:
const items = actor.items; // This is still fine.

Descendant Updates

Historically, it has been impossible to update grandchild embedded Documents directly, that is, retrieving a grandchild Document instance and calling update or delete on it. Achieving this could only be done by updating the parent Item's effects array directly, for example:

const item = actor.items.getName('Shield');
const effect = item.effects.getName('Special Effect');

// Disable the effect.
item.update({ effects: [{ _id: effect.id, disabled: true }] });

There are some ongoing issues with the API that will prevent the above from appropriately calling ActiveEffect#_preUpdate or preUpdateActiveEffect, but more than that, it is not very ergonomic, and is awkward to have to use a different pattern in these specific cases rather than using the same API as with other Documents.

In V11, this limitation has been removed, and grandchild Documents can be updated directly, just like any other Document. All CRUD workflow methods and hooks will be called appropriately.

const item = actor.items.getName('Shield');
const effect = item.effects.getName('Special Effect');

// Disable the effect.
effect.update({ disabled: true });

With grandchild Documents able to be updated directly, it may be desirable that their ancestors can react to such changes. Previously only the *EmbeddedDocuments methods existed (such as _onUpdateEmbeddedDocuments), which would react only to direct child updates. A new family of methods, *DescendantDocuments, replaces them. As expected, these methods react to any CRUD operation on any descendant Document. If you wish to react only to direct child updates as before, you can do so by checking the parent argument.

_onUpdateDescendantDocuments(parent, collection, documents, data, options, userId) {
  if ( parent === this ) {
    // The documents were direct children of this one.
  }
}