319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
|
|
import Document from "./document.mjs";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {import("./_types.mjs").DatabaseGetOperation} DatabaseGetOperation
|
||
|
|
* @typedef {import("./_types.mjs").DatabaseCreateOperation} DatabaseCreateOperation
|
||
|
|
* @typedef {import("./_types.mjs").DatabaseUpdateOperation} DatabaseUpdateOperation
|
||
|
|
* @typedef {import("./_types.mjs").DatabaseDeleteOperation} DatabaseDeleteOperation
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An abstract base class extended on both the client and server which defines how Documents are retrieved, created,
|
||
|
|
* updated, and deleted.
|
||
|
|
* @alias foundry.abstract.DatabaseBackend
|
||
|
|
* @abstract
|
||
|
|
*/
|
||
|
|
export default class DatabaseBackend {
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Get Operations */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Retrieve Documents based on provided query parameters.
|
||
|
|
* It recommended to use CompendiumCollection#getDocuments or CompendiumCollection#getIndex rather
|
||
|
|
* than calling this method directly.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseGetOperation} operation Parameters of the get operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]|object[]>} An array of retrieved Document instances or index objects
|
||
|
|
*/
|
||
|
|
async get(documentClass, operation, user) {
|
||
|
|
operation = await this.#configureGet(operation);
|
||
|
|
return this._getDocuments(documentClass, operation, user);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate and configure the parameters of the get operation.
|
||
|
|
* @param {DatabaseGetOperation} operation The requested operation
|
||
|
|
*/
|
||
|
|
async #configureGet(operation) {
|
||
|
|
await this.#configureOperation(operation);
|
||
|
|
operation.broadcast = false; // Get requests are never broadcast
|
||
|
|
return operation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Retrieve Document instances using the specified operation parameters.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseGetOperation} operation Parameters of the get operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]|object[]>} An array of retrieved Document instances or index objects
|
||
|
|
* @abstract
|
||
|
|
* @internal
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
async _getDocuments(documentClass, operation, user) {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Create Operations */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create new Documents using provided data and context.
|
||
|
|
* It is recommended to use {@link Document.createDocuments} or {@link Document.create} rather than calling this
|
||
|
|
* method directly.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseCreateOperation} operation Parameters of the create operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]>} An array of created Document instances
|
||
|
|
*/
|
||
|
|
async create(documentClass, operation, user) {
|
||
|
|
operation = await this.#configureCreate(operation);
|
||
|
|
return this._createDocuments(documentClass, operation, user);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate and configure the parameters of the create operation.
|
||
|
|
* @param {DatabaseCreateOperation} operation The requested operation
|
||
|
|
*/
|
||
|
|
async #configureCreate(operation) {
|
||
|
|
if ( !Array.isArray(operation.data) ) {
|
||
|
|
throw new Error("The data provided to the DatabaseBackend#create operation must be an array of data objects");
|
||
|
|
}
|
||
|
|
await this.#configureOperation(operation);
|
||
|
|
operation.render ??= true;
|
||
|
|
operation.renderSheet ??= false;
|
||
|
|
return operation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create Document instances using provided data and operation parameters.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseCreateOperation} operation Parameters of the create operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]>} An array of created Document instances
|
||
|
|
* @abstract
|
||
|
|
* @internal
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
async _createDocuments(documentClass, operation, user) {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Update Operations */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update Documents using provided data and context.
|
||
|
|
* It is recommended to use {@link Document.updateDocuments} or {@link Document#update} rather than calling this
|
||
|
|
* method directly.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseUpdateOperation} operation Parameters of the update operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]>} An array of updated Document instances
|
||
|
|
*/
|
||
|
|
async update(documentClass, operation, user) {
|
||
|
|
operation = await this.#configureUpdate(operation);
|
||
|
|
return this._updateDocuments(documentClass, operation, user);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate and configure the parameters of the update operation.
|
||
|
|
* @param {DatabaseUpdateOperation} operation The requested operation
|
||
|
|
*/
|
||
|
|
async #configureUpdate(operation) {
|
||
|
|
if ( !Array.isArray(operation.updates) ) {
|
||
|
|
throw new Error("The updates provided to the DatabaseBackend#update operation must be an array of data objects");
|
||
|
|
}
|
||
|
|
await this.#configureOperation(operation);
|
||
|
|
operation.diff ??= true;
|
||
|
|
operation.recursive ??= true;
|
||
|
|
operation.render ??= true;
|
||
|
|
return operation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update Document instances using provided data and operation parameters.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseUpdateOperation} operation Parameters of the update operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]>} An array of updated Document instances
|
||
|
|
* @abstract
|
||
|
|
* @internal
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
async _updateDocuments(documentClass, operation, user) {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Delete Operations */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete Documents using provided ids and context.
|
||
|
|
* It is recommended to use {@link foundry.abstract.Document.deleteDocuments} or
|
||
|
|
* {@link foundry.abstract.Document#delete} rather than calling this method directly.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseDeleteOperation} operation Parameters of the delete operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]>} An array of deleted Document instances
|
||
|
|
*/
|
||
|
|
async delete(documentClass, operation, user) {
|
||
|
|
operation = await this.#configureDelete(operation);
|
||
|
|
return this._deleteDocuments(documentClass, operation, user);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate and configure the parameters of the delete operation.
|
||
|
|
* @param {DatabaseDeleteOperation} operation The requested operation
|
||
|
|
*/
|
||
|
|
async #configureDelete(operation) {
|
||
|
|
if ( !Array.isArray(operation.ids) ) {
|
||
|
|
throw new Error("The document ids provided to the DatabaseBackend#delete operation must be an array of strings");
|
||
|
|
}
|
||
|
|
await this.#configureOperation(operation);
|
||
|
|
operation.deleteAll ??= false;
|
||
|
|
operation.render ??= true;
|
||
|
|
return operation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete Document instances using provided ids and operation parameters.
|
||
|
|
* @param {typeof Document} documentClass The Document class definition
|
||
|
|
* @param {DatabaseDeleteOperation} operation Parameters of the delete operation
|
||
|
|
* @param {BaseUser} [user] The requesting User
|
||
|
|
* @returns {Promise<Document[]>} An array of deleted Document instances
|
||
|
|
* @abstract
|
||
|
|
* @internal
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
async _deleteDocuments(documentClass, operation, user) {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Helper Methods */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Common database operation configuration steps.
|
||
|
|
* @param {DatabaseOperation} operation The requested operation
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async #configureOperation(operation) {
|
||
|
|
if ( operation.pack && !this.getCompendiumScopes().includes(operation.pack) ) {
|
||
|
|
throw new Error(`Compendium pack "${operation.pack}" is not a valid Compendium identifier`);
|
||
|
|
}
|
||
|
|
operation.parent = await this._getParent(operation);
|
||
|
|
operation.modifiedTime = Date.now();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the parent Document (if any) associated with a request context.
|
||
|
|
* @param {DatabaseOperation} operation The requested database operation
|
||
|
|
* @return {Promise<Document|null>} The parent Document, or null
|
||
|
|
* @internal
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
async _getParent(operation) {
|
||
|
|
if ( operation.parent && !(operation.parent instanceof Document) ) {
|
||
|
|
throw new Error("A parent Document provided to the database operation must be a Document instance");
|
||
|
|
}
|
||
|
|
else if ( operation.parent ) return operation.parent;
|
||
|
|
if ( operation.parentUuid ) return globalThis.fromUuid(operation.parentUuid, {invalid: true});
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Describe the scopes which are suitable as the namespace for a flag key
|
||
|
|
* @returns {string[]}
|
||
|
|
*/
|
||
|
|
getFlagScopes() {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Describe the scopes which are suitable as the namespace for a flag key
|
||
|
|
* @returns {string[]}
|
||
|
|
*/
|
||
|
|
getCompendiumScopes() {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log a database operations message.
|
||
|
|
* @param {string} level The logging level
|
||
|
|
* @param {string} message The message
|
||
|
|
* @abstract
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_log(level, message) {}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log a database operation for an embedded document, capturing the action taken and relevant IDs
|
||
|
|
* @param {string} action The action performed
|
||
|
|
* @param {string} type The document type
|
||
|
|
* @param {abstract.Document[]} documents The documents modified
|
||
|
|
* @param {string} [level=info] The logging level
|
||
|
|
* @param {abstract.Document} [parent] A parent document
|
||
|
|
* @param {string} [pack] A compendium pack within which the operation occurred
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_logOperation(action, type, documents, {parent, pack, level="info"}={}) {
|
||
|
|
let msg = (documents.length === 1) ? `${action} ${type}` : `${action} ${documents.length} ${type} documents`;
|
||
|
|
if (documents.length === 1) msg += ` with id [${documents[0].id}]`;
|
||
|
|
else if (documents.length <= 5) msg += ` with ids: [${documents.map(d => d.id)}]`;
|
||
|
|
msg += this.#logContext(parent, pack);
|
||
|
|
this._log(level, msg);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Construct a standardized error message given the context of an attempted operation
|
||
|
|
* @returns {string}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_logError(user, action, subject, {parent, pack}={}) {
|
||
|
|
if ( subject instanceof Document ) {
|
||
|
|
subject = subject.id ? `${subject.documentName} [${subject.id}]` : `a new ${subject.documentName}`;
|
||
|
|
}
|
||
|
|
let msg = `User ${user.name} lacks permission to ${action} ${subject}`;
|
||
|
|
return msg + this.#logContext(parent, pack);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine a string suffix for a log message based on the parent and/or compendium context.
|
||
|
|
* @param {Document|null} parent
|
||
|
|
* @param {string|null} pack
|
||
|
|
* @returns {string}
|
||
|
|
*/
|
||
|
|
#logContext(parent, pack) {
|
||
|
|
let context = "";
|
||
|
|
if ( parent ) context += ` in parent ${parent.constructor.metadata.name} [${parent.id}]`;
|
||
|
|
if ( pack ) context += ` in Compendium ${pack}`;
|
||
|
|
return context;
|
||
|
|
}
|
||
|
|
}
|