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

306 lines
11 KiB
JavaScript

import Collection from "../utils/collection.mjs";
import {randomID} from "../utils/helpers.mjs";
/**
* An extension of the Collection.
* Used for the specific task of containing embedded Document instances within a parent Document.
*/
export default class EmbeddedCollection extends Collection {
/**
* @param {string} name The name of this collection in the parent Document.
* @param {DataModel} parent The parent DataModel instance to which this collection belongs.
* @param {object[]} sourceArray The source data array for the collection in the parent Document data.
*/
constructor(name, parent, sourceArray) {
if ( typeof name !== "string" ) throw new Error("The signature of EmbeddedCollection has changed in v11.");
super();
Object.defineProperties(this, {
_source: {value: sourceArray, writable: false},
documentClass: {value: parent.constructor.hierarchy[name].model, writable: false},
name: {value: name, writable: false},
model: {value: parent, writable: false}
});
}
/**
* The Document implementation used to construct instances within this collection.
* @type {typeof foundry.abstract.Document}
*/
documentClass;
/**
* The name of this collection in the parent Document.
* @type {string}
*/
name;
/**
* The parent DataModel to which this EmbeddedCollection instance belongs.
* @type {DataModel}
*/
model;
/**
* Has this embedded collection been initialized as a one-time workflow?
* @type {boolean}
* @protected
*/
_initialized = false;
/**
* The source data array from which the embedded collection is created
* @type {object[]}
* @private
*/
_source;
/**
* Record the set of document ids where the Document was not initialized because of invalid source data
* @type {Set<string>}
*/
invalidDocumentIds = new Set();
/* -------------------------------------------- */
/**
* Instantiate a Document for inclusion in the Collection.
* @param {object} data The Document data.
* @param {DocumentConstructionContext} [context] Document creation context.
* @returns {Document}
*/
createDocument(data, context={}) {
return new this.documentClass(data, {
...context,
parent: this.model,
parentCollection: this.name,
pack: this.model.pack
});
}
/* -------------------------------------------- */
/**
* Initialize the EmbeddedCollection object by constructing its contained Document instances
* @param {DocumentConstructionContext} [options] Initialization options.
*/
initialize(options={}) {
// Repeat initialization
if ( this._initialized ) {
for ( const doc of this ) doc._initialize(options);
return;
}
// First-time initialization
this.clear();
for ( const d of this._source ) this._initializeDocument(d, options);
this._initialized = true;
}
/* -------------------------------------------- */
/**
* Initialize an embedded document and store it in the collection.
* @param {object} data The Document data.
* @param {DocumentConstructionContext} [context] Context to configure Document initialization.
* @protected
*/
_initializeDocument(data, context) {
if ( !data._id ) data._id = randomID(16);
let doc;
try {
doc = this.createDocument(data, context);
super.set(doc.id, doc);
} catch(err) {
this._handleInvalidDocument(data._id, err, context);
}
}
/* -------------------------------------------- */
/**
* Log warnings or errors when a Document is found to be invalid.
* @param {string} id The invalid Document's ID.
* @param {Error} err The validation error.
* @param {object} [options] Options to configure invalid Document handling.
* @param {boolean} [options.strict=true] Whether to throw an error or only log a warning.
* @protected
*/
_handleInvalidDocument(id, err, {strict=true}={}) {
const docName = this.documentClass.documentName;
const parent = this.model;
this.invalidDocumentIds.add(id);
// Wrap the error with more information
const uuid = `${parent.uuid}.${docName}.${id}`;
const msg = `Failed to initialize ${docName} [${uuid}]:\n${err.message}`;
const error = new Error(msg, {cause: err});
if ( strict ) globalThis.logger.error(error);
else globalThis.logger.warn(error);
if ( globalThis.Hooks && strict ) {
Hooks.onError(`${this.constructor.name}#_initializeDocument`, error, {id, documentName: docName});
}
}
/* -------------------------------------------- */
/**
* Get an element from the EmbeddedCollection by its ID.
* @param {string} id The ID of the Embedded Document to retrieve.
* @param {object} [options] Additional options to configure retrieval.
* @param {boolean} [options.strict=false] Throw an Error if the requested Embedded Document does not exist.
* @param {boolean} [options.invalid=false] Allow retrieving an invalid Embedded Document.
* @returns {Document}
* @throws If strict is true and the Embedded Document cannot be found.
*/
get(id, {invalid=false, strict=false}={}) {
let result = super.get(id);
if ( !result && invalid ) result = this.getInvalid(id, { strict: false });
if ( !result && strict ) throw new Error(`${this.constructor.documentName} id [${id}] does not exist in the `
+ `${this.constructor.name} collection.`);
return result;
}
/* ---------------------------------------- */
/**
* Add an item to the collection.
* @param {string} key The embedded Document ID.
* @param {Document} value The embedded Document instance.
* @param {object} [options] Additional options to the set operation.
* @param {boolean} [options.modifySource=true] Whether to modify the collection's source as part of the operation.
* */
set(key, value, {modifySource=true, ...options}={}) {
if ( modifySource ) this._set(key, value, options);
return super.set(key, value);
}
/* -------------------------------------------- */
/**
* Modify the underlying source array to include the Document.
* @param {string} key The Document ID key.
* @param {Document} value The Document.
* @protected
*/
_set(key, value) {
if ( this.has(key) || this.invalidDocumentIds.has(key) ) this._source.findSplice(d => d._id === key, value._source);
else this._source.push(value._source);
}
/* ---------------------------------------- */
/**
* @param {string} key The embedded Document ID.
* @param {object} [options] Additional options to the delete operation.
* @param {boolean} [options.modifySource=true] Whether to modify the collection's source as part of the operation.
* */
delete(key, {modifySource=true, ...options}={}) {
if ( modifySource ) this._delete(key, options);
return super.delete(key);
}
/* -------------------------------------------- */
/**
* Remove the value from the underlying source array.
* @param {string} key The Document ID key.
* @param {object} [options] Additional options to configure deletion behavior.
* @protected
*/
_delete(key, options={}) {
if ( this.has(key) || this.invalidDocumentIds.has(key) ) this._source.findSplice(d => d._id === key);
}
/* ---------------------------------------- */
/**
* Update an EmbeddedCollection using an array of provided document data.
* @param {DataModel[]} changes An array of provided Document data
* @param {object} [options={}] Additional options which modify how the collection is updated
*/
update(changes, options={}) {
const updated = new Set();
// Create or update documents within the collection
for ( let data of changes ) {
if ( !data._id ) data._id = randomID(16);
this._createOrUpdate(data, options);
updated.add(data._id);
}
// If the update was not recursive, remove all non-updated documents
if ( options.recursive === false ) {
for ( const id of this._source.map(d => d._id) ) {
if ( !updated.has(id) ) this.delete(id, options);
}
}
}
/* -------------------------------------------- */
/**
* Create or update an embedded Document in this collection.
* @param {DataModel} data The update delta.
* @param {object} [options={}] Additional options which modify how the collection is updated.
* @protected
*/
_createOrUpdate(data, options) {
const current = this.get(data._id);
if ( current ) current.updateSource(data, options);
else {
const doc = this.createDocument(data);
this.set(doc.id, doc);
}
}
/* ---------------------------------------- */
/**
* Obtain a temporary Document instance for a document id which currently has invalid source data.
* @param {string} id A document ID with invalid source data.
* @param {object} [options] Additional options to configure retrieval.
* @param {boolean} [options.strict=true] Throw an Error if the requested ID is not in the set of invalid IDs for
* this collection.
* @returns {Document} An in-memory instance for the invalid Document
* @throws If strict is true and the requested ID is not in the set of invalid IDs for this collection.
*/
getInvalid(id, {strict=true}={}) {
if ( !this.invalidDocumentIds.has(id) ) {
if ( strict ) throw new Error(`${this.constructor.documentName} id [${id}] is not in the set of invalid ids`);
return;
}
const data = this._source.find(d => d._id === id);
return this.documentClass.fromSource(foundry.utils.deepClone(data), {parent: this.model});
}
/* ---------------------------------------- */
/**
* Convert the EmbeddedCollection to an array of simple objects.
* @param {boolean} [source=true] Draw data for contained Documents from the underlying data source?
* @returns {object[]} The extracted array of primitive objects
*/
toObject(source=true) {
const arr = [];
for ( let doc of this.values() ) {
arr.push(doc.toObject(source));
}
return arr;
}
/* -------------------------------------------- */
/**
* Follow-up actions to take when a database operation modifies Documents in this EmbeddedCollection.
* @param {DatabaseAction} action The database action performed
* @param {foundry.abstract.Document[]} documents The array of modified Documents
* @param {any[]} result The result of the database operation
* @param {DatabaseOperation} operation Database operation details
* @param {foundry.documents.BaseUser} user The User who performed the operation
* @internal
*/
_onModifyContents(action, documents, result, operation, user) {}
}