Files
Foundry-VTT-Docker/resources/app/common/abstract/document.mjs
2025-01-04 00:34:03 +01:00

1214 lines
48 KiB
JavaScript

import DataModel from "./data.mjs";
import {getDefiningClass, getProperty, hasProperty, setProperty, mergeObject} from "../utils/helpers.mjs";
import * as CONST from "../constants.mjs";
import {logCompatibilityWarning} from "../utils/logging.mjs";
import {SchemaField, TypeDataField} from "../data/fields.mjs";
/**
* An extension of the base DataModel which defines a Document.
* Documents are special in that they are persisted to the database and referenced by _id.
* @memberof abstract
* @abstract
* @alias foundry.abstract.Document
*
* @param {object} data Initial data from which to construct the Document
* @param {DocumentConstructionContext} context Construction context options
*
* @property {string|null} _id The document identifier, unique within its Collection, or null if the
* Document has not yet been assigned an identifier
* @property {string} [name] Documents typically have a human-readable name
* @property {DataModel} [system] Certain document types may have a system data model which contains
* subtype-specific data defined by the game system or a module
* @property {DocumentStats} [_stats] Primary document types have a _stats object which provides metadata
* about their status
* @property {Record<string, any>} flags Documents each have an object of arbitrary flags which are used by
* systems or modules to store additional Document-specific data
*/
export default class Document extends DataModel {
/** @override */
_configure({pack=null, parentCollection=null}={}) {
/**
* An immutable reverse-reference to the name of the collection that this Document exists in on its parent, if any.
* @type {string|null}
*/
Object.defineProperty(this, "parentCollection", {
value: this._getParentCollection(parentCollection),
writable: false
});
/**
* An immutable reference to a containing Compendium collection to which this Document belongs.
* @type {string|null}
*/
Object.defineProperty(this, "pack", {
value: (() => {
if ( typeof pack === "string" ) return pack;
if ( this.parent?.pack ) return this.parent.pack;
if ( pack === null ) return null;
throw new Error("The provided compendium pack ID must be a string");
})(),
writable: false
});
// Construct Embedded Collections
const collections = {};
for ( const [fieldName, field] of Object.entries(this.constructor.hierarchy) ) {
if ( !field.constructor.implementation ) continue;
const data = this._source[fieldName];
const c = collections[fieldName] = new field.constructor.implementation(fieldName, this, data);
Object.defineProperty(this, fieldName, {value: c, writable: false});
}
/**
* A mapping of embedded Document collections which exist in this model.
* @type {Record<string, EmbeddedCollection>}
*/
Object.defineProperty(this, "collections", {value: Object.seal(collections), writable: false});
}
/* ---------------------------------------- */
/**
* Ensure that all Document classes share the same schema of their base declaration.
* @type {SchemaField}
* @override
*/
static get schema() {
if ( this._schema ) return this._schema;
const base = this.baseDocument;
if ( !base.hasOwnProperty("_schema") ) {
const schema = new SchemaField(Object.freeze(base.defineSchema()));
Object.defineProperty(base, "_schema", {value: schema, writable: false});
}
Object.defineProperty(this, "_schema", {value: base._schema, writable: false});
return base._schema;
}
/* -------------------------------------------- */
/** @inheritdoc */
_initialize(options={}) {
super._initialize(options);
const singletons = {};
for ( const [fieldName, field] of Object.entries(this.constructor.hierarchy) ) {
if ( field instanceof foundry.data.fields.EmbeddedDocumentField ) {
Object.defineProperty(singletons, fieldName, { get: () => this[fieldName] });
}
}
/**
* A mapping of singleton embedded Documents which exist in this model.
* @type {Record<string, Document>}
*/
Object.defineProperty(this, "singletons", {value: Object.seal(singletons), configurable: true});
}
/* -------------------------------------------- */
/** @override */
static *_initializationOrder() {
const hierarchy = this.hierarchy;
// Initialize non-hierarchical fields first
for ( const [name, field] of this.schema.entries() ) {
if ( name in hierarchy ) continue;
yield [name, field];
}
// Initialize hierarchical fields last
for ( const [name, field] of Object.entries(hierarchy) ) {
yield [name, field];
}
}
/* -------------------------------------------- */
/* Model Configuration */
/* -------------------------------------------- */
/**
* Default metadata which applies to each instance of this Document type.
* @type {object}
*/
static metadata = Object.freeze({
name: "Document",
collection: "documents",
indexed: false,
compendiumIndexFields: [],
label: "DOCUMENT.Document",
coreTypes: [CONST.BASE_DOCUMENT_TYPE],
embedded: {},
permissions: {
create: "ASSISTANT",
update: "ASSISTANT",
delete: "ASSISTANT"
},
preserveOnImport: ["_id", "sort", "ownership"],
/*
* The metadata has to include the version of this Document schema, which needs to be increased
* whenever the schema is changed such that Document data created before this version
* would come out different if `fromSource(data).toObject()` was applied to it so that
* we always vend data to client that is in the schema of the current core version.
* The schema version needs to be bumped if
* - a field was added or removed,
* - the class/type of any field was changed,
* - the casting or cleaning behavior of any field class was changed,
* - the data model of an embedded data field was changed,
* - certain field properties are changed (e.g. required, nullable, blank, ...), or
* - there have been changes to cleanData or migrateData of the Document.
*
* Moreover, the schema version needs to be bumped if the sanitization behavior
* of any field in the schema was changed.
*/
schemaVersion: undefined
});
/* -------------------------------------------- */
/**
* The database backend used to execute operations and handle results.
* @type {abstract.DatabaseBackend}
*/
static get database() {
return globalThis.CONFIG.DatabaseBackend;
}
/* -------------------------------------------- */
/**
* Return a reference to the configured subclass of this base Document type.
* @type {typeof Document}
*/
static get implementation() {
return globalThis.CONFIG[this.documentName]?.documentClass || this;
}
/* -------------------------------------------- */
/**
* The base document definition that this document class extends from.
* @type {typeof Document}
*/
static get baseDocument() {
let cls;
let parent = this;
while ( parent ) {
cls = parent;
parent = Object.getPrototypeOf(cls);
if ( parent === Document ) return cls;
}
throw new Error(`Base Document class identification failed for "${this.documentName}"`);
}
/* -------------------------------------------- */
/**
* The named collection to which this Document belongs.
* @type {string}
*/
static get collectionName() {
return this.metadata.collection;
}
get collectionName() {
return this.constructor.collectionName;
}
/* -------------------------------------------- */
/**
* The canonical name of this Document type, for example "Actor".
* @type {string}
*/
static get documentName() {
return this.metadata.name;
}
get documentName() {
return this.constructor.documentName;
}
/* ---------------------------------------- */
/**
* The allowed types which may exist for this Document class.
* @type {string[]}
*/
static get TYPES() {
return Object.keys(game.model[this.metadata.name]);
}
/* -------------------------------------------- */
/**
* Does this Document support additional subtypes?
* @type {boolean}
*/
static get hasTypeData() {
return this.metadata.hasTypeData;
}
/* -------------------------------------------- */
/* Model Properties */
/* -------------------------------------------- */
/**
* The Embedded Document hierarchy for this Document.
* @returns {Readonly<Record<string, EmbeddedCollectionField|EmbeddedDocumentField>>}
*/
static get hierarchy() {
const hierarchy = {};
for ( const [fieldName, field] of this.schema.entries() ) {
if ( field.constructor.hierarchical ) hierarchy[fieldName] = field;
}
Object.defineProperty(this, "hierarchy", {value: Object.freeze(hierarchy), writable: false});
return hierarchy;
}
/* -------------------------------------------- */
/**
* Identify the collection in a parent Document that this Document belongs to, if any.
* @param {string|null} [parentCollection] An explicitly provided parent collection name.
* @returns {string|null}
* @internal
*/
_getParentCollection(parentCollection) {
if ( !this.parent ) return null;
if ( parentCollection ) return parentCollection;
return this.parent.constructor.getCollectionName(this.documentName);
}
/**
* The canonical identifier for this Document.
* @type {string|null}
*/
get id() {
return this._id;
}
/**
* Test whether this Document is embedded within a parent Document
* @type {boolean}
*/
get isEmbedded() {
return !!(this.parent && this.parentCollection);
}
/* -------------------------------------------- */
/**
* A Universally Unique Identifier (uuid) for this Document instance.
* @type {string}
*/
get uuid() {
let parts = [this.documentName, this.id];
if ( this.parent ) parts = [this.parent.uuid].concat(parts);
else if ( this.pack ) parts = ["Compendium", this.pack].concat(parts);
return parts.join(".");
}
/* ---------------------------------------- */
/* Model Permissions */
/* ---------------------------------------- */
/**
* Test whether a given User has a sufficient role in order to create Documents of this type in general.
* @param {documents.BaseUser} user The User being tested
* @return {boolean} Does the User have a sufficient role to create?
*/
static canUserCreate(user) {
// TODO: https://github.com/foundryvtt/foundryvtt/issues/11280
const perm = this.metadata.permissions.create;
if ( perm instanceof Function ) {
throw new Error('Document.canUserCreate is not supported for this document type. ' +
'Use Document#canUserModify(user, "create") to test whether a user is permitted to create a ' +
'specific document instead.');
}
return user.hasPermission(perm) || user.hasRole(perm, {exact: false});
}
/* ---------------------------------------- */
/**
* Get the explicit permission level that a User has over this Document, a value in CONST.DOCUMENT_OWNERSHIP_LEVELS.
* This method returns the value recorded in Document ownership, regardless of the User's role.
* To test whether a user has a certain capability over the document, testUserPermission should be used.
* @param {documents.BaseUser} [user=game.user] The User being tested
* @returns {number|null} A numeric permission level from CONST.DOCUMENT_OWNERSHIP_LEVELS or null
*/
getUserLevel(user) {
user = user || game.user;
// Compendium content uses role-based ownership
if ( this.pack ) return this.compendium.getUserLevel(user);
// World content uses granular per-User ownership
const ownership = this["ownership"] || {};
return ownership[user.id] ?? ownership.default ?? null;
}
/* ---------------------------------------- */
/**
* Test whether a certain User has a requested permission level (or greater) over the Document
* @param {documents.BaseUser} user The User being tested
* @param {string|number} permission The permission level from DOCUMENT_OWNERSHIP_LEVELS to test
* @param {object} options Additional options involved in the permission test
* @param {boolean} [options.exact=false] Require the exact permission level requested?
* @return {boolean} Does the user have this permission level over the Document?
*/
testUserPermission(user, permission, {exact=false}={}) {
const perms = CONST.DOCUMENT_OWNERSHIP_LEVELS;
let level;
if ( user.isGM ) level = perms.OWNER;
else if ( user.isBanned ) level = perms.NONE;
else level = this.getUserLevel(user);
const target = (typeof permission === "string") ? (perms[permission] ?? perms.OWNER) : permission;
return exact ? level === target : level >= target;
}
/* ---------------------------------------- */
/**
* Test whether a given User has permission to perform some action on this Document
* @param {documents.BaseUser} user The User attempting modification
* @param {string} action The attempted action
* @param {object} [data] Data involved in the attempted action
* @return {boolean} Does the User have permission?
*/
canUserModify(user, action, data={}) {
const permissions = this.constructor.metadata.permissions;
const perm = permissions[action];
// Specialized permission test function
if ( perm instanceof Function ) return perm(user, this, data);
// User-level permission
else if ( perm in CONST.USER_PERMISSIONS ) return user.hasPermission(perm);
// Document-level permission
const isOwner = this.testUserPermission(user, "OWNER");
const hasRole = (perm in CONST.USER_ROLES) && user.hasRole(perm);
return isOwner || hasRole;
}
/* ---------------------------------------- */
/* Model Methods */
/* ---------------------------------------- */
/**
* Clone a document, creating a new document by combining current data with provided overrides.
* The cloned document is ephemeral and not yet saved to the database.
* @param {Object} [data={}] Additional data which overrides current document data at the time
* of creation
* @param {DocumentConstructionContext} [context={}] Additional context options passed to the create method
* @param {boolean} [context.save=false] Save the clone to the World database?
* @param {boolean} [context.keepId=false] Keep the same ID of the original document
* @param {boolean} [context.addSource=false] Track the clone source.
* @returns {Document|Promise<Document>} The cloned Document instance
*/
clone(data={}, {save=false, keepId=false, addSource=false, ...context}={}) {
if ( !keepId ) data["-=_id"] = null;
if ( addSource ) data["_stats.duplicateSource"] = this.uuid;
context.parent = this.parent;
context.pack = this.pack;
context.strict = false;
const doc = super.clone(data, context);
return save ? this.constructor.create(doc, context) : doc;
}
/* -------------------------------------------- */
/**
* For Documents which include game system data, migrate the system data object to conform to its latest data model.
* The data model is defined by the template.json specification included by the game system.
* @returns {object} The migrated system data object
*/
migrateSystemData() {
if ( !this.constructor.hasTypeData ) {
throw new Error(`The ${this.documentName} Document does not include a TypeDataField.`);
}
if ( (this.system instanceof DataModel) && !(this.system.modelProvider instanceof System) ) {
throw new Error(`The ${this.documentName} Document does not have system-provided package data.`);
}
const model = game.model[this.documentName]?.[this["type"]] || {};
return mergeObject(model, this["system"], {
insertKeys: false,
insertValues: true,
enforceTypes: false,
overwrite: true,
inplace: false
});
}
/* ---------------------------------------- */
/** @inheritdoc */
toObject(source=true) {
const data = super.toObject(source);
return this.constructor.shimData(data);
}
/* -------------------------------------------- */
/* Database Operations */
/* -------------------------------------------- */
/**
* Create multiple Documents using provided input data.
* Data is provided as an array of objects where each individual object becomes one new Document.
*
* @param {Array<object|Document>} data An array of data objects or existing Documents to persist.
* @param {Partial<Omit<DatabaseCreateOperation, "data">>} [operation={}] Parameters of the requested creation
* operation
* @return {Promise<Document[]>} An array of created Document instances
*
* @example Create a single Document
* ```js
* const data = [{name: "New Actor", type: "character", img: "path/to/profile.jpg"}];
* const created = await Actor.createDocuments(data);
* ```
*
* @example Create multiple Documents
* ```js
* const data = [{name: "Tim", type: "npc"], [{name: "Tom", type: "npc"}];
* const created = await Actor.createDocuments(data);
* ```
*
* @example Create multiple embedded Documents within a parent
* ```js
* const actor = game.actors.getName("Tim");
* const data = [{name: "Sword", type: "weapon"}, {name: "Breastplate", type: "equipment"}];
* const created = await Item.createDocuments(data, {parent: actor});
* ```
*
* @example Create a Document within a Compendium pack
* ```js
* const data = [{name: "Compendium Actor", type: "character", img: "path/to/profile.jpg"}];
* const created = await Actor.createDocuments(data, {pack: "mymodule.mypack"});
* ```
*/
static async createDocuments(data=[], operation={}) {
if ( operation.parent?.pack ) operation.pack = operation.parent.pack;
operation.data = data;
const created = await this.database.create(this.implementation, operation);
/** @deprecated since v12 */
if ( getDefiningClass(this, "_onCreateDocuments") !== Document ) {
foundry.utils.logCompatibilityWarning("The Document._onCreateDocuments static method is deprecated in favor of "
+ "Document._onCreateOperation", {since: 12, until: 14});
await this._onCreateDocuments(created, operation);
}
return created;
}
/* -------------------------------------------- */
/**
* Update multiple Document instances using provided differential data.
* Data is provided as an array of objects where each individual object updates one existing Document.
*
* @param {object[]} updates An array of differential data objects, each used to update a single Document
* @param {Partial<Omit<DatabaseUpdateOperation, "updates">>} [operation={}] Parameters of the database update
* operation
* @return {Promise<Document[]>} An array of updated Document instances
*
* @example Update a single Document
* ```js
* const updates = [{_id: "12ekjf43kj2312ds", name: "Timothy"}];
* const updated = await Actor.updateDocuments(updates);
* ```
*
* @example Update multiple Documents
* ```js
* const updates = [{_id: "12ekjf43kj2312ds", name: "Timothy"}, {_id: "kj549dk48k34jk34", name: "Thomas"}]};
* const updated = await Actor.updateDocuments(updates);
* ```
*
* @example Update multiple embedded Documents within a parent
* ```js
* const actor = game.actors.getName("Timothy");
* const updates = [{_id: sword.id, name: "Magic Sword"}, {_id: shield.id, name: "Magic Shield"}];
* const updated = await Item.updateDocuments(updates, {parent: actor});
* ```
*
* @example Update Documents within a Compendium pack
* ```js
* const actor = await pack.getDocument(documentId);
* const updated = await Actor.updateDocuments([{_id: actor.id, name: "New Name"}], {pack: "mymodule.mypack"});
* ```
*/
static async updateDocuments(updates=[], operation={}) {
if ( operation.parent?.pack ) operation.pack = operation.parent.pack;
operation.updates = updates;
const updated = await this.database.update(this.implementation, operation);
/** @deprecated since v12 */
if ( getDefiningClass(this, "_onUpdateDocuments") !== Document ) {
foundry.utils.logCompatibilityWarning("The Document._onUpdateDocuments static method is deprecated in favor of "
+ "Document._onUpdateOperation", {since: 12, until: 14});
await this._onUpdateDocuments(updated, operation);
}
return updated;
}
/* -------------------------------------------- */
/**
* Delete one or multiple existing Documents using an array of provided ids.
* Data is provided as an array of string ids for the documents to delete.
*
* @param {string[]} ids An array of string ids for the documents to be deleted
* @param {Partial<Omit<DatabaseDeleteOperation, "ids">>} [operation={}] Parameters of the database deletion
* operation
* @return {Promise<Document[]>} An array of deleted Document instances
*
* @example Delete a single Document
* ```js
* const tim = game.actors.getName("Tim");
* const deleted = await Actor.deleteDocuments([tim.id]);
* ```
*
* @example Delete multiple Documents
* ```js
* const tim = game.actors.getName("Tim");
* const tom = game.actors.getName("Tom");
* const deleted = await Actor.deleteDocuments([tim.id, tom.id]);
* ```
*
* @example Delete multiple embedded Documents within a parent
* ```js
* const tim = game.actors.getName("Tim");
* const sword = tim.items.getName("Sword");
* const shield = tim.items.getName("Shield");
* const deleted = await Item.deleteDocuments([sword.id, shield.id], parent: actor});
* ```
*
* @example Delete Documents within a Compendium pack
* ```js
* const actor = await pack.getDocument(documentId);
* const deleted = await Actor.deleteDocuments([actor.id], {pack: "mymodule.mypack"});
* ```
*/
static async deleteDocuments(ids=[], operation={}) {
if ( operation.parent?.pack ) operation.pack = operation.parent.pack;
operation.ids = ids;
const deleted = await this.database.delete(this.implementation, operation);
/** @deprecated since v12 */
if ( getDefiningClass(this, "_onDeleteDocuments") !== Document ) {
foundry.utils.logCompatibilityWarning("The Document._onDeleteDocuments static method is deprecated in favor of "
+ "Document._onDeleteOperation", {since: 12, until: 14});
await this._onDeleteDocuments(deleted, operation);
}
return deleted;
}
/* -------------------------------------------- */
/**
* Create a new Document using provided input data, saving it to the database.
* @see Document.createDocuments
* @param {object|Document|(object|Document)[]} [data={}] Initial data used to create this Document, or a Document
* instance to persist.
* @param {Partial<Omit<DatabaseCreateOperation, "data">>} [operation={}] Parameters of the creation operation
* @returns {Promise<Document | Document[] | undefined>} The created Document instance
*
* @example Create a World-level Item
* ```js
* const data = [{name: "Special Sword", type: "weapon"}];
* const created = await Item.create(data);
* ```
*
* @example Create an Actor-owned Item
* ```js
* const data = [{name: "Special Sword", type: "weapon"}];
* const actor = game.actors.getName("My Hero");
* const created = await Item.create(data, {parent: actor});
* ```
*
* @example Create an Item in a Compendium pack
* ```js
* const data = [{name: "Special Sword", type: "weapon"}];
* const created = await Item.create(data, {pack: "mymodule.mypack"});
* ```
*/
static async create(data, operation={}) {
const createData = data instanceof Array ? data : [data];
const created = await this.createDocuments(createData, operation);
return data instanceof Array ? created : created.shift();
}
/* -------------------------------------------- */
/**
* Update this Document using incremental data, saving it to the database.
* @see Document.updateDocuments
* @param {object} [data={}] Differential update data which modifies the existing values of this document
* @param {Partial<Omit<DatabaseUpdateOperation, "updates">>} [operation={}] Parameters of the update operation
* @returns {Promise<Document>} The updated Document instance
*/
async update(data={}, operation={}) {
data._id = this.id;
operation.parent = this.parent;
operation.pack = this.pack;
const updates = await this.constructor.updateDocuments([data], operation);
return updates.shift();
}
/* -------------------------------------------- */
/**
* Delete this Document, removing it from the database.
* @see Document.deleteDocuments
* @param {Partial<Omit<DatabaseDeleteOperation, "ids">>} [operation={}] Parameters of the deletion operation
* @returns {Promise<Document>} The deleted Document instance
*/
async delete(operation={}) {
operation.parent = this.parent;
operation.pack = this.pack;
const deleted = await this.constructor.deleteDocuments([this.id], operation);
return deleted.shift();
}
/* -------------------------------------------- */
/**
* Get a World-level Document of this type by its id.
* @param {string} documentId The Document ID
* @param {DatabaseGetOperation} [operation={}] Parameters of the get operation
* @returns {abstract.Document|null} The retrieved Document, or null
*/
static get(documentId, operation={}) {
if ( !documentId ) return null;
if ( operation.pack ) {
const pack = game.packs.get(operation.pack);
return pack?.index.get(documentId) || null;
}
else {
const collection = game.collections?.get(this.documentName);
return collection?.get(documentId) || null;
}
}
/* -------------------------------------------- */
/* Embedded Operations */
/* -------------------------------------------- */
/**
* A compatibility method that returns the appropriate name of an embedded collection within this Document.
* @param {string} name An existing collection name or a document name.
* @returns {string|null} The provided collection name if it exists, the first available collection for the
* document name provided, or null if no appropriate embedded collection could be found.
* @example Passing an existing collection name.
* ```js
* Actor.getCollectionName("items");
* // returns "items"
* ```
*
* @example Passing a document name.
* ```js
* Actor.getCollectionName("Item");
* // returns "items"
* ```
*/
static getCollectionName(name) {
if ( name in this.hierarchy ) return name;
for ( const [collectionName, field] of Object.entries(this.hierarchy) ) {
if ( field.model.documentName === name ) return collectionName;
}
return null;
}
/* -------------------------------------------- */
/**
* Obtain a reference to the Array of source data within the data object for a certain embedded Document name
* @param {string} embeddedName The name of the embedded Document type
* @return {DocumentCollection} The Collection instance of embedded Documents of the requested type
*/
getEmbeddedCollection(embeddedName) {
const collectionName = this.constructor.getCollectionName(embeddedName);
if ( !collectionName ) {
throw new Error(`${embeddedName} is not a valid embedded Document within the ${this.documentName} Document`);
}
const field = this.constructor.hierarchy[collectionName];
return field.getCollection(this);
}
/* -------------------------------------------- */
/**
* Get an embedded document by its id from a named collection in the parent document.
* @param {string} embeddedName The name of the embedded Document type
* @param {string} id The id of the child document to retrieve
* @param {object} [options] Additional options which modify how embedded documents are retrieved
* @param {boolean} [options.strict=false] Throw an Error if the requested id does not exist. See Collection#get
* @param {boolean} [options.invalid=false] Allow retrieving an invalid Embedded Document.
* @return {Document} The retrieved embedded Document instance, or undefined
* @throws If the embedded collection does not exist, or if strict is true and the Embedded Document could not be
* found.
*/
getEmbeddedDocument(embeddedName, id, {invalid=false, strict=false}={}) {
const collection = this.getEmbeddedCollection(embeddedName);
return collection.get(id, {invalid, strict});
}
/* -------------------------------------------- */
/**
* Create multiple embedded Document instances within this parent Document using provided input data.
* @see Document.createDocuments
* @param {string} embeddedName The name of the embedded Document type
* @param {object[]} data An array of data objects used to create multiple documents
* @param {DatabaseCreateOperation} [operation={}] Parameters of the database creation workflow
* @return {Promise<Document[]>} An array of created Document instances
*/
async createEmbeddedDocuments(embeddedName, data=[], operation={}) {
this.getEmbeddedCollection(embeddedName); // Validation only
operation.parent = this;
operation.pack = this.pack;
const cls = getDocumentClass(embeddedName);
return cls.createDocuments(data, operation);
}
/* -------------------------------------------- */
/**
* Update multiple embedded Document instances within a parent Document using provided differential data.
* @see Document.updateDocuments
* @param {string} embeddedName The name of the embedded Document type
* @param {object[]} updates An array of differential data objects, each used to update a
* single Document
* @param {DatabaseUpdateOperation} [operation={}] Parameters of the database update workflow
* @return {Promise<Document[]>} An array of updated Document instances
*/
async updateEmbeddedDocuments(embeddedName, updates=[], operation={}) {
this.getEmbeddedCollection(embeddedName); // Validation only
operation.parent = this;
operation.pack = this.pack;
const cls = getDocumentClass(embeddedName);
return cls.updateDocuments(updates, operation);
}
/* -------------------------------------------- */
/**
* Delete multiple embedded Document instances within a parent Document using provided string ids.
* @see Document.deleteDocuments
* @param {string} embeddedName The name of the embedded Document type
* @param {string[]} ids An array of string ids for each Document to be deleted
* @param {DatabaseDeleteOperation} [operation={}] Parameters of the database deletion workflow
* @return {Promise<Document[]>} An array of deleted Document instances
*/
async deleteEmbeddedDocuments(embeddedName, ids, operation={}) {
this.getEmbeddedCollection(embeddedName); // Validation only
operation.parent = this;
operation.pack = this.pack;
const cls = getDocumentClass(embeddedName);
return cls.deleteDocuments(ids, operation);
}
/* -------------------------------------------- */
/**
* Iterate over all embedded Documents that are hierarchical children of this Document.
* @param {string} [_parentPath] A parent field path already traversed
* @returns {Generator<[string, Document]>}
*/
* traverseEmbeddedDocuments(_parentPath) {
for ( const [fieldName, field] of Object.entries(this.constructor.hierarchy) ) {
let fieldPath = _parentPath ? `${_parentPath}.${fieldName}` : fieldName;
// Singleton embedded document
if ( field instanceof foundry.data.fields.EmbeddedDocumentField ) {
const document = this[fieldName];
if ( document ) {
yield [fieldPath, document];
yield* document.traverseEmbeddedDocuments(fieldPath);
}
}
// Embedded document collection
else if ( field instanceof foundry.data.fields.EmbeddedCollectionField ) {
const collection = this[fieldName];
const isDelta = field instanceof foundry.data.fields.EmbeddedCollectionDeltaField;
for ( const document of collection.values() ) {
if ( isDelta && !collection.manages(document.id) ) continue;
yield [fieldPath, document];
yield* document.traverseEmbeddedDocuments(fieldPath);
}
}
}
}
/* -------------------------------------------- */
/* Flag Operations */
/* -------------------------------------------- */
/**
* Get the value of a "flag" for this document
* See the setFlag method for more details on flags
*
* @param {string} scope The flag scope which namespaces the key
* @param {string} key The flag key
* @return {*} The flag value
*/
getFlag(scope, key) {
const scopes = this.constructor.database.getFlagScopes();
if ( !scopes.includes(scope) ) throw new Error(`Flag scope "${scope}" is not valid or not currently active`);
/** @deprecated since v12 */
if ( (scope === "core") && (key === "sourceId") ) {
foundry.utils.logCompatibilityWarning("The core.sourceId flag has been deprecated. "
+ "Please use the _stats.compendiumSource property instead.", { since: 12, until: 14 });
return this._stats?.compendiumSource;
}
if ( !this.flags || !(scope in this.flags) ) return undefined;
return getProperty(this.flags?.[scope], key);
}
/* -------------------------------------------- */
/**
* Assign a "flag" to this document.
* Flags represent key-value type data which can be used to store flexible or arbitrary data required by either
* the core software, game systems, or user-created modules.
*
* Each flag should be set using a scope which provides a namespace for the flag to help prevent collisions.
*
* Flags set by the core software use the "core" scope.
* Flags set by game systems or modules should use the canonical name attribute for the module
* Flags set by an individual world should "world" as the scope.
*
* Flag values can assume almost any data type. Setting a flag value to null will delete that flag.
*
* @param {string} scope The flag scope which namespaces the key
* @param {string} key The flag key
* @param {*} value The flag value
* @return {Promise<Document>} A Promise resolving to the updated document
*/
async setFlag(scope, key, value) {
const scopes = this.constructor.database.getFlagScopes();
if ( !scopes.includes(scope) ) throw new Error(`Flag scope "${scope}" is not valid or not currently active`);
return this.update({
flags: {
[scope]: {
[key]: value
}
}
});
}
/* -------------------------------------------- */
/**
* Remove a flag assigned to the document
* @param {string} scope The flag scope which namespaces the key
* @param {string} key The flag key
* @return {Promise<Document>} The updated document instance
*/
async unsetFlag(scope, key) {
const scopes = this.constructor.database.getFlagScopes();
if ( !scopes.includes(scope) ) throw new Error(`Flag scope "${scope}" is not valid or not currently active`);
const head = key.split(".");
const tail = `-=${head.pop()}`;
key = ["flags", scope, ...head, tail].join(".");
return this.update({[key]: null});
}
/* -------------------------------------------- */
/* Database Creation Operations */
/* -------------------------------------------- */
/**
* Pre-process a creation operation for a single Document instance. Pre-operation events only occur for the client
* which requested the operation.
*
* Modifications to the pending Document instance must be performed using {@link Document#updateSource}.
*
* @param {object} data The initial data object provided to the document creation request
* @param {object} options Additional options which modify the creation request
* @param {documents.BaseUser} user The User requesting the document creation
* @returns {Promise<boolean|void>} Return false to exclude this Document from the creation operation
* @internal
*/
async _preCreate(data, options, user) {}
/**
* Post-process a creation operation for a single Document instance. Post-operation events occur for all connected
* clients.
*
* @param {object} data The initial data object provided to the document creation request
* @param {object} options Additional options which modify the creation request
* @param {string} userId The id of the User requesting the document update
* @internal
*/
_onCreate(data, options, userId) {}
/**
* Pre-process a creation operation, potentially altering its instructions or input data. Pre-operation events only
* occur for the client which requested the operation.
*
* This batch-wise workflow occurs after individual {@link Document#_preCreate} workflows and provides a final
* pre-flight check before a database operation occurs.
*
* Modifications to pending documents must mutate the documents array or alter individual document instances using
* {@link Document#updateSource}.
*
* @param {Document[]} documents Pending document instances to be created
* @param {DatabaseCreateOperation} operation Parameters of the database creation operation
* @param {documents.BaseUser} user The User requesting the creation operation
* @returns {Promise<boolean|void>} Return false to cancel the creation operation entirely
* @internal
*/
static async _preCreateOperation(documents, operation, user) {}
/**
* Post-process a creation operation, reacting to database changes which have occurred. Post-operation events occur
* for all connected clients.
*
* This batch-wise workflow occurs after individual {@link Document#_onCreate} workflows.
*
* @param {Document[]} documents The Document instances which were created
* @param {DatabaseCreateOperation} operation Parameters of the database creation operation
* @param {documents.BaseUser} user The User who performed the creation operation
* @returns {Promise<void>}
* @internal
*/
static async _onCreateOperation(documents, operation, user) {}
/* -------------------------------------------- */
/* Database Update Operations */
/* -------------------------------------------- */
/**
* Pre-process an update operation for a single Document instance. Pre-operation events only occur for the client
* which requested the operation.
*
* @param {object} changes The candidate changes to the Document
* @param {object} options Additional options which modify the update request
* @param {documents.BaseUser} user The User requesting the document update
* @returns {Promise<boolean|void>} A return value of false indicates the update operation should be cancelled.
* @internal
*/
async _preUpdate(changes, options, user) {}
/**
* Post-process an update operation for a single Document instance. Post-operation events occur for all connected
* clients.
*
* @param {object} changed The differential data that was changed relative to the documents prior values
* @param {object} options Additional options which modify the update request
* @param {string} userId The id of the User requesting the document update
* @internal
*/
_onUpdate(changed, options, userId) {}
/**
* Pre-process an update operation, potentially altering its instructions or input data. Pre-operation events only
* occur for the client which requested the operation.
*
* This batch-wise workflow occurs after individual {@link Document#_preUpdate} workflows and provides a final
* pre-flight check before a database operation occurs.
*
* Modifications to the requested updates are performed by mutating the data array of the operation.
* {@link Document#updateSource}.
*
* @param {Document[]} documents Document instances to be updated
* @param {DatabaseUpdateOperation} operation Parameters of the database update operation
* @param {documents.BaseUser} user The User requesting the update operation
* @returns {Promise<boolean|void>} Return false to cancel the update operation entirely
* @internal
*/
static async _preUpdateOperation(documents, operation, user) {}
/**
* Post-process an update operation, reacting to database changes which have occurred. Post-operation events occur
* for all connected clients.
*
* This batch-wise workflow occurs after individual {@link Document#_onUpdate} workflows.
*
* @param {Document[]} documents The Document instances which were updated
* @param {DatabaseUpdateOperation} operation Parameters of the database update operation
* @param {documents.BaseUser} user The User who performed the update operation
* @returns {Promise<void>}
* @internal
*/
static async _onUpdateOperation(documents, operation, user) {}
/* -------------------------------------------- */
/* Database Delete Operations */
/* -------------------------------------------- */
/**
* Pre-process a deletion operation for a single Document instance. Pre-operation events only occur for the client
* which requested the operation.
*
* @param {object} options Additional options which modify the deletion request
* @param {documents.BaseUser} user The User requesting the document deletion
* @returns {Promise<boolean|void>} A return value of false indicates the deletion operation should be cancelled.
* @internal
*/
async _preDelete(options, user) {}
/**
* Post-process a deletion operation for a single Document instance. Post-operation events occur for all connected
* clients.
*
* @param {object} options Additional options which modify the deletion request
* @param {string} userId The id of the User requesting the document update
* @internal
*/
_onDelete(options, userId) {}
/**
* Pre-process a deletion operation, potentially altering its instructions or input data. Pre-operation events only
* occur for the client which requested the operation.
*
* This batch-wise workflow occurs after individual {@link Document#_preDelete} workflows and provides a final
* pre-flight check before a database operation occurs.
*
* Modifications to the requested deletions are performed by mutating the operation object.
* {@link Document#updateSource}.
*
* @param {Document[]} documents Document instances to be deleted
* @param {DatabaseDeleteOperation} operation Parameters of the database update operation
* @param {documents.BaseUser} user The User requesting the deletion operation
* @returns {Promise<boolean|void>} Return false to cancel the deletion operation entirely
* @internal
*/
static async _preDeleteOperation(documents, operation, user) {}
/**
* Post-process a deletion operation, reacting to database changes which have occurred. Post-operation events occur
* for all connected clients.
*
* This batch-wise workflow occurs after individual {@link Document#_onDelete} workflows.
*
* @param {Document[]} documents The Document instances which were deleted
* @param {DatabaseDeleteOperation} operation Parameters of the database deletion operation
* @param {documents.BaseUser} user The User who performed the deletion operation
* @returns {Promise<void>}
* @internal
*/
static async _onDeleteOperation(documents, operation, user) {}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v10
* @ignore
*/
get data() {
if ( this.constructor.schema.has("system") ) {
throw new Error(`You are accessing the ${this.constructor.name} "data" field of which was deprecated in v10 and `
+ `replaced with "system". Continued usage of pre-v10 ".data" paths is no longer supported"`);
}
}
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
static get hasSystemData() {
foundry.utils.logCompatibilityWarning(`You are accessing ${this.name}.hasSystemData which is deprecated. `
+ `Please use ${this.name}.hasTypeData instead.`, {since: 11, until: 13});
return this.hasTypeData;
}
/* ---------------------------------------- */
/**
* A reusable helper for adding migration shims.
* @protected
* @ignore
*/
static _addDataFieldShims(data, shims, options) {
for ( const [oldKey, newKey] of Object.entries(shims) ) {
this._addDataFieldShim(data, oldKey, newKey, options);
}
}
/* ---------------------------------------- */
/**
* A reusable helper for adding a migration shim
* @protected
* @ignore
*/
static _addDataFieldShim(data, oldKey, newKey, options={}) {
if ( data.hasOwnProperty(oldKey) ) return;
Object.defineProperty(data, oldKey, {
get: () => {
if ( options.warning ) logCompatibilityWarning(options.warning);
else this._logDataFieldMigration(oldKey, newKey, options);
return ("value" in options) ? options.value : getProperty(data, newKey);
},
set: value => {
if ( newKey ) setProperty(data, newKey, value);
},
configurable: true,
enumerable: false
});
}
/* ---------------------------------------- */
/**
* Define a simple migration from one field name to another.
* The value of the data can be transformed during the migration by an optional application function.
* @param {object} data The data object being migrated
* @param {string} oldKey The old field name
* @param {string} newKey The new field name
* @param {function(data: object): any} [apply] An application function, otherwise the old value is applied
* @returns {boolean} Whether a migration was applied.
* @internal
*/
static _addDataFieldMigration(data, oldKey, newKey, apply) {
if ( !hasProperty(data, newKey) && hasProperty(data, oldKey) ) {
const prop = Object.getOwnPropertyDescriptor(data, oldKey);
if ( prop && !prop.writable ) return false;
setProperty(data, newKey, apply ? apply(data) : getProperty(data, oldKey));
delete data[oldKey];
return true;
}
return false;
}
/* ---------------------------------------- */
/** @protected */
static _logDataFieldMigration(oldKey, newKey, options={}) {
const msg = `You are accessing ${this.name}#${oldKey} which has been migrated to ${this.name}#${newKey}`;
return logCompatibilityWarning(msg, {...options})
}
/* ---------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static async _onCreateDocuments(documents, operation) {}
/**
* @deprecated since v12
* @ignore
*/
static async _onUpdateDocuments(documents, operation) {}
/**
* @deprecated since v12
* @ignore
*/
static async _onDeleteDocuments(documents, operation) {}
}