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} 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} */ 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} */ 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>} */ 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} 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} data An array of data objects or existing Documents to persist. * @param {Partial>} [operation={}] Parameters of the requested creation * operation * @return {Promise} 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>} [operation={}] Parameters of the database update * operation * @return {Promise} 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>} [operation={}] Parameters of the database deletion * operation * @return {Promise} 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>} [operation={}] Parameters of the creation operation * @returns {Promise} 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>} [operation={}] Parameters of the update operation * @returns {Promise} 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>} [operation={}] Parameters of the deletion operation * @returns {Promise} 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} 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} 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} 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} 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} 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} 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} 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} * @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} 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} 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} * @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} 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} 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} * @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) {} }