Add data validation and a world setting for enabling the Global API
This commit is contained in:
parent
c3d632274a
commit
354b22da57
7 changed files with 180 additions and 56 deletions
|
|
@ -10,36 +10,42 @@ import { MemoryDatabase } from "./utils/databases/Memory.mjs";
|
|||
import { UserFlagDatabase } from "./utils/databases/UserFlag.mjs";
|
||||
|
||||
// Utils
|
||||
import { barGraphSchema, numberBucketSchema, rowSchema, stringBucketSchema, tableSchema } from "./utils/databases/model.mjs";
|
||||
import { filterPrivateRows, PrivacyMode } from "./utils/privacy.mjs";
|
||||
import { validateBucketConfig, validateValue } from "./utils/buckets.mjs";
|
||||
|
||||
const { deepFreeze } = foundry.utils;
|
||||
|
||||
Object.defineProperty(
|
||||
globalThis,
|
||||
`stats`,
|
||||
{
|
||||
value: deepFreeze({
|
||||
Apps: {
|
||||
TestApp,
|
||||
StatsViewer,
|
||||
TableCreator,
|
||||
TableManager,
|
||||
},
|
||||
utils: {
|
||||
filterPrivateRows,
|
||||
validateValue,
|
||||
validateBucketConfig,
|
||||
},
|
||||
enums: {
|
||||
PrivacyMode,
|
||||
},
|
||||
databases: {
|
||||
Database,
|
||||
MemoryDatabase,
|
||||
UserFlagDatabase,
|
||||
},
|
||||
}),
|
||||
writable: false,
|
||||
export const api = deepFreeze({
|
||||
Apps: {
|
||||
TestApp,
|
||||
StatsViewer,
|
||||
TableCreator,
|
||||
TableManager,
|
||||
},
|
||||
);
|
||||
utils: {
|
||||
filterPrivateRows,
|
||||
validateValue,
|
||||
validateBucketConfig,
|
||||
},
|
||||
enums: {
|
||||
PrivacyMode,
|
||||
},
|
||||
databases: {
|
||||
Database,
|
||||
MemoryDatabase,
|
||||
UserFlagDatabase,
|
||||
},
|
||||
schemas: {
|
||||
buckets: {
|
||||
range: numberBucketSchema,
|
||||
number: numberBucketSchema,
|
||||
string: stringBucketSchema,
|
||||
},
|
||||
graphs: {
|
||||
bar: barGraphSchema,
|
||||
},
|
||||
table: tableSchema,
|
||||
row: rowSchema,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { TableCreator } from "../Apps/TableCreator.mjs";
|
|||
import { TableManager } from "../Apps/TableManager.mjs";
|
||||
|
||||
// Misc Imports
|
||||
import { api } from "../api.mjs";
|
||||
import helpers from "../handlebarsHelpers/_index.mjs";
|
||||
import { Logger } from "../utils/Logger.mjs";
|
||||
import { registerCustomComponents } from "../Apps/elements/_index.mjs";
|
||||
|
|
@ -45,6 +46,18 @@ Hooks.on(`init`, () => {
|
|||
CONFIG.stats.db = MemoryDatabase;
|
||||
};
|
||||
|
||||
game.modules.get(__ID__).api = api;
|
||||
if (game.settings.get(__ID__, `globalAPI`)) {
|
||||
Object.defineProperty(
|
||||
globalThis,
|
||||
`stats`,
|
||||
{
|
||||
value: api,
|
||||
writable: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
Handlebars.registerHelper(helpers);
|
||||
registerCustomComponents();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
/*
|
||||
World Settings:
|
||||
- Track rolls automatically
|
||||
- Track inactive rolls (e.g. the "lower" in a "kh" roll)
|
||||
- Track self rolls (defaulta false)
|
||||
*/
|
||||
|
||||
export function registerWorldSettings() {
|
||||
game.settings.register(__ID__, `autoTrackRolls`, {
|
||||
name: `Roll Auto-Tracking`,
|
||||
hint: `Whether or not the module should automatically add rolls made in the chat to the database. This is useful if the system you're using has implemented an integration with the module, or if you only want macros to handle the database additions.`,
|
||||
name: `STAT_TRACKER.settings.autoTrackRolls.name`,
|
||||
hint: `STAT_TRACKER.settings.autoTrackRolls.hint`,
|
||||
scope: `world`,
|
||||
type: Boolean,
|
||||
config: true,
|
||||
default: true,
|
||||
requiresReload: true,
|
||||
});
|
||||
|
||||
game.settings.register(__ID__, `globalAPI`, {
|
||||
name: `STAT_TRACKER.settings.globalAPI.name`,
|
||||
hint: `STAT_TRACKER.settings.globalAPI.hint`,
|
||||
scope: `world`,
|
||||
type: Boolean,
|
||||
config: true,
|
||||
default: import.meta.env.DEV,
|
||||
requiresReload: true,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import { BucketTypes, validateBucketConfig } from "../buckets.mjs";
|
||||
import { Logger } from "../Logger.mjs";
|
||||
import { PrivacyMode } from "../privacy.mjs";
|
||||
import { validateBucketConfig } from "../buckets.mjs";
|
||||
import { tableSchema } from "./model.mjs";
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
|
|
@ -28,6 +30,12 @@ Default Subtables:
|
|||
|
||||
const { deleteProperty, diffObject, expandObject, mergeObject } = foundry.utils;
|
||||
|
||||
/**
|
||||
* The generic Database implementation, any subclasses should implement all of
|
||||
* the required methods, optionally overriding the methods provided by this class,
|
||||
* data validation should be used on any and all of the create* methods to ensure
|
||||
* consistency across databases.
|
||||
*/
|
||||
export class Database {
|
||||
// MARK: Table Ops
|
||||
static async createTable(tableConfig) {
|
||||
|
|
@ -36,25 +44,42 @@ export class Database {
|
|||
return false;
|
||||
};
|
||||
|
||||
const name = tableConfig.name;
|
||||
if (name.split(`/`).length > 2) {
|
||||
ui.notifications.error(`Subtables are not able to have subtables`);
|
||||
const { error, value: corrected } = tableSchema.validate(
|
||||
tableConfig,
|
||||
{ abortEarly: false, convert: true, dateFormat: `iso`, render: false },
|
||||
);
|
||||
if (error) {
|
||||
ui.notifications.error(`Table being created did not conform to required schema, see console for more information.`, { console: false });
|
||||
Logger.error(error);
|
||||
return false;
|
||||
};
|
||||
|
||||
const tables = game.settings.get(__ID__, `tables`);
|
||||
const name = tableConfig.name;
|
||||
const [ table, subtable ] = name.split(`/`);
|
||||
|
||||
const tables = game.settings.get(__ID__, `tables`);
|
||||
if (subtable && tables[table]) {
|
||||
ui.notifications.error(`Cannot add subtable for a table that already exists`);
|
||||
return false;
|
||||
};
|
||||
|
||||
if (table === `Dice`) {
|
||||
if (!subtable.match(/^d[0-9]+$/)) {
|
||||
ui.notifications.error(`Cannot create a Dice subtable that doesn't use "dX" as it's subtable name.`);
|
||||
return false;
|
||||
};
|
||||
if (tableConfig.buckets.type === BucketTypes.RANGE) {
|
||||
ui.notifications.error(`Cannot create a Dice subtable with a non-range bucket type`);
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
if (tables[name]) {
|
||||
ui.notifications.error(`Cannot create table that already exists`);
|
||||
return false;
|
||||
};
|
||||
|
||||
tables[name] = tableConfig;
|
||||
tables[name] = corrected;
|
||||
game.settings.set(__ID__, `tables`, tables);
|
||||
this.render({ tags: [`table`] });
|
||||
return true;
|
||||
|
|
@ -99,7 +124,7 @@ export class Database {
|
|||
try {
|
||||
updated.buckets = validateBucketConfig(updated.buckets);
|
||||
} catch (e) {
|
||||
ui.notifications.error(e);
|
||||
Logger.error(e);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { filterPrivateRows, PrivacyMode } from "../privacy.mjs";
|
||||
import { Database } from "./Database.mjs";
|
||||
import { Logger } from "../Logger.mjs";
|
||||
import { rowSchema } from "./model.mjs";
|
||||
|
||||
const { hasProperty, mergeObject, randomID } = foundry.utils;
|
||||
|
||||
|
|
@ -13,12 +14,22 @@ export class UserFlagDatabase extends Database {
|
|||
let user = game.users.get(userID);
|
||||
if (!table || !user) { return };
|
||||
|
||||
row._id ||= randomID();
|
||||
row._id = randomID();
|
||||
row.timestamp = new Date().toISOString();
|
||||
|
||||
const { error, value: corrected } = rowSchema.validate(
|
||||
row,
|
||||
{ abortEarly: false, convert: true, dateFormat: `iso`, render: false },
|
||||
);
|
||||
if (error) {
|
||||
ui.notifications.error(`Row being created did not conform to required schema, see console for more information.`, { console: false });
|
||||
Logger.error(error);
|
||||
return false;
|
||||
};
|
||||
|
||||
const userData = user.getFlag(__ID__, dataFlag);
|
||||
userData[tableID] ??= [];
|
||||
userData[tableID].push(row);
|
||||
userData[tableID].push(corrected);
|
||||
await user.setFlag(__ID__, dataFlag, userData);
|
||||
|
||||
if (rerender) {
|
||||
|
|
@ -36,9 +47,21 @@ export class UserFlagDatabase extends Database {
|
|||
userData[tableID] ??= [];
|
||||
|
||||
for (const row of rows) {
|
||||
row._id ||= randomID();
|
||||
row._id = randomID();
|
||||
row.timestamp = new Date().toISOString();
|
||||
userData[tableID].push(row);
|
||||
|
||||
const { error, value: corrected } = rowSchema.validate(
|
||||
row,
|
||||
{ abortEarly: false, convert: true, dateFormat: `iso`, render: false },
|
||||
);
|
||||
if (error) {
|
||||
ui.notifications.error(`A row being created did not conform to required schema, see console for more information.`, { console: false });
|
||||
Logger.error(`Failing row:`, row);
|
||||
Logger.error(error);
|
||||
continue;
|
||||
};
|
||||
|
||||
userData[tableID].push(corrected);
|
||||
};
|
||||
|
||||
await user.setFlag(__ID__, dataFlag, userData);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as Joi from "joi";
|
||||
import { PrivacyMode } from "../privacy.mjs";
|
||||
|
||||
// MARK: Buckets
|
||||
const numberBucketSchema = Joi.object({
|
||||
export const numberBucketSchema = Joi.object({
|
||||
type: Joi.string().valid(`number`, `range`).required(),
|
||||
min: Joi
|
||||
.number()
|
||||
|
|
@ -29,15 +30,18 @@ const numberBucketSchema = Joi.object({
|
|||
}),
|
||||
});
|
||||
|
||||
const stringBucketSchema = Joi.object({
|
||||
export const stringBucketSchema = Joi.object({
|
||||
type: Joi.string().valid(`string`).required(),
|
||||
choices: Joi.array(Joi.string()).optional(),
|
||||
choices: Joi.array().items(Joi.string().trim().invalid(``)).optional(),
|
||||
});
|
||||
|
||||
// MARK: Graphs
|
||||
const barGraphSchema = Joi.object({
|
||||
export const barGraphSchema = Joi.object({
|
||||
type: Joi.string().valid(`bar`).required(),
|
||||
stacked: Joi.boolean().required(),
|
||||
stacked: Joi
|
||||
.boolean()
|
||||
.default(true)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// MARK: Table
|
||||
|
|
@ -45,16 +49,51 @@ export const tableSchema = Joi.object({
|
|||
name: Joi
|
||||
.string()
|
||||
.trim()
|
||||
.invalid(``)
|
||||
.required()
|
||||
.pattern(/^[a-z \-_]+(\/[a-z \-_]+)?$/i),
|
||||
buckets: Joi.alternatives([
|
||||
numberBucketSchema,
|
||||
stringBucketSchema,
|
||||
]).match(`one`),
|
||||
graph: Joi.alternatives([
|
||||
barGraphSchema,
|
||||
]).match(`one`),
|
||||
.pattern(/^[0-9a-z \-_]+(\/[0-9a-z \-_]+)?$/i),
|
||||
buckets: Joi
|
||||
.alternatives()
|
||||
.try(
|
||||
numberBucketSchema,
|
||||
stringBucketSchema,
|
||||
)
|
||||
.match(`one`)
|
||||
.required(),
|
||||
graph: Joi
|
||||
.alternatives()
|
||||
.try(
|
||||
barGraphSchema,
|
||||
)
|
||||
.match(`one`)
|
||||
.required(),
|
||||
});
|
||||
|
||||
// MARK: Row
|
||||
export const rowSchema = Joi.object({});
|
||||
/**
|
||||
* The schema for the row objects, this does not validate that the
|
||||
* value of the row conforms to the bucket configurations, however
|
||||
* it does validate that the value is at least one of the accepted
|
||||
* types. For validation of the value itself check "validateValue"
|
||||
* in `utils/buckets.mjs`
|
||||
*/
|
||||
export const rowSchema = Joi.object({
|
||||
_id: Joi
|
||||
.string()
|
||||
.alphanum()
|
||||
.required(),
|
||||
timestamp: Joi
|
||||
.string()
|
||||
.isoDate()
|
||||
.required(),
|
||||
value: Joi
|
||||
.alternatives([
|
||||
Joi.string().trim().invalid(``),
|
||||
Joi.number(),
|
||||
])
|
||||
.required(),
|
||||
privacy: Joi
|
||||
.string()
|
||||
.valid(...Object.values(PrivacyMode))
|
||||
.required(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,16 @@
|
|||
"Subtable": "Subtable",
|
||||
"Users": "Users",
|
||||
"DataVisibility": "Data Visibility"
|
||||
},
|
||||
"settings": {
|
||||
"autoTrackRolls": {
|
||||
"name": "Roll Auto-Tracking",
|
||||
"hint": "Whether or not the module should automatically add rolls made in the chat to the database. This is useful if the system you're using has implemented an integration with the module, or if you only want macros to handle the database additions."
|
||||
},
|
||||
"globalAPI": {
|
||||
"name": "Global API",
|
||||
"hint": "Whether or not the module provides a global interface for interacting with the module's backend. This is convenient for macros and using the dev console."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue