Initial
This commit is contained in:
305
resources/app/common/abstract/embedded-collection.mjs
Normal file
305
resources/app/common/abstract/embedded-collection.mjs
Normal file
@@ -0,0 +1,305 @@
|
||||
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) {}
|
||||
}
|
||||
Reference in New Issue
Block a user