stat-tracker/module/utils/databases/Database.mjs

200 lines
5.6 KiB
JavaScript

/* eslint-disable no-unused-vars */
import { PrivacyMode } from "../privacy.mjs";
import { validateBucketConfig } from "../buckets.mjs";
/*
NOTE:
This database design currently does not support anything like a default subtable
or nested tables more than 1-layer deep. These limitations are currently intentional
and if the desire for those functionalities is requested, this is some information
on how they can be implemented.
Tables >= 1 layer deep:
Adding a "parent" property to each table that accepts a table's ID, there will
need to be a defined table at the specified ID, and each table's ID should no
longer contain the parent table(s) within it (e.g. "Dice/d4" -> "d4"). This
could also be made in such a way where any buckets/graph settings on the parent
are applied to the subtable when it is created. The primary unknown with this
idea is what do we do when someone has a table like "Dice/d4" and then attempts
to make a subtable "Dice/d4/crits" or something like that.
Default Subtables:
This would require a partial implementation similar to the Tables >= 1 layer
deep, however each table would accept an "options" top level property that accepts
a "defaultSubtable" property specifying the ID of the subtable that should be
selected by default, this defaultSubtable property would *only* be valid on
tables that are parents to other tables.
*/
const { deleteProperty, diffObject, expandObject, mergeObject } = foundry.utils;
export class Database {
// MARK: Table Ops
static async createTable(tableConfig) {
if (!game.user.isGM) {
ui.notifications.error(`You do not have the required permission to create a new table`);
return false;
};
const name = tableConfig.name;
if (name.split(`/`).length > 2) {
ui.notifications.error(`Subtables are not able to have subtables`);
return false;
};
const tables = game.settings.get(__ID__, `tables`);
const [ table, subtable ] = name.split(`/`);
if (subtable && tables[table]) {
ui.notifications.error(`Cannot add subtable for a table that already exists`);
return false;
};
if (tables[name]) {
ui.notifications.error(`Cannot create table that already exists`);
return false;
};
tables[name] = tableConfig;
game.settings.set(__ID__, `tables`, tables);
this.render({ tags: [`table`] });
return true;
};
/** @returns {Array<Table>} */
static async getTables() {
const tables = game.settings.get(__ID__, `tables`);
return Object.values(tables) ?? [];
};
static async getTable(tableID) {
const tables = game.settings.get(__ID__, `tables`);
if (!tables[tableID]) { return };
return tables[tableID];
};
static async updateTable(tableID, changes) {
const table = this.getTable(tableID);
if (!tables[tableID]) {
ui.notifications.error(`Cannot update table that doesn't exist`);
return false;
};
// Bucket coercion in case called via the API
deleteProperty(changes, `name`);
deleteProperty(changes, `buckets.type`);
const diff = diffObject(
table,
expandObject(changes),
{ inner: true, deletionKeys: true },
);
if (Object.keys(diff).length === 0) { return false };
const updated = mergeObject(
table,
diff,
{ inplace: false, performDeletions: true },
);
try {
updated.buckets = validateBucketConfig(updated.buckets);
} catch (e) {
ui.notifications.error(e);
return false;
};
const tables = game.settings.get(__ID__, `tables`);
tables[tableID] = updated;
game.settings.set(__ID__, `tables`, tables);
this.render({ tags: [`table`] });
return true;
};
static async deleteTable(tableID) {
if (!game.user.isGM) {
ui.notifications.error(`You do not have the required permission to delete a table`);
return false;
};
const tables = game.settings.get(__ID__, `tables`);
if (!tables[tableID]) {
ui.notifications.error(`Cannot delete a table that doesn't exist`);
return false;
};
delete tables[tableID];
game.settings.set(__ID__, `tables`, tables);
return true;
};
// MARK: Row Ops
static async createRow(tableID, userID, row, opts) {
throw new Error(`createRow() must be implemented`);
};
static async createRows(tableID, userID, rows, opts) {
throw new Error(`createRows() must be implemented`);
};
static async getRows(tableID, userIDs, privacy = [PrivacyMode.PUBLIC]) {
throw new Error(`getRows() must be implemented`);
};
static async updateRow(tableID, userID, rowID, changes) {
throw new Error(`updateRow() must be implemented`);
};
static async deleteRow(tableID, userID, rowID) {
throw new Error(`deleteRow() must be implemented`);
};
// MARK: Applications
static _apps = new Map();
/**
* Adds an application into the registry so that when a data update
* is received, we can re-render the sheets.
*
* @param app an ApplicationV2 instance
*/
static addApp(app) {
this._apps.set(app.id, app);
this.registerListeners();
};
/**
* Adds an application into the registry so that when a data update
* is received, we can re-render the sheets.
*
* @param app an ApplicationV2 instance
*/
static removeApp(app) {
this._apps.delete(app.id);
if (this._apps.size === 0) {
this.unregisterListeners();
};
};
/**
* Rerenders all of the applications that are displaying data from
* this database
*/
static async render(opts) {
for (const app of this._apps.values()) {
app.render(foundry.utils.deepClone(opts));
};
};
// MARK: Listeners
/**
* Used to listen for changes from other clients and rerender the apps
* as required in order to keep the data as up-to-date as possible.
*/
static async registerListeners() {};
static async triggerListeners() {};
static async unregisterListeners() {};
};
/* eslint-enable no-unused-vars */