Add data validation and a world setting for enabling the Global API

This commit is contained in:
Oliver-Akins 2025-05-25 02:00:13 -06:00
parent c3d632274a
commit 354b22da57
7 changed files with 180 additions and 56 deletions

View file

@ -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;
};

View file

@ -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);

View file

@ -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(),
});