Merge branch 'version-2' into version-2-dev
This commit is contained in:
@@ -133,6 +133,7 @@ let CreatureSchema = new SimpleSchema({
|
||||
'computeErrors.$.details' : {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
// Tabletop
|
||||
|
||||
@@ -11,11 +11,11 @@ export default function applyAction(node, {creature, targets, scope, log}){
|
||||
const prop = node.node;
|
||||
if (prop.target === 'self') targets = [creature];
|
||||
|
||||
// Log the name and description
|
||||
// Log the name and summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.description?.text){
|
||||
recalculateInlineCalculations(prop.description, scope, log);
|
||||
content.value = prop.description.value;
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, scope, log);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (content.name || content.value){
|
||||
log.content.push(content);
|
||||
@@ -33,7 +33,7 @@ export default function applyAction(node, {creature, targets, scope, log}){
|
||||
targets.forEach(target => {
|
||||
applyAttackToTarget({attack, target, scope, log});
|
||||
// Apply the children, but only to the current target
|
||||
applyChildren(node, {targets: [target], scope, log});
|
||||
applyChildren(node, {creature, targets: [target], scope, log});
|
||||
});
|
||||
} else {
|
||||
applyAttackWithoutTarget({attack, scope, log});
|
||||
@@ -65,6 +65,13 @@ function applyAttackWithoutTarget({attack, scope, log}){
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
if (!criticalMiss){
|
||||
scope['$attackHit'] = {value: true}
|
||||
}
|
||||
if (!criticalHit){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
}
|
||||
|
||||
log.content.push({
|
||||
name,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
@@ -106,10 +113,10 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
if ((result > armor) || (criticalHit)){
|
||||
scope['$attackHit'] = true;
|
||||
if (criticalMiss || result < armor){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
} else {
|
||||
scope['$attackMiss'] = true;
|
||||
scope['$attackHit'] = {value: true};
|
||||
}
|
||||
} else {
|
||||
log.content.push({
|
||||
@@ -127,7 +134,7 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
function rollAttack(attack, scope){
|
||||
const rollModifierText = numberToSignedString(attack.value, true);
|
||||
let value, resultPrefix;
|
||||
if (attack.advantage === 1 || scope['$attackAdvantage']){
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -136,7 +143,7 @@ function rollAttack(attack, scope){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (attack.advantage === -1 || scope['$attackDisadvantage']){
|
||||
} else if (scope['$attackAdvantage'] === -1){
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -161,15 +168,10 @@ function applyCrits(value, scope){
|
||||
let criticalMiss;
|
||||
if (criticalHit){
|
||||
scope['$criticalHit'] = {value: true};
|
||||
scope['$attackHit'] = {value: true};
|
||||
} else {
|
||||
criticalMiss = value === 1;
|
||||
if (criticalMiss){
|
||||
scope['$criticalMiss'] = 1;
|
||||
scope['$attackMiss'] = {value: true};
|
||||
} else {
|
||||
// Untargeted attacks hit by default
|
||||
scope['$attackHit'] = {value: true}
|
||||
scope['$criticalMiss'] = {value: true};
|
||||
}
|
||||
}
|
||||
return {criticalHit, criticalMiss};
|
||||
|
||||
@@ -16,17 +16,47 @@ export default function applyBranch(node, {
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
if (prop.condition?.value) applyChildren();
|
||||
break;
|
||||
case 'index':
|
||||
if (node.children.length){
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
log.content.push({
|
||||
name: 'Branch Error',
|
||||
value: 'Index did not resolve into a valid number'
|
||||
});
|
||||
break;
|
||||
}
|
||||
let index = Math.floor(prop.condition?.value);
|
||||
if (index < 1) index = 1;
|
||||
if (index > node.children.length) index = node.children.length;
|
||||
applyProperty(node.children[index - 1], {
|
||||
creature, targets, scope, log
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'hit':
|
||||
if (scope['$attackHit']?.value) applyChildren();
|
||||
if (scope['$attackHit']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On hit**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'miss':
|
||||
if (scope['$attackMiss']?.value) applyChildren();
|
||||
if (scope['$attackMiss']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On miss**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'failedSave':
|
||||
if (scope['$saveFailed']?.value) applyChildren();
|
||||
if (scope['$saveFailed']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On failed save**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'successfulSave':
|
||||
if (scope['$saveSucceeded']?.value) applyChildren();
|
||||
if (scope['$saveSucceeded']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On save**',});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'random':
|
||||
if (node.children.length){
|
||||
|
||||
@@ -10,6 +10,7 @@ import { get } from 'lodash';
|
||||
import resolve, { map, toString } from '/imports/parser/resolve.js';
|
||||
import symbol from '/imports/parser/parseTree/symbol.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||
|
||||
export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
@@ -32,7 +33,30 @@ export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
collection: prop.parent.collection,
|
||||
};
|
||||
buffTargets.forEach(target => {
|
||||
// Apply the buff
|
||||
copyNodeListToTarget(propList, target, oldParent);
|
||||
|
||||
//Log the buff
|
||||
if (prop.name || prop.description?.value){
|
||||
if (target._id === creature._id){
|
||||
// Targeting self
|
||||
log.content.push({
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
});
|
||||
} else {
|
||||
// Targeting other
|
||||
insertCreatureLog.call({
|
||||
log: {
|
||||
creatureId: target._id,
|
||||
content: [{
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
}],
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Don't apply the children of the buff, they get copied to the target instead
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { some, intersection, difference } from 'lodash';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
|
||||
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
|
||||
@@ -35,15 +36,12 @@ export default function applyDamage(node, {
|
||||
const logValue = [];
|
||||
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
|
||||
|
||||
// Compile the dice roll and store that string first
|
||||
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
|
||||
// logValue.push(toString(compiled));
|
||||
// logErrors(context.errors, log);
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.amount, log);
|
||||
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
logValue.push(toString(rolled));
|
||||
if (rolled.parseType !== 'constant'){
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, log);
|
||||
|
||||
// Reset the errors so we don't log the same errors twice
|
||||
@@ -61,14 +59,16 @@ export default function applyDamage(node, {
|
||||
} else {
|
||||
prop.amount.value = toString(reduced);
|
||||
}
|
||||
|
||||
const damage = +reduced.value;
|
||||
let damage = +reduced.value;
|
||||
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
// Round the damage to a whole number
|
||||
damage = Math.floor(damage);
|
||||
|
||||
// Memoise the damage suffix for the log
|
||||
let suffix = (criticalHit ? ' critical ' : ' ') +
|
||||
prop.damageType +
|
||||
@@ -78,6 +78,14 @@ export default function applyDamage(node, {
|
||||
// Iterate through all the targets
|
||||
damageTargets.forEach(target => {
|
||||
|
||||
// Apply weaknesses/resistances/immunities
|
||||
damage = applyDamageMultipliers({
|
||||
target,
|
||||
damage,
|
||||
damageProp: prop,
|
||||
logValue
|
||||
});
|
||||
|
||||
// Deal the damage to the target
|
||||
let damageDealt = dealDamageWork({
|
||||
creature: target,
|
||||
@@ -114,3 +122,51 @@ export default function applyDamage(node, {
|
||||
});
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
||||
const damageType = damageProp?.damageType;
|
||||
if (!damageType) return damage;
|
||||
|
||||
const multiplier = target?.variables?.[damageType];
|
||||
if (!multiplier) return damage;
|
||||
|
||||
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
|
||||
|
||||
if (
|
||||
multiplier.immunity &&
|
||||
some(multiplier.immunities, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
logValue.push(`Immune to ${damageTypeText}`);
|
||||
return 0;
|
||||
} else {
|
||||
if (
|
||||
multiplier.resistance &&
|
||||
some(multiplier.resistances, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
logValue.push(`Resistant to ${damageTypeText}`);
|
||||
damage = Math.floor(damage / 2);
|
||||
}
|
||||
if (
|
||||
multiplier.vulnerability &&
|
||||
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
logValue.push(`Vulnerable to ${damageTypeText}`);
|
||||
damage = Math.floor(damage * 2);
|
||||
}
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
|
||||
function multiplierAppliesTo(damageProp){
|
||||
return multiplier => {
|
||||
const hasRequiredTags = difference(
|
||||
multiplier.includeTags, damageProp.tags
|
||||
).length === 0;
|
||||
|
||||
const hasNoExcludedTags = intersection(
|
||||
multiplier.excludeTags, damageProp.tags
|
||||
).length === 0;
|
||||
|
||||
return hasRequiredTags && hasNoExcludedTags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,58 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
|
||||
export default function applyRoll(node, {creature, targets, scope, log}){
|
||||
const prop = node.node;
|
||||
|
||||
if (prop.roll?.calculation){
|
||||
recalculateCalculation(prop.roll, scope, log);
|
||||
const applyChildren = node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
|
||||
if (isFinite(prop.roll.value)){
|
||||
scope[prop.variableName] = prop.roll.value;
|
||||
if (prop.roll?.calculation){
|
||||
const logValue = [];
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.roll, log);
|
||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, log);
|
||||
|
||||
// Reset the errors so we don't log the same errors twice
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, log);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
prop.roll.value = reduced.value;
|
||||
} else if (reduced.parseType === 'error'){
|
||||
prop.roll.value = null;
|
||||
} else {
|
||||
prop.roll.value = toString(reduced);
|
||||
}
|
||||
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
||||
return applyChildren();
|
||||
}
|
||||
const value = reduced.value;
|
||||
|
||||
scope[prop.variableName] = value;
|
||||
logValue.push(`**${value}**`);
|
||||
|
||||
if (!prop.silent){
|
||||
log.content.push({
|
||||
name: prop.name,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
log.content.push({
|
||||
name: prop.name,
|
||||
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
|
||||
@@ -22,10 +22,21 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
}
|
||||
log.content.push({
|
||||
name: prop.name,
|
||||
value: ' DC ' + dc,
|
||||
value: `DC **${dc}**`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
// If there are no save targets, apply all children as if the save both
|
||||
// succeeeded and failed
|
||||
if (!saveTargets?.length){
|
||||
scope['$saveFailed'] = {value: true};
|
||||
scope['$saveSucceeded'] = {value: true};
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
}
|
||||
|
||||
// Each target makes the saving throw
|
||||
saveTargets.forEach(target => {
|
||||
delete scope['$saveFailed'];
|
||||
delete scope['$saveSucceeded'];
|
||||
@@ -55,24 +66,24 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||
resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||
} else {
|
||||
value = b;
|
||||
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (save.advantage === -1){
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||
resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||
} else {
|
||||
value = b;
|
||||
resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else {
|
||||
values = rollDice(1, 20);
|
||||
value = values[0];
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
||||
}
|
||||
scope['$saveDiceRoll'] = {value};
|
||||
const result = value + save.value || 0;
|
||||
@@ -84,8 +95,8 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
scope['$saveFailed'] = {value: true};
|
||||
}
|
||||
log.content.push({
|
||||
name: 'Save',
|
||||
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
value: resultPrefix + '\n**' + result + '**',
|
||||
inline: true,
|
||||
});
|
||||
return applyChildren();
|
||||
|
||||
@@ -20,16 +20,28 @@ export default class CreatureComputation {
|
||||
// Store by id
|
||||
this.propsById[prop._id] = prop;
|
||||
|
||||
// Store tags
|
||||
const storePropOnTag = (prop, tag) => {
|
||||
if (!tag) return;
|
||||
if (this.propsWithTag[tag]){
|
||||
this.propsWithTag[tag].push(prop._id);
|
||||
} else {
|
||||
this.propsWithTag[tag] = [prop._id];
|
||||
}
|
||||
}
|
||||
// Store sets of ids in each tag
|
||||
if (prop.tags){
|
||||
prop.tags.forEach(tag => {
|
||||
if (this.propsWithTag[tag]){
|
||||
this.propsWithTag[tag].push(prop._id);
|
||||
} else {
|
||||
this.propsWithTag[tag] = [prop._id];
|
||||
}
|
||||
storePropOnTag(prop, tag);
|
||||
});
|
||||
}
|
||||
// Store tags for the property type
|
||||
storePropOnTag(prop, `#${prop.type}`);
|
||||
// Store tags for some string properties
|
||||
storePropOnTag(prop, prop.damageType);
|
||||
storePropOnTag(prop, prop.skillType);
|
||||
storePropOnTag(prop, prop.attributeType);
|
||||
storePropOnTag(prop, prop.reset);
|
||||
|
||||
// Store the prop in the dependency graph
|
||||
this.dependencyGraph.addNode(prop._id, prop);
|
||||
|
||||
@@ -218,7 +218,7 @@ function linkDamageMultiplier(dependencyGraph, prop){
|
||||
prop.damageTypes.forEach(damageType => {
|
||||
// Remove all non-letter characters from the damage name
|
||||
const damageName = damageType.replace(/[^a-z]/gi, '')
|
||||
dependencyGraph.addLink(`${damageName}Multiplier`, prop._id, prop.type);
|
||||
dependencyGraph.addLink(damageName, prop._id, prop.type);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ function linkSkill(dependencyGraph, prop){
|
||||
}
|
||||
// Skills depend on the creature's proficiencyBonus
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
|
||||
|
||||
// Depends on base value
|
||||
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
|
||||
}
|
||||
|
||||
@@ -54,6 +54,21 @@ function combineAggregations(computation, node){
|
||||
|
||||
function computeVariableProp(computation, node, prop){
|
||||
if (!prop) return;
|
||||
|
||||
// Combine damage multipliers in all props so that they can't be overridden
|
||||
if (node.data.immunity){
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
|
||||
if (prop.type === 'attribute'){
|
||||
computeVariableAsAttribute(computation, node, prop);
|
||||
} else if (prop.type === 'skill'){
|
||||
@@ -73,21 +88,16 @@ function combineMultiplierAggregator(node){
|
||||
if (!aggregator) return;
|
||||
|
||||
// Combine
|
||||
let value;
|
||||
if (aggregator.immunityCount){
|
||||
value = 0;
|
||||
} else if (
|
||||
aggregator.resistanceCount &&
|
||||
!aggregator.vulnerabilityCount
|
||||
){
|
||||
value = 0.5;
|
||||
} else if (
|
||||
!aggregator.resistanceCount &&
|
||||
aggregator.vulnerabilityCount
|
||||
){
|
||||
value = 2;
|
||||
} else {
|
||||
value = 1;
|
||||
if (aggregator.immunities?.length){
|
||||
node.data.immunity = true;
|
||||
node.data.immunities = aggregator.immunities;
|
||||
}
|
||||
if (aggregator.resistances?.length){
|
||||
node.data.resistance = true;
|
||||
node.data.resistances = aggregator.resistances;
|
||||
}
|
||||
if (aggregator.vulnerabilities?.length){
|
||||
node.data.vulnerability = true;
|
||||
node.data.vulnerabilities = aggregator.vulnerabilities;
|
||||
}
|
||||
node.data.damageMultiplyValue = value;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import { pick } from 'lodash';
|
||||
|
||||
export default function aggregateDamageMultipliers({node, linkedNode, link}){
|
||||
if (link.data !== 'damageMultiplier') return;
|
||||
const multiplierValue = linkedNode.data.value;
|
||||
if (multiplierValue === undefined) return;
|
||||
|
||||
// Store an aggregator, its presence indicates damage multipliers target this
|
||||
// variable
|
||||
if (!node.data.multiplierAggregator) node.data.multiplierAggregator = {
|
||||
immunityCount: 0,
|
||||
resistanceCount: 0,
|
||||
vulnerabilityCount: 0,
|
||||
immunities: [],
|
||||
resistances: [],
|
||||
vulnerabilities: [],
|
||||
}
|
||||
// Store a short reference to the aggregator
|
||||
const aggregator = node.data.multiplierAggregator;
|
||||
// Sum the counts of each type of multiplier
|
||||
|
||||
// Make a stripped down copy of the multiplier to store in the aggregator
|
||||
const keysToStore = ['_id', 'name'];
|
||||
if (linkedNode.data.excludeTags?.length){
|
||||
keysToStore.push('excludeTags');
|
||||
}
|
||||
if (linkedNode.data.includeTags?.length){
|
||||
keysToStore.push('includeTags');
|
||||
}
|
||||
const storedMultiplier = pick(linkedNode.data, keysToStore);
|
||||
|
||||
// Store the multiplier in the appropriate field
|
||||
if (multiplierValue === 0){
|
||||
aggregator.immunityCount += 1;
|
||||
aggregator.immunities.push(storedMultiplier);
|
||||
} else if (multiplierValue === 0.5){
|
||||
aggregator.resistanceCount += 1;
|
||||
aggregator.resistances.push(storedMultiplier);
|
||||
} else if (multiplierValue === 2){
|
||||
aggregator.vulnerabilityCount += 1;
|
||||
aggregator.vulnerabilities.push(storedMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,21 @@ import getAggregatorResult from './getAggregatorResult.js';
|
||||
*/
|
||||
export default function computeImplicitVariable(node){
|
||||
const prop = {};
|
||||
|
||||
// Combine damage multipliers
|
||||
if (node.data.immunity){
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
|
||||
const result = getAggregatorResult(node);
|
||||
if (result !== undefined){
|
||||
prop.value = result;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import getAggregatorResult from './getAggregatorResult.js';
|
||||
|
||||
export default function computeVariableAsAttribute(computation, node, prop){
|
||||
let result = getAggregatorResult(node, prop) || 0;
|
||||
let result = getAggregatorResult(node) || 0;
|
||||
|
||||
prop.total = result;
|
||||
prop.value = prop.total - (prop.damage || 0);
|
||||
|
||||
@@ -29,8 +29,9 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
}
|
||||
|
||||
// Combine everything to get the final result
|
||||
const statBase = node.data.baseValue;
|
||||
const statBase = node.data.baseValue || 0;
|
||||
const aggregator = node.data.effectAggregator;
|
||||
const aggregatorBase = aggregator?.base || 0;
|
||||
|
||||
// If there is no aggregator, determine if the prop can hide, then exit
|
||||
if (!aggregator){
|
||||
@@ -41,7 +42,7 @@ export default function computeVariableAsSkill(computation, node, prop){
|
||||
return;
|
||||
}
|
||||
// Combine aggregator
|
||||
const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0;
|
||||
const base = statBase > aggregatorBase ? statBase : aggregatorBase;
|
||||
let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul;
|
||||
if (result < aggregator.min) result = aggregator.min;
|
||||
if (result > aggregator.max) result = aggregator.max;
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
||||
|
||||
export default function getAggregatorResult(node){
|
||||
// Work out the base value as the greater of the deining stat value or
|
||||
// the damage multiplier value
|
||||
// Work out the base value as the greater of the deining stat value
|
||||
// This baseValue comes from aggregating definitions
|
||||
let statBase = node.data.baseValue;
|
||||
|
||||
const damageMultiplyValue = node.data.damageMultiplyValue;
|
||||
if (statBase === undefined || damageMultiplyValue > statBase){
|
||||
statBase = damageMultiplyValue;
|
||||
}
|
||||
// get a reference to the aggregator
|
||||
const aggregator = node.data.effectAggregator;
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ export default function(){
|
||||
const computation = buildComputationFromProps(testProperties);
|
||||
computeCreatureComputation(computation);
|
||||
const scope = id => computation.scope[id];
|
||||
assert.equal(scope('blugeoningMultiplier').value, 1);
|
||||
assert.equal(scope('customDamageMultiplier').value, 0.5);
|
||||
assert.equal(scope('slashingMultiplier').value, 0);
|
||||
assert.isTrue(scope('blugeoning').vulnerability);
|
||||
assert.isTrue(scope('customDamage').resistance);
|
||||
assert.isNotTrue(scope('customDamage').immunity);
|
||||
assert.isNotTrue(scope('customDamage').vulnerability);
|
||||
assert.isTrue(scope('slashing').immunity);
|
||||
}
|
||||
|
||||
var testProperties = [
|
||||
|
||||
@@ -12,10 +12,19 @@ export default function computeCreature(creatureId){
|
||||
writeAlteredProperties(computation);
|
||||
writeScope(creatureId, computation.scope);
|
||||
} catch (e){
|
||||
const errorText = e.reason || e.message || e.toString();
|
||||
computation.errors.push({
|
||||
type: 'crash',
|
||||
details: e.reason,
|
||||
details: {error: errorText},
|
||||
});
|
||||
const logError = {
|
||||
creatureId,
|
||||
computeError: errorText,
|
||||
};
|
||||
if (e.stack){
|
||||
logError.location = e.stack.split('\n')[1];
|
||||
}
|
||||
console.error(logError);
|
||||
} finally {
|
||||
writeErrors(creatureId, computation.errors);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ let BranchSchema = createPropertySchema({
|
||||
'eachTarget',
|
||||
// Pick one child at random
|
||||
'random',
|
||||
// Pick one child based on a given index
|
||||
'index',
|
||||
// if it has option children, asks to select one
|
||||
// Otherwise presents its own text with yes/no
|
||||
//'choice',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
/*
|
||||
* DamageMultipliers are multipliers that affect how much damage is taken from
|
||||
@@ -20,6 +21,7 @@ let DamageMultiplierSchema = new SimpleSchema({
|
||||
'damageTypes.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
},
|
||||
// The value of the damage multiplier
|
||||
value: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
const DamageSchema = createPropertySchema({
|
||||
// The roll that determines how much to damage the attribute
|
||||
@@ -24,6 +25,7 @@ const DamageSchema = createPropertySchema({
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
defaultValue: 'slashing',
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ let RollSchema = createPropertySchema({
|
||||
parseLevel: 'compile',
|
||||
optional: true,
|
||||
},
|
||||
// Prevent the roll from showing up in the log
|
||||
silent: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyRollSchema = createPropertySchema({
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { set } from 'lodash';
|
||||
|
||||
set(Meteor.settings,
|
||||
'packages.collection2.disableCollectionNamesInValidation',
|
||||
true);
|
||||
|
||||
SimpleSchema.extendOptions([
|
||||
'parseLevel',
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import './migrateTo.js';
|
||||
import './validateDatabase.js';
|
||||
import './getVersion.js';
|
||||
|
||||
@@ -16,8 +16,8 @@ const migrateTo = new ValidatedMethod({
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
numRequests: 1,
|
||||
timeInterval: 10000,
|
||||
},
|
||||
run({version}) {
|
||||
if (Meteor.isClient) return;
|
||||
|
||||
35
app/imports/migrations/methods/validateDatabase.js
Normal file
35
app/imports/migrations/methods/validateDatabase.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
|
||||
|
||||
const validateDatabase = new ValidatedMethod({
|
||||
name: 'validateDatabase',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 10000,
|
||||
},
|
||||
run() {
|
||||
assertAdmin(this.userId);
|
||||
// Very computationally expensive data diagnostics
|
||||
// Only run in an offline instance you control
|
||||
return;
|
||||
if (Meteor.isClient) return;
|
||||
|
||||
Meteor.Collection.getAll().forEach(collection => {
|
||||
if (!collection.instance._c2?._simpleSchemas) return;
|
||||
collection.instance.find({}).forEach(doc => {
|
||||
const schema = collection.instance.simpleSchema(doc);
|
||||
let cleanDoc = schema.clean(doc);
|
||||
try {
|
||||
schema.validate(cleanDoc, {modifier: false});
|
||||
} catch (e){
|
||||
console.log(collection.name, doc._id, e.message || e.reason || e.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default validateDatabase;
|
||||
39
app/imports/migrations/server/dbv1/cleanAt1.js
Normal file
39
app/imports/migrations/server/dbv1/cleanAt1.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { get, set } from 'lodash';
|
||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||
import { calculationUp } from '/imports/migrations/server/dbv1/dbv1.js';
|
||||
|
||||
export default function cleanAt1(archive){
|
||||
archive.properties = archive.properties.map(prop => {
|
||||
let cleanProp = prop;
|
||||
try {
|
||||
if (prop.type === 'attack') prop.type = 'action';
|
||||
// Get the schema
|
||||
const schema = CreatureProperties.simpleSchema(prop);
|
||||
// Clean all the text fields with inline calcs
|
||||
schema.inlineCalculationFields().forEach(key => {
|
||||
applyFnToKey(prop, key, (prop, key) => {
|
||||
let field = get(prop, key);
|
||||
if (typeof field === 'string' || typeof field === 'number'){
|
||||
field = calculationUp(field);
|
||||
set(prop, key, {text: `${field}`});
|
||||
}
|
||||
});
|
||||
});
|
||||
schema.computedFields().forEach(key => {
|
||||
applyFnToKey(prop, key, (prop, key) => {
|
||||
let field = get(prop, key) || get(prop, key + 'Calculation');
|
||||
if (typeof field === 'string' || typeof field === 'number'){
|
||||
field = calculationUp(field);
|
||||
set(prop, key, {calculation: `${field}`});
|
||||
}
|
||||
});
|
||||
});
|
||||
cleanProp = schema.clean(prop);
|
||||
schema.validate(cleanProp);
|
||||
} catch (e){
|
||||
console.warn({propId: prop._id, error: e.message || e.reason || e.toString()});
|
||||
}
|
||||
return cleanProp;
|
||||
});
|
||||
}
|
||||
@@ -215,16 +215,17 @@ function getInlineComputationTransforms(key){
|
||||
];
|
||||
}
|
||||
|
||||
function calculationUp(val){
|
||||
export function calculationUp(val){
|
||||
if (typeof val !== 'string') return val;
|
||||
if (!val.replace) console.log({val, replace: val.replace});
|
||||
return val.replace(/#(\w+).(\w+)Result/g, '#$1.$2')
|
||||
.replace('.value', '.total')
|
||||
.replace('.currentValue', '.value');
|
||||
.replace(/\.value/g, '.total')
|
||||
.replace(/\.currentValue/g, '.value');
|
||||
}
|
||||
|
||||
function calculationDown(val){
|
||||
if (typeof val !== 'string') return val;
|
||||
return val.replace('.value', '.currentValue').replace('.total', '.value');
|
||||
return val.replace(/\.value/g, '.currentValue').replace(/\.total/g, '.value');
|
||||
}
|
||||
|
||||
function nanToNull(val){
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import cleanAt1 from '/imports/migrations/server/dbv1/cleanAt1.js';
|
||||
|
||||
/* eslint no-fallthrough: "off" -- Using switch fallthrough to run all
|
||||
migration steps after the current version of the file. */
|
||||
@@ -17,12 +17,3 @@ function migrateLegacyArchive(archive){
|
||||
// TODO:
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
function cleanAt1(archive){
|
||||
archive.properties.map(prop => {
|
||||
const schema = CreatureProperties.simpleSchema(prop);
|
||||
const cleanProp = schema.clean(prop);
|
||||
schema.validate(cleanProp);
|
||||
return cleanProp;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
:search-input.sync="searchInput"
|
||||
:disabled="isDisabled"
|
||||
:multiple="multiple"
|
||||
outlined
|
||||
@change="customChange"
|
||||
@focus="focused = true"
|
||||
@@ -24,12 +25,28 @@
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
props: {
|
||||
multiple: Boolean,
|
||||
},
|
||||
data(){ return {
|
||||
searchInput: '',
|
||||
}},
|
||||
computed: {
|
||||
// Multiple combobox gets a long default debounce time while single
|
||||
// value gets a shorter one
|
||||
debounceTime() {
|
||||
if (Number.isFinite(this.debounce)){
|
||||
return this.debounce;
|
||||
} else if (Number.isFinite(this.context.debounceTime)){
|
||||
return this.context.debounceTime;
|
||||
} else {
|
||||
return this.multiple ? 1000 : 100;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
customChange(val){
|
||||
this.change(val);
|
||||
this.input(val);
|
||||
this.searchInput = '';
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
data(){ return {
|
||||
error: false,
|
||||
ackErrors: null,
|
||||
rulesErrors: null,
|
||||
focused: false,
|
||||
loading: false,
|
||||
dirty: false,
|
||||
@@ -30,6 +31,7 @@ export default {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
rules: Array,
|
||||
},
|
||||
watch: {
|
||||
focused(newFocus){
|
||||
@@ -42,7 +44,11 @@ export default {
|
||||
// Start the loading bar on defocus if the input is dirty
|
||||
// It might be a lie, we aren't doing the work yet, but it feels laggy
|
||||
// to defocus an element and then it starts working after a delay
|
||||
if (!newFocus && this.dirty){
|
||||
if (
|
||||
!newFocus &&
|
||||
this.dirty &&
|
||||
!(this.rulesErrors && this.rulesErrors.length)
|
||||
){
|
||||
if (this.hasChangeListener) this.loading = true;
|
||||
}
|
||||
},
|
||||
@@ -54,7 +60,10 @@ export default {
|
||||
}
|
||||
},
|
||||
value(newValue){
|
||||
if (!this.focused){
|
||||
if (
|
||||
!this.focused &&
|
||||
!(this.rulesErrors && this.rulesErrors.length)
|
||||
){
|
||||
this.safeValue = newValue;
|
||||
}
|
||||
},
|
||||
@@ -69,6 +78,22 @@ export default {
|
||||
this.$emit('input', val);
|
||||
this.inputValue = val;
|
||||
this.dirty = true;
|
||||
|
||||
// Apply the rules if there are any
|
||||
this.rulesErrors = null;
|
||||
if (this.rules && this.rules.length){
|
||||
this.rules.forEach(rule => {
|
||||
const result = rule(val);
|
||||
if (typeof result === 'string'){
|
||||
if (!this.rulesErrors) this.rulesErrors = [];
|
||||
this.rulesErrors.push(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.rulesErrors){
|
||||
return;
|
||||
}
|
||||
|
||||
this.debouncedChange(val);
|
||||
},
|
||||
acknowledgeChange(error){
|
||||
@@ -81,6 +106,8 @@ export default {
|
||||
this.ackErrors = error;
|
||||
} else if (error.reason){
|
||||
this.ackErrors = error.reason;
|
||||
} else if (error.message){
|
||||
this.ackErrors = error.message;
|
||||
} else {
|
||||
this.ackErrors = 'Something went wrong'
|
||||
console.error(error);
|
||||
@@ -106,6 +133,9 @@ export default {
|
||||
computed: {
|
||||
errors(){
|
||||
let errors = this.ackErrors ? [this.ackErrors] : [];
|
||||
if (Array.isArray(this.rulesErrors)){
|
||||
errors.push(...this.rulesErrors)
|
||||
}
|
||||
if (Array.isArray(this.errorMessages)){
|
||||
errors.push(...this.errorMessages);
|
||||
} else if (typeof this.errorMessages === 'string' && this.errorMessages){
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<damage-multiplier-card
|
||||
v-if="multipliers && multipliers.length"
|
||||
:multipliers="multipliers"
|
||||
@click-multiplier="clickProperty"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="appliedBuffs.length"
|
||||
class="buffs"
|
||||
@@ -199,10 +205,6 @@
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div v-if="numKeys(creature.damageMultipliers)">
|
||||
<damage-multiplier-card :model="creature.damageMultipliers" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="savingThrows.length"
|
||||
class="saving-throws"
|
||||
@@ -367,7 +369,9 @@
|
||||
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
const getProperties = function(creature, filter){
|
||||
const getProperties = function(creature, filter, options = {
|
||||
sort: {order: 1}
|
||||
}){
|
||||
if (!creature) return;
|
||||
if (creature.settings.hideUnusedStats){
|
||||
filter.hide = {$ne: true};
|
||||
@@ -376,9 +380,8 @@
|
||||
filter.removed = {$ne: true};
|
||||
filter.inactive = {$ne: true};
|
||||
filter.overridden = {$ne: true};
|
||||
return CreatureProperties.find(filter, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
|
||||
return CreatureProperties.find(filter, options);
|
||||
};
|
||||
|
||||
const getAttributeOfType = function(creature, type){
|
||||
@@ -421,7 +424,7 @@
|
||||
}},
|
||||
meteor: {
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId);
|
||||
return Creatures.findOne(this.creatureId, {fields: {settings: 1}});
|
||||
},
|
||||
abilities(){
|
||||
return getAttributeOfType(this.creature, 'ability');
|
||||
@@ -484,6 +487,13 @@
|
||||
appliedBuffs(){
|
||||
return getProperties(this.creature, {type: 'buff'});
|
||||
},
|
||||
multipliers(){
|
||||
return getProperties(this.creature, {
|
||||
type: 'damageMultiplier'
|
||||
}, {
|
||||
sort: {value: 1, order: 1}
|
||||
});
|
||||
},
|
||||
attacks(){
|
||||
let props = getProperties(this.creature, {type: 'attack'})
|
||||
return props && props.map(attack => {
|
||||
@@ -511,10 +521,6 @@
|
||||
damageProperty.call({_id, operation: 'increment' ,value: -value});
|
||||
}
|
||||
},
|
||||
numKeys(obj){
|
||||
if (!obj) return 0;
|
||||
return Object.keys(obj).length;
|
||||
},
|
||||
softRemove(_id){
|
||||
softRemoveProperty.call({_id}, error => {
|
||||
if (error) console.error(error);
|
||||
@@ -531,7 +537,7 @@
|
||||
if (!spellId) return;
|
||||
doCastSpell.call({spellId, slotId}, error => {
|
||||
if (!error) return;
|
||||
snackbar({text: error.reason});
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -220,36 +220,21 @@ export default {
|
||||
},
|
||||
change({path, value, ack}){
|
||||
if (path && path[0] === 'equipped'){
|
||||
equipItem.call({_id: this.currentId, equipped: value}, (error) =>{
|
||||
if (error) console.warn(error);
|
||||
ack && ack(error && error.reason || error);
|
||||
});
|
||||
equipItem.call({_id: this.currentId, equipped: value}, ack);
|
||||
return;
|
||||
}
|
||||
updateCreatureProperty.call({_id: this.currentId, path, value}, (error) =>{
|
||||
if (error) console.warn(error);
|
||||
ack && ack(error && error.reason || error);
|
||||
});
|
||||
updateCreatureProperty.call({_id: this.currentId, path, value}, ack);
|
||||
},
|
||||
damage({operation, value, ack}){
|
||||
damageProperty.call({_id: this.currentId, operation, value}, (error) =>{
|
||||
if (error) console.warn(error);
|
||||
ack && ack(error && error.reason || error);
|
||||
});
|
||||
damageProperty.call({_id: this.currentId, operation, value}, ack);
|
||||
},
|
||||
push({path, value, ack}){
|
||||
pushToProperty.call({_id: this.currentId, path, value}, (error) =>{
|
||||
if (error) console.warn(error);
|
||||
ack && ack(error && error.reason || error);
|
||||
});
|
||||
pushToProperty.call({_id: this.currentId, path, value}, ack);
|
||||
},
|
||||
pull({path, ack}){
|
||||
let itemId = get(this.model, path)._id;
|
||||
path.pop();
|
||||
pullFromProperty.call({_id: this.currentId, path, itemId}, (error) =>{
|
||||
if (error) console.warn(error);
|
||||
ack && ack(error && error.reason || error);
|
||||
});
|
||||
pullFromProperty.call({_id: this.currentId, path, itemId}, ack);
|
||||
},
|
||||
remove(){
|
||||
const _id = this.currentId;
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
:key="index"
|
||||
class="content-line"
|
||||
>
|
||||
<h4 class="content-name">
|
||||
<h4
|
||||
class="content-name"
|
||||
style="min-height: 12px;"
|
||||
>
|
||||
{{ content.name }}
|
||||
</h4>
|
||||
<markdown-text
|
||||
@@ -13,6 +16,10 @@
|
||||
class="content-value"
|
||||
:markdown="content.value"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
style="min-height: 12px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,7 +43,7 @@ export default {
|
||||
<style lang="css" scoped>
|
||||
.content-line {
|
||||
min-height: 24px;
|
||||
margin-top: 2px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.content-line .details {
|
||||
|
||||
@@ -1,72 +1,96 @@
|
||||
<template lang="html">
|
||||
<v-card>
|
||||
<v-list
|
||||
three-line
|
||||
>
|
||||
<v-list-item v-if="weaknesses.length">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
Vulnerabilities
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ weaknesses }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="resistances.length">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
Resistances
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ resistances }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="immunities.length">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
Immunities
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ immunities }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
<div>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="multiplier in multipliers"
|
||||
:key="multiplier._id"
|
||||
:data-id="multiplier._id"
|
||||
@click="$emit('click-multiplier', {_id: multiplier._id})"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ title(multiplier) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="multiplier.name">
|
||||
{{ multiplier.name }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="d-flex flex-wrap align-center">
|
||||
<v-chip
|
||||
v-for="(damageType, index) in multiplier.damageTypes"
|
||||
:key="index"
|
||||
class="my-1 mr-1"
|
||||
style="cursor: pointer"
|
||||
:input-value="true"
|
||||
outlined
|
||||
small
|
||||
label
|
||||
>
|
||||
{{ damageType }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle
|
||||
v-if="multiplier.includeTags && multiplier.includeTags.length"
|
||||
class="d-flex flex-wrap align-center"
|
||||
>
|
||||
<div>
|
||||
For:
|
||||
</div>
|
||||
<v-chip
|
||||
v-for="(damageType, index) in multiplier.includeTags"
|
||||
:key="index"
|
||||
class="ma-1"
|
||||
style="cursor: pointer"
|
||||
:input-value="true"
|
||||
small
|
||||
outlined
|
||||
>
|
||||
{{ damageType }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle
|
||||
v-if="multiplier.excludeTags && multiplier.excludeTags.length"
|
||||
class="d-flex flex-wrap align-center"
|
||||
>
|
||||
<div>
|
||||
Except:
|
||||
</div>
|
||||
<v-chip
|
||||
v-for="(damageType, index) in multiplier.excludeTags"
|
||||
:key="index"
|
||||
class="ma-1"
|
||||
style="cursor: pointer"
|
||||
:input-value="true"
|
||||
small
|
||||
outlined
|
||||
>
|
||||
{{ damageType }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
|
||||
export default {
|
||||
props: {
|
||||
model:{
|
||||
type: Object,
|
||||
multipliers:{
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
weaknesses(){
|
||||
return getKeysOfValue(this.model, 2).join(', ');
|
||||
},
|
||||
resistances(){
|
||||
return getKeysOfValue(this.model, 0.5).join(', ');
|
||||
},
|
||||
immunities(){
|
||||
return getKeysOfValue(this.model, 0).join(', ');
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function getKeysOfValue(object, value){
|
||||
let keys = [];
|
||||
for (let key in object){
|
||||
if (object[key] === value){
|
||||
keys.push(key);
|
||||
methods: {
|
||||
title(prop){
|
||||
switch (prop.value){
|
||||
case 0: return 'Immunity';
|
||||
case 0.5: return 'Resistance';
|
||||
case 2: return 'Vulnerability';
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
@change="({path, value, ack}) =>
|
||||
$emit('change', {path: ['condition', ...path], value, ack})"
|
||||
/>
|
||||
<computed-field
|
||||
v-else-if="model.branchType === 'index'"
|
||||
label="Index"
|
||||
hint="Which child to apply. An index of 2 will choose the 2nd child."
|
||||
:model="model.condition"
|
||||
:error-messages="errors.condition"
|
||||
@change="({path, value, ack}) =>
|
||||
$emit('change', {path: ['condition', ...path], value, ack})"
|
||||
/>
|
||||
</v-expand-transition>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
@@ -51,7 +60,8 @@
|
||||
{value: 'failedSave', text: 'Save failed'},
|
||||
{value: 'successfulSave', text: 'Save succeeded'},
|
||||
{value: 'eachTarget', text: 'Apply to each target'},
|
||||
{value: 'random', text: 'Random'},
|
||||
{value: 'random', text: 'Random'},
|
||||
{value: 'index', text: 'Calculated index'},
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
@@ -64,6 +74,7 @@
|
||||
case 'successfulSave': return 'If the parent save is made, the child properties are applied';
|
||||
case 'eachTarget': return 'Applies each child property once per target';
|
||||
case 'random': return 'Chooses one child property at random and applies it';
|
||||
case 'index': return 'Chooses one child property to apply based on the given index';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
label="Damage Type"
|
||||
style="flex-basis: 200px;"
|
||||
hint="Use the Healing type to restore hit points"
|
||||
:rules="damageTypeRules"
|
||||
:items="DAMAGE_TYPES"
|
||||
:value="model.damageType"
|
||||
:error-messages="errors.damageType"
|
||||
@@ -46,7 +47,8 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
hint=""
|
||||
:items="['magical', 'silvered']"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
@@ -57,6 +59,7 @@
|
||||
<script lang="js">
|
||||
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
|
||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
export default {
|
||||
mixins: [propertyFormMixin],
|
||||
@@ -68,6 +71,14 @@ export default {
|
||||
},
|
||||
data(){return{
|
||||
DAMAGE_TYPES,
|
||||
damageTypeRules: [
|
||||
value => {
|
||||
if (!value) return 'Damage type is required';
|
||||
if (!VARIABLE_NAME_REGEX.test(value)){
|
||||
return `${value} is not a valid damage name`
|
||||
}
|
||||
}
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
targetOptions(){
|
||||
|
||||
@@ -1,93 +1,102 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-form">
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
:error-messages="errors.name"
|
||||
@change="change('name', ...arguments)"
|
||||
/>
|
||||
<div class="layout wrap">
|
||||
<smart-select
|
||||
label="Damage Type"
|
||||
style="flex-basis: 300px;"
|
||||
multiple
|
||||
:items="damageTypes"
|
||||
:value="model.damageTypes"
|
||||
:error-messages="errors.damageTypes"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="change('damageTypes', ...arguments)"
|
||||
/>
|
||||
<smart-select
|
||||
label="Value"
|
||||
style="flex-basis: 300px;"
|
||||
:items="values"
|
||||
:value="model.value"
|
||||
:error-messages="errors.value"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="change('value', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
:error-messages="errors.name"
|
||||
@change="change('name', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<smart-select
|
||||
label="Value"
|
||||
style="flex-basis: 300px;"
|
||||
:items="values"
|
||||
:value="model.value"
|
||||
:error-messages="errors.value"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="change('value', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<smart-combobox
|
||||
label="Damage Types"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:rules="damageTypeRules"
|
||||
:items="DAMAGE_TYPES"
|
||||
:value="model.damageTypes"
|
||||
:error-messages="errors.damageTypes"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@update:error="error"
|
||||
@change="change('damageTypes', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<form-sections>
|
||||
<form-section
|
||||
name="Advanced"
|
||||
>
|
||||
<smart-combobox
|
||||
label="Damage tags required"
|
||||
hint="This damage multiplier will only apply to damage that has all of these tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:items="['magical', 'silvered']"
|
||||
:value="model.includeTags"
|
||||
@change="change('includeTags', ...arguments)"
|
||||
/>
|
||||
<smart-combobox
|
||||
label="Damage tags excluded"
|
||||
hint="Damage that includes any of these tags will bypass this damage multiplier"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:items="['magical', 'silvered']"
|
||||
:value="model.excludeTags"
|
||||
@change="change('excludeTags', ...arguments)"
|
||||
/>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint=""
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
</form-section>
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSections,
|
||||
FormSection,
|
||||
},
|
||||
mixins: [propertyFormMixin],
|
||||
data(){return {
|
||||
damageTypes: [
|
||||
{
|
||||
value: 'bludgeoning',
|
||||
text: 'Bludgeoning',
|
||||
}, {
|
||||
value: 'piercing',
|
||||
text: 'Piercing',
|
||||
}, {
|
||||
value: 'slashing',
|
||||
text: 'Slashing',
|
||||
}, {
|
||||
value: 'acid',
|
||||
text: 'Acid',
|
||||
}, {
|
||||
value: 'cold',
|
||||
text: 'Cold',
|
||||
}, {
|
||||
value: 'fire',
|
||||
text: 'Fire',
|
||||
}, {
|
||||
value: 'force',
|
||||
text: 'Force',
|
||||
}, {
|
||||
value: 'lightning',
|
||||
text: 'Lightning',
|
||||
}, {
|
||||
value: 'necrotic',
|
||||
text: 'Necrotic',
|
||||
}, {
|
||||
value: 'poison',
|
||||
text: 'Poison',
|
||||
}, {
|
||||
value: 'psychic',
|
||||
text: 'Psychic',
|
||||
}, {
|
||||
value: 'radiant',
|
||||
text: 'Radiant',
|
||||
}, {
|
||||
value: 'thunder',
|
||||
text: 'Thunder',
|
||||
},
|
||||
],
|
||||
DAMAGE_TYPES,
|
||||
values: [
|
||||
{
|
||||
value: 0,
|
||||
@@ -100,7 +109,23 @@
|
||||
text: 'Vulnerability',
|
||||
},
|
||||
],
|
||||
damageTypeRules: [
|
||||
value => {
|
||||
if (value && value.length){
|
||||
for(let i = 0; i < value.length; i++){
|
||||
if (!VARIABLE_NAME_REGEX.test(value[i])){
|
||||
return `${value[i]} is not a valid damage name`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
};},
|
||||
methods: {
|
||||
error(e){
|
||||
console.log({e})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -36,6 +36,20 @@
|
||||
/>
|
||||
<form-sections>
|
||||
<form-section name="Advanced">
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
>
|
||||
<smart-switch
|
||||
label="Don't show in log"
|
||||
:value="model.silent"
|
||||
:error-messages="errors.silent"
|
||||
@change="change('silent', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
|
||||
@@ -222,6 +222,14 @@
|
||||
</v-slide-x-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<inline-computation-field
|
||||
label="Summary"
|
||||
hint="This will appear in the action card in the character sheet, summarise what the action does"
|
||||
:model="model.summary"
|
||||
:error-messages="errors.summary"
|
||||
@change="({path, value, ack}) =>
|
||||
$emit('change', {path: ['summary', ...path], value, ack})"
|
||||
/>
|
||||
<inline-computation-field
|
||||
label="Description"
|
||||
:model="model.description"
|
||||
|
||||
@@ -28,6 +28,7 @@ export default {
|
||||
case 'successfulSave': return 'On save';
|
||||
case 'eachTarget': return 'Each target';
|
||||
case 'random': return 'Pick one at random';
|
||||
case 'index': return 'Pick one by index';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
name="Condition"
|
||||
:calculation="model.condition"
|
||||
/>
|
||||
<property-field
|
||||
v-else-if="model.branchType === 'index'"
|
||||
name="Index"
|
||||
:calculation="model.condition"
|
||||
/>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,6 +35,7 @@
|
||||
case 'successfulSave': return 'On save';
|
||||
case 'eachTarget': return 'Each target';
|
||||
case 'random': return 'Pick one at random';
|
||||
case 'index': return 'Pick one by index';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,57 @@
|
||||
<div>
|
||||
<v-row dense>
|
||||
<property-field
|
||||
name="Operation"
|
||||
name="Value"
|
||||
:value="operation"
|
||||
/>
|
||||
<property-field
|
||||
name="Damage types"
|
||||
:value="model.damageTypes.join(', ')"
|
||||
/>
|
||||
wrap
|
||||
>
|
||||
<v-chip
|
||||
v-for="(damageType, index) in model.damageTypes"
|
||||
:key="index"
|
||||
class="mt-1 mr-1"
|
||||
:input-value="true"
|
||||
outlined
|
||||
small
|
||||
label
|
||||
>
|
||||
{{ damageType }}
|
||||
</v-chip>
|
||||
</property-field>
|
||||
<property-field
|
||||
v-if="model.includeTags && model.includeTags.length"
|
||||
name="Damage tags required"
|
||||
wrap
|
||||
>
|
||||
<v-chip
|
||||
v-for="(damageType, index) in model.includeTags"
|
||||
:key="index"
|
||||
class="mt-1 mr-1"
|
||||
:input-value="true"
|
||||
small
|
||||
outlined
|
||||
>
|
||||
{{ damageType }}
|
||||
</v-chip>
|
||||
</property-field>
|
||||
<property-field
|
||||
v-if="model.excludeTags && model.excludeTags.length"
|
||||
name="Damage tags excluded"
|
||||
wrap
|
||||
>
|
||||
<v-chip
|
||||
v-for="(damageType, index) in model.excludeTags"
|
||||
:key="index"
|
||||
class="mt-1 mr-1"
|
||||
:input-value="true"
|
||||
small
|
||||
outlined
|
||||
>
|
||||
{{ damageType }}
|
||||
</v-chip>
|
||||
</property-field>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,19 +19,22 @@
|
||||
{{ name }}
|
||||
</v-sheet>
|
||||
<div
|
||||
class="flex-grow-1 layout align-center justify-center flex-wrap"
|
||||
class="flex-grow-1 d-flex align-center flex-wrap"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="layout align-center"
|
||||
class="d-flex align-center"
|
||||
:class="{
|
||||
'text-body-1': !isLarge,
|
||||
'text-h4': isLarge,
|
||||
'justify-center': isCenter,
|
||||
'justify-end': end,
|
||||
'flex-wrap': wrap,
|
||||
'mono': isMono,
|
||||
'flex-grow-0': calculation && calculation.effects,
|
||||
'flex-grow-1': !calculation || !calculation.effects,
|
||||
'ma-3': calculation && calculation.effects,
|
||||
...$attrs.class,
|
||||
}"
|
||||
style="overflow-x: auto;"
|
||||
v-bind="$attrs"
|
||||
@@ -98,6 +101,7 @@ export default {
|
||||
large: Boolean,
|
||||
mono: Boolean,
|
||||
signed: Boolean,
|
||||
wrap: Boolean,
|
||||
cols: {
|
||||
type: Object,
|
||||
default: () => ({cols: 12, sm: 6, md: 4}),
|
||||
|
||||
1
app/packages/collection2/.npm/package/.gitignore
vendored
Normal file
1
app/packages/collection2/.npm/package/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
7
app/packages/collection2/.npm/package/README
Normal file
7
app/packages/collection2/.npm/package/README
Normal file
@@ -0,0 +1,7 @@
|
||||
This directory and the files immediately inside it are automatically generated
|
||||
when you change this package's NPM dependencies. Commit the files in this
|
||||
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
|
||||
so that others run the same versions of sub-dependencies.
|
||||
|
||||
You should NOT check in the node_modules directory that Meteor automatically
|
||||
creates; if you are using git, the .gitignore file tells git to ignore it.
|
||||
20
app/packages/collection2/.npm/package/npm-shrinkwrap.json
generated
Normal file
20
app/packages/collection2/.npm/package/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"lodash.isempty": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
|
||||
"integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4="
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
|
||||
},
|
||||
"lodash.isobject": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
|
||||
"integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0="
|
||||
}
|
||||
}
|
||||
}
|
||||
50
app/packages/collection2/.versions
Normal file
50
app/packages/collection2/.versions
Normal file
@@ -0,0 +1,50 @@
|
||||
aldeed:collection2@3.5.0
|
||||
allow-deny@1.1.0
|
||||
babel-compiler@7.7.0
|
||||
babel-runtime@1.5.0
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
boilerplate-generator@1.7.1
|
||||
callback-hook@1.3.1
|
||||
check@1.3.1
|
||||
ddp@1.4.0
|
||||
ddp-client@2.5.0
|
||||
ddp-common@1.4.0
|
||||
ddp-server@2.4.0
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.7.1
|
||||
ecmascript@0.15.3
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.11.1
|
||||
ecmascript-runtime-server@0.10.1
|
||||
ejson@1.1.1
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
id-map@1.1.1
|
||||
inter-process-messaging@0.1.1
|
||||
logging@1.2.0
|
||||
meteor@1.9.3
|
||||
minimongo@1.7.0
|
||||
modern-browsers@0.1.5
|
||||
modules@0.16.0
|
||||
modules-runtime@0.12.0
|
||||
mongo@1.12.0
|
||||
mongo-decimal@0.1.2
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
npm-mongo@3.9.1
|
||||
ordered-dict@1.1.0
|
||||
promise@0.12.0
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.0
|
||||
react-fast-refresh@0.1.1
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
socket-stream-client@0.4.0
|
||||
tmeasday:check-npm-versions@1.0.2
|
||||
tracker@1.2.0
|
||||
typescript@4.3.5
|
||||
underscore@1.0.10
|
||||
webapp@1.11.1
|
||||
webapp-hashing@1.1.0
|
||||
739
app/packages/collection2/collection2.js
Normal file
739
app/packages/collection2/collection2.js
Normal file
@@ -0,0 +1,739 @@
|
||||
import { EventEmitter } from 'meteor/raix:eventemitter';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';
|
||||
import { EJSON } from 'meteor/ejson';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import isObject from 'lodash.isobject';
|
||||
import { flattenSelector } from './lib';
|
||||
|
||||
checkNpmVersions({ 'simpl-schema': '>=0.0.0' }, 'aldeed:collection2');
|
||||
|
||||
const SimpleSchema = require('simpl-schema').default;
|
||||
|
||||
// Exported only for listening to events
|
||||
const Collection2 = new EventEmitter();
|
||||
|
||||
Collection2.cleanOptions = {
|
||||
filter: true,
|
||||
autoConvert: true,
|
||||
removeEmptyStrings: true,
|
||||
trimStrings: true,
|
||||
removeNullsFromArrays: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Mongo.Collection.prototype.attachSchema
|
||||
* @param {SimpleSchema|Object} ss - SimpleSchema instance or a schema definition object
|
||||
* from which to create a new SimpleSchema instance
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [options.transform=false] Set to `true` if your document must be passed
|
||||
* through the collection's transform to properly validate.
|
||||
* @param {Boolean} [options.replace=false] Set to `true` to replace any existing schema instead of combining
|
||||
* @return {undefined}
|
||||
*
|
||||
* Use this method to attach a schema to a collection created by another package,
|
||||
* such as Meteor.users. It is most likely unsafe to call this method more than
|
||||
* once for a single collection, or to call this for a collection that had a
|
||||
* schema object passed to its constructor.
|
||||
*/
|
||||
Mongo.Collection.prototype.attachSchema = function c2AttachSchema(ss, options) {
|
||||
options = options || {};
|
||||
|
||||
// Allow passing just the schema object
|
||||
if (!SimpleSchema.isSimpleSchema(ss)) {
|
||||
ss = new SimpleSchema(ss);
|
||||
}
|
||||
|
||||
function attachTo(obj) {
|
||||
// we need an array to hold multiple schemas
|
||||
// position 0 is reserved for the "base" schema
|
||||
obj._c2 = obj._c2 || {};
|
||||
obj._c2._simpleSchemas = obj._c2._simpleSchemas || [ null ];
|
||||
|
||||
if (typeof options.selector === 'object') {
|
||||
// Selector Schemas
|
||||
|
||||
// Extend selector schema with base schema
|
||||
const baseSchema = obj._c2._simpleSchemas[0];
|
||||
if (baseSchema) {
|
||||
ss = extendSchema(baseSchema.schema, ss);
|
||||
}
|
||||
|
||||
// Index of existing schema with identical selector
|
||||
let schemaIndex;
|
||||
|
||||
// Loop through existing schemas with selectors,
|
||||
for (schemaIndex = obj._c2._simpleSchemas.length - 1; 0 < schemaIndex; schemaIndex--) {
|
||||
const schema = obj._c2._simpleSchemas[schemaIndex];
|
||||
if (schema && isEqual(schema.selector, options.selector)) break;
|
||||
}
|
||||
|
||||
if (schemaIndex <= 0) {
|
||||
// We didn't find the schema in our array - push it into the array
|
||||
obj._c2._simpleSchemas.push({
|
||||
schema: ss,
|
||||
selector: options.selector,
|
||||
});
|
||||
} else {
|
||||
// We found a schema with an identical selector in our array,
|
||||
if (options.replace === true) {
|
||||
// Replace existing selector schema with new selector schema
|
||||
obj._c2._simpleSchemas[schemaIndex].schema = ss;
|
||||
} else {
|
||||
// Extend existing selector schema with new selector schema.
|
||||
obj._c2._simpleSchemas[schemaIndex].schema = extendSchema(obj._c2._simpleSchemas[schemaIndex].schema, ss);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Base Schema
|
||||
if (options.replace === true) {
|
||||
// Replace base schema and delete all other schemas
|
||||
obj._c2._simpleSchemas = [{
|
||||
schema: ss,
|
||||
selector: options.selector,
|
||||
}];
|
||||
} else {
|
||||
// Set base schema if not yet set
|
||||
if (!obj._c2._simpleSchemas[0]) {
|
||||
return obj._c2._simpleSchemas[0] = { schema: ss, selector: undefined };
|
||||
}
|
||||
// Extend base schema and therefore extend all schemas
|
||||
obj._c2._simpleSchemas.forEach((schema, index) => {
|
||||
if (obj._c2._simpleSchemas[index]) {
|
||||
obj._c2._simpleSchemas[index].schema = extendSchema(obj._c2._simpleSchemas[index].schema, ss);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachTo(this);
|
||||
// Attach the schema to the underlying LocalCollection, too
|
||||
if (this._collection instanceof LocalCollection) {
|
||||
this._collection._c2 = this._collection._c2 || {};
|
||||
attachTo(this._collection);
|
||||
}
|
||||
|
||||
defineDeny(this, options);
|
||||
keepInsecure(this);
|
||||
|
||||
Collection2.emit('schema.attached', this, ss, options);
|
||||
};
|
||||
|
||||
[Mongo.Collection, LocalCollection].forEach((obj) => {
|
||||
/**
|
||||
* simpleSchema
|
||||
* @description function detect the correct schema by given params. If it
|
||||
* detect multi-schema presence in the collection, then it made an attempt to find a
|
||||
* `selector` in args
|
||||
* @param {Object} doc - It could be <update> on update/upsert or document
|
||||
* itself on insert/remove
|
||||
* @param {Object} [options] - It could be <update> on update/upsert etc
|
||||
* @param {Object} [query] - it could be <query> on update/upsert
|
||||
* @return {Object} Schema
|
||||
*/
|
||||
obj.prototype.simpleSchema = function (doc, options, query) {
|
||||
if (!this._c2) return null;
|
||||
if (this._c2._simpleSchema) return this._c2._simpleSchema;
|
||||
|
||||
const schemas = this._c2._simpleSchemas;
|
||||
if (schemas && schemas.length > 0) {
|
||||
|
||||
let schema, selector, target;
|
||||
// Position 0 reserved for base schema
|
||||
for (var i = 1; i < schemas.length; i++) {
|
||||
schema = schemas[i];
|
||||
selector = Object.keys(schema.selector)[0];
|
||||
|
||||
// We will set this to undefined because in theory you might want to select
|
||||
// on a null value.
|
||||
target = undefined;
|
||||
// here we are looking for selector in different places
|
||||
// $set should have more priority here
|
||||
if (doc.$set && typeof doc.$set[selector] !== 'undefined') {
|
||||
target = doc.$set[selector];
|
||||
} else if (typeof doc[selector] !== 'undefined') {
|
||||
target = doc[selector];
|
||||
} else if (options && options.selector) {
|
||||
target = options.selector[selector];
|
||||
} else if (query && query[selector]) { // on upsert/update operations
|
||||
target = query[selector];
|
||||
}
|
||||
|
||||
// we need to compare given selector with doc property or option to
|
||||
// find right schema
|
||||
if (target !== undefined && target === schema.selector[selector]) {
|
||||
return schema.schema;
|
||||
}
|
||||
}
|
||||
if (schemas[0]) {
|
||||
return schemas[0].schema;
|
||||
} else {
|
||||
throw new Error('No default schema');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
});
|
||||
|
||||
// Wrap DB write operation methods
|
||||
['insert', 'update'].forEach((methodName) => {
|
||||
const _super = Mongo.Collection.prototype[methodName];
|
||||
Mongo.Collection.prototype[methodName] = function(...args) {
|
||||
let options = (methodName === 'insert') ? args[1] : args[2];
|
||||
|
||||
// Support missing options arg
|
||||
if (!options || typeof options === 'function') {
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (this._c2 && options.bypassCollection2 !== true) {
|
||||
let userId = null;
|
||||
try { // https://github.com/aldeed/meteor-collection2/issues/175
|
||||
userId = Meteor.userId();
|
||||
} catch (err) {}
|
||||
|
||||
args = doValidate(
|
||||
this,
|
||||
methodName,
|
||||
args,
|
||||
Meteor.isServer || this._connection === null, // getAutoValues
|
||||
userId,
|
||||
Meteor.isServer // isFromTrustedCode
|
||||
);
|
||||
if (!args) {
|
||||
// doValidate already called the callback or threw the error so we're done.
|
||||
// But insert should always return an ID to match core behavior.
|
||||
return methodName === 'insert' ? this._makeNewID() : undefined;
|
||||
}
|
||||
} else {
|
||||
// We still need to adjust args because insert does not take options
|
||||
if (methodName === 'insert' && typeof args[1] !== 'function') args.splice(1, 1);
|
||||
}
|
||||
|
||||
return _super.apply(this, args);
|
||||
};
|
||||
});
|
||||
|
||||
/*
|
||||
* Private
|
||||
*/
|
||||
|
||||
function doValidate(collection, type, args, getAutoValues, userId, isFromTrustedCode) {
|
||||
let doc, callback, error, options, isUpsert, selector, last, hasCallback;
|
||||
|
||||
if (!args.length) {
|
||||
throw new Error(type + ' requires an argument');
|
||||
}
|
||||
|
||||
// Gather arguments and cache the selector
|
||||
if (type === 'insert') {
|
||||
doc = args[0];
|
||||
options = args[1];
|
||||
callback = args[2];
|
||||
|
||||
// The real insert doesn't take options
|
||||
if (typeof options === 'function') {
|
||||
args = [doc, options];
|
||||
} else if (typeof callback === 'function') {
|
||||
args = [doc, callback];
|
||||
} else {
|
||||
args = [doc];
|
||||
}
|
||||
} else if (type === 'update') {
|
||||
selector = args[0];
|
||||
doc = args[1];
|
||||
options = args[2];
|
||||
callback = args[3];
|
||||
} else {
|
||||
throw new Error('invalid type argument');
|
||||
}
|
||||
|
||||
const validatedObjectWasInitiallyEmpty = isEmpty(doc);
|
||||
|
||||
// Support missing options arg
|
||||
if (!callback && typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
last = args.length - 1;
|
||||
|
||||
hasCallback = (typeof args[last] === 'function');
|
||||
|
||||
// If update was called with upsert:true, flag as an upsert
|
||||
isUpsert = (type === 'update' && options.upsert === true);
|
||||
|
||||
// we need to pass `doc` and `options` to `simpleSchema` method, that's why
|
||||
// schema declaration moved here
|
||||
let schema = collection.simpleSchema(doc, options, selector);
|
||||
const isLocalCollection = (collection._connection === null);
|
||||
|
||||
// On the server and for local collections, we allow passing `getAutoValues: false` to disable autoValue functions
|
||||
if ((Meteor.isServer || isLocalCollection) && options.getAutoValues === false) {
|
||||
getAutoValues = false;
|
||||
}
|
||||
|
||||
// Process pick/omit options if they are present
|
||||
const picks = Array.isArray(options.pick) ? options.pick : null;
|
||||
const omits = Array.isArray(options.omit) ? options.omit : null;
|
||||
|
||||
if (picks && omits) {
|
||||
// Pick and omit cannot both be present in the options
|
||||
throw new Error('pick and omit options are mutually exclusive');
|
||||
} else if (picks) {
|
||||
schema = schema.pick(...picks);
|
||||
} else if (omits) {
|
||||
schema = schema.omit(...omits);
|
||||
}
|
||||
|
||||
// Determine validation context
|
||||
let validationContext = options.validationContext;
|
||||
if (validationContext) {
|
||||
if (typeof validationContext === 'string') {
|
||||
validationContext = schema.namedContext(validationContext);
|
||||
}
|
||||
} else {
|
||||
validationContext = schema.namedContext();
|
||||
}
|
||||
|
||||
// Add a default callback function if we're on the client and no callback was given
|
||||
/*
|
||||
if (Meteor.isClient && !callback) {
|
||||
// Client can't block, so it can't report errors by exception,
|
||||
// only by callback. If they forget the callback, give them a
|
||||
// default one that logs the error, so they aren't totally
|
||||
// baffled if their writes don't work because their database is
|
||||
// down.
|
||||
callback = function(err) {
|
||||
if (err) {
|
||||
Meteor._debug(type + " failed: " + (err.reason || err.stack));
|
||||
}
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
// If client validation is fine or is skipped but then something
|
||||
// is found to be invalid on the server, we get that error back
|
||||
// as a special Meteor.Error that we need to parse.
|
||||
if (Meteor.isClient && hasCallback) {
|
||||
callback = args[last] = wrapCallbackForParsingServerErrors(validationContext, callback);
|
||||
}
|
||||
|
||||
const schemaAllowsId = schema.allowsKey('_id');
|
||||
if (type === 'insert' && !doc._id && schemaAllowsId) {
|
||||
doc._id = collection._makeNewID();
|
||||
}
|
||||
|
||||
// Get the docId for passing in the autoValue/custom context
|
||||
let docId;
|
||||
if (type === 'insert') {
|
||||
docId = doc._id; // might be undefined
|
||||
} else if (type === 'update' && selector) {
|
||||
docId = typeof selector === 'string' || selector instanceof Mongo.ObjectID ? selector : selector._id;
|
||||
}
|
||||
|
||||
// If _id has already been added, remove it temporarily if it's
|
||||
// not explicitly defined in the schema.
|
||||
let cachedId;
|
||||
if (doc._id && !schemaAllowsId) {
|
||||
cachedId = doc._id;
|
||||
delete doc._id;
|
||||
}
|
||||
|
||||
const autoValueContext = {
|
||||
isInsert: (type === 'insert'),
|
||||
isUpdate: (type === 'update' && options.upsert !== true),
|
||||
isUpsert,
|
||||
userId,
|
||||
isFromTrustedCode,
|
||||
docId,
|
||||
isLocalCollection
|
||||
};
|
||||
|
||||
const extendAutoValueContext = {
|
||||
...((schema._cleanOptions || {}).extendAutoValueContext || {}),
|
||||
...autoValueContext,
|
||||
...options.extendAutoValueContext,
|
||||
};
|
||||
|
||||
const cleanOptionsForThisOperation = {};
|
||||
['autoConvert', 'filter', 'removeEmptyStrings', 'removeNullsFromArrays', 'trimStrings'].forEach(prop => {
|
||||
if (typeof options[prop] === 'boolean') {
|
||||
cleanOptionsForThisOperation[prop] = options[prop];
|
||||
}
|
||||
});
|
||||
|
||||
// Preliminary cleaning on both client and server. On the server and for local
|
||||
// collections, automatic values will also be set at this point.
|
||||
schema.clean(doc, {
|
||||
mutate: true, // Clean the doc/modifier in place
|
||||
isModifier: (type !== 'insert'),
|
||||
// Start with some Collection2 defaults, which will usually be overwritten
|
||||
...Collection2.cleanOptions,
|
||||
// The extend with the schema-level defaults (from SimpleSchema constructor options)
|
||||
...(schema._cleanOptions || {}),
|
||||
// Finally, options for this specific operation should take precedence
|
||||
...cleanOptionsForThisOperation,
|
||||
extendAutoValueContext, // This was extended separately above
|
||||
getAutoValues, // Force this override
|
||||
});
|
||||
|
||||
// We clone before validating because in some cases we need to adjust the
|
||||
// object a bit before validating it. If we adjusted `doc` itself, our
|
||||
// changes would persist into the database.
|
||||
let docToValidate = {};
|
||||
for (var prop in doc) {
|
||||
// We omit prototype properties when cloning because they will not be valid
|
||||
// and mongo omits them when saving to the database anyway.
|
||||
if (Object.prototype.hasOwnProperty.call(doc, prop)) {
|
||||
docToValidate[prop] = doc[prop];
|
||||
}
|
||||
}
|
||||
|
||||
// On the server, upserts are possible; SimpleSchema handles upserts pretty
|
||||
// well by default, but it will not know about the fields in the selector,
|
||||
// which are also stored in the database if an insert is performed. So we
|
||||
// will allow these fields to be considered for validation by adding them
|
||||
// to the $set in the modifier, while stripping out query selectors as these
|
||||
// don't make it into the upserted document and break validation.
|
||||
// This is no doubt prone to errors, but there probably isn't any better way
|
||||
// right now.
|
||||
if (Meteor.isServer && isUpsert && isObject(selector)) {
|
||||
const set = docToValidate.$set || {};
|
||||
docToValidate.$set = flattenSelector(selector);
|
||||
|
||||
if (!schemaAllowsId) delete docToValidate.$set._id;
|
||||
Object.assign(docToValidate.$set, set);
|
||||
}
|
||||
// Set automatic values for validation on the client.
|
||||
// On the server, we already updated doc with auto values, but on the client,
|
||||
// we will add them to docToValidate for validation purposes only.
|
||||
// This is because we want all actual values generated on the server.
|
||||
if (Meteor.isClient && !isLocalCollection) {
|
||||
schema.clean(docToValidate, {
|
||||
autoConvert: false,
|
||||
extendAutoValueContext,
|
||||
filter: false,
|
||||
getAutoValues: true,
|
||||
isModifier: (type !== 'insert'),
|
||||
mutate: true, // Clean the doc/modifier in place
|
||||
removeEmptyStrings: false,
|
||||
removeNullsFromArrays: false,
|
||||
trimStrings: false,
|
||||
});
|
||||
}
|
||||
|
||||
// XXX Maybe move this into SimpleSchema
|
||||
if (!validatedObjectWasInitiallyEmpty && isEmpty(docToValidate)) {
|
||||
throw new Error('After filtering out keys not in the schema, your ' +
|
||||
(type === 'update' ? 'modifier' : 'object') +
|
||||
' is now empty');
|
||||
}
|
||||
|
||||
// Validate doc
|
||||
let isValid;
|
||||
if (options.validate === false) {
|
||||
isValid = true;
|
||||
} else {
|
||||
isValid = validationContext.validate(docToValidate, {
|
||||
modifier: (type === 'update' || type === 'upsert'),
|
||||
upsert: isUpsert,
|
||||
extendedCustomContext: {
|
||||
isInsert: (type === 'insert'),
|
||||
isUpdate: (type === 'update' && options.upsert !== true),
|
||||
isUpsert,
|
||||
userId,
|
||||
isFromTrustedCode,
|
||||
docId,
|
||||
isLocalCollection,
|
||||
...(options.extendedCustomContext || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// Add the ID back
|
||||
if (cachedId) {
|
||||
doc._id = cachedId;
|
||||
}
|
||||
|
||||
// Update the args to reflect the cleaned doc
|
||||
// XXX not sure this is necessary since we mutate
|
||||
if (type === 'insert') {
|
||||
args[0] = doc;
|
||||
} else {
|
||||
args[1] = doc;
|
||||
}
|
||||
|
||||
// If callback, set invalidKey when we get a mongo unique error
|
||||
if (Meteor.isServer && hasCallback) {
|
||||
args[last] = wrapCallbackForParsingMongoValidationErrors(validationContext, args[last]);
|
||||
}
|
||||
|
||||
return args;
|
||||
} else {
|
||||
error = getErrorObject(validationContext, Meteor.settings?.packages?.collection2?.disableCollectionNamesInValidation ? '' : `in ${collection._name} ${type}`);
|
||||
if (callback) {
|
||||
// insert/update/upsert pass `false` when there's an error, so we do that
|
||||
callback(error, false);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorObject(context, appendToMessage = '') {
|
||||
let message;
|
||||
const invalidKeys = (typeof context.validationErrors === 'function') ? context.validationErrors() : context.invalidKeys();
|
||||
if (invalidKeys.length) {
|
||||
const firstErrorKey = invalidKeys[0].name;
|
||||
const firstErrorMessage = context.keyErrorMessage(firstErrorKey);
|
||||
|
||||
// If the error is in a nested key, add the full key to the error message
|
||||
// to be more helpful.
|
||||
if (firstErrorKey.indexOf('.') === -1) {
|
||||
message = firstErrorMessage;
|
||||
} else {
|
||||
message = `${firstErrorMessage} (${firstErrorKey})`;
|
||||
}
|
||||
} else {
|
||||
message = 'Failed validation';
|
||||
}
|
||||
message = `${message} ${appendToMessage}`.trim();
|
||||
const error = new Error(message);
|
||||
error.invalidKeys = invalidKeys;
|
||||
error.validationContext = context;
|
||||
// If on the server, we add a sanitized error, too, in case we're
|
||||
// called from a method.
|
||||
if (Meteor.isServer) {
|
||||
error.sanitizedError = new Meteor.Error(400, message, EJSON.stringify(error.invalidKeys));
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
function addUniqueError(context, errorMessage) {
|
||||
const name = errorMessage.split('c2_')[1].split(' ')[0];
|
||||
const val = errorMessage.split('dup key:')[1].split('"')[1];
|
||||
|
||||
const addValidationErrorsPropName = (typeof context.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys';
|
||||
context[addValidationErrorsPropName]([{
|
||||
name: name,
|
||||
type: 'notUnique',
|
||||
value: val
|
||||
}]);
|
||||
}
|
||||
|
||||
function wrapCallbackForParsingMongoValidationErrors(validationContext, cb) {
|
||||
return function wrappedCallbackForParsingMongoValidationErrors(...args) {
|
||||
const error = args[0];
|
||||
if (error &&
|
||||
((error.name === 'MongoError' && error.code === 11001) || error.message.indexOf('MongoError: E11000') !== -1) &&
|
||||
error.message.indexOf('c2_') !== -1) {
|
||||
addUniqueError(validationContext, error.message);
|
||||
args[0] = getErrorObject(validationContext);
|
||||
}
|
||||
return cb.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
function wrapCallbackForParsingServerErrors(validationContext, cb) {
|
||||
const addValidationErrorsPropName = (typeof validationContext.addValidationErrors === 'function') ? 'addValidationErrors' : 'addInvalidKeys';
|
||||
return function wrappedCallbackForParsingServerErrors(...args) {
|
||||
const error = args[0];
|
||||
// Handle our own validation errors
|
||||
if (error instanceof Meteor.Error &&
|
||||
error.error === 400 &&
|
||||
error.reason === 'INVALID' &&
|
||||
typeof error.details === 'string') {
|
||||
const invalidKeysFromServer = EJSON.parse(error.details);
|
||||
validationContext[addValidationErrorsPropName](invalidKeysFromServer);
|
||||
args[0] = getErrorObject(validationContext);
|
||||
}
|
||||
// Handle Mongo unique index errors, which are forwarded to the client as 409 errors
|
||||
else if (error instanceof Meteor.Error &&
|
||||
error.error === 409 &&
|
||||
error.reason &&
|
||||
error.reason.indexOf('E11000') !== -1 &&
|
||||
error.reason.indexOf('c2_') !== -1) {
|
||||
addUniqueError(validationContext, error.reason);
|
||||
args[0] = getErrorObject(validationContext);
|
||||
}
|
||||
return cb.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
let alreadyInsecure = {};
|
||||
function keepInsecure(c) {
|
||||
// If insecure package is in use, we need to add allow rules that return
|
||||
// true. Otherwise, it would seemingly turn off insecure mode.
|
||||
if (Package && Package.insecure && !alreadyInsecure[c._name]) {
|
||||
c.allow({
|
||||
insert: function() {
|
||||
return true;
|
||||
},
|
||||
update: function() {
|
||||
return true;
|
||||
},
|
||||
remove: function () {
|
||||
return true;
|
||||
},
|
||||
fetch: [],
|
||||
transform: null
|
||||
});
|
||||
alreadyInsecure[c._name] = true;
|
||||
}
|
||||
// If insecure package is NOT in use, then adding the two deny functions
|
||||
// does not have any effect on the main app's security paradigm. The
|
||||
// user will still be required to add at least one allow function of her
|
||||
// own for each operation for this collection. And the user may still add
|
||||
// additional deny functions, but does not have to.
|
||||
}
|
||||
|
||||
let alreadyDefined = {};
|
||||
function defineDeny(c, options) {
|
||||
if (!alreadyDefined[c._name]) {
|
||||
|
||||
const isLocalCollection = (c._connection === null);
|
||||
|
||||
// First define deny functions to extend doc with the results of clean
|
||||
// and auto-values. This must be done with "transform: null" or we would be
|
||||
// extending a clone of doc and therefore have no effect.
|
||||
c.deny({
|
||||
insert: function(userId, doc) {
|
||||
// Referenced doc is cleaned in place
|
||||
c.simpleSchema(doc).clean(doc, {
|
||||
mutate: true,
|
||||
isModifier: false,
|
||||
// We don't do these here because they are done on the client if desired
|
||||
filter: false,
|
||||
autoConvert: false,
|
||||
removeEmptyStrings: false,
|
||||
trimStrings: false,
|
||||
extendAutoValueContext: {
|
||||
isInsert: true,
|
||||
isUpdate: false,
|
||||
isUpsert: false,
|
||||
userId: userId,
|
||||
isFromTrustedCode: false,
|
||||
docId: doc._id,
|
||||
isLocalCollection: isLocalCollection
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
// Referenced modifier is cleaned in place
|
||||
c.simpleSchema(modifier).clean(modifier, {
|
||||
mutate: true,
|
||||
isModifier: true,
|
||||
// We don't do these here because they are done on the client if desired
|
||||
filter: false,
|
||||
autoConvert: false,
|
||||
removeEmptyStrings: false,
|
||||
trimStrings: false,
|
||||
extendAutoValueContext: {
|
||||
isInsert: false,
|
||||
isUpdate: true,
|
||||
isUpsert: false,
|
||||
userId: userId,
|
||||
isFromTrustedCode: false,
|
||||
docId: doc && doc._id,
|
||||
isLocalCollection: isLocalCollection
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
fetch: ['_id'],
|
||||
transform: null
|
||||
});
|
||||
|
||||
// Second define deny functions to validate again on the server
|
||||
// for client-initiated inserts and updates. These should be
|
||||
// called after the clean/auto-value functions since we're adding
|
||||
// them after. These must *not* have "transform: null" if options.transform is true because
|
||||
// we need to pass the doc through any transforms to be sure
|
||||
// that custom types are properly recognized for type validation.
|
||||
c.deny({
|
||||
insert: function(userId, doc) {
|
||||
// We pass the false options because we will have done them on client if desired
|
||||
doValidate(
|
||||
c,
|
||||
'insert',
|
||||
[
|
||||
doc,
|
||||
{
|
||||
trimStrings: false,
|
||||
removeEmptyStrings: false,
|
||||
filter: false,
|
||||
autoConvert: false
|
||||
},
|
||||
function(error) {
|
||||
if (error) {
|
||||
throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys));
|
||||
}
|
||||
}
|
||||
],
|
||||
false, // getAutoValues
|
||||
userId,
|
||||
false // isFromTrustedCode
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
// NOTE: This will never be an upsert because client-side upserts
|
||||
// are not allowed once you define allow/deny functions.
|
||||
// We pass the false options because we will have done them on client if desired
|
||||
doValidate(
|
||||
c,
|
||||
'update',
|
||||
[
|
||||
{_id: doc && doc._id},
|
||||
modifier,
|
||||
{
|
||||
trimStrings: false,
|
||||
removeEmptyStrings: false,
|
||||
filter: false,
|
||||
autoConvert: false
|
||||
},
|
||||
function(error) {
|
||||
if (error) {
|
||||
throw new Meteor.Error(400, 'INVALID', EJSON.stringify(error.invalidKeys));
|
||||
}
|
||||
}
|
||||
],
|
||||
false, // getAutoValues
|
||||
userId,
|
||||
false // isFromTrustedCode
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
fetch: ['_id'],
|
||||
...(options.transform === true ? {} : {transform: null}),
|
||||
});
|
||||
|
||||
// note that we've already done this collection so that we don't do it again
|
||||
// if attachSchema is called again
|
||||
alreadyDefined[c._name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function extendSchema(s1, s2) {
|
||||
if (s2.version >= 2) {
|
||||
const ss = new SimpleSchema(s1);
|
||||
ss.extend(s2);
|
||||
return ss;
|
||||
} else {
|
||||
return new SimpleSchema([ s1, s2 ]);
|
||||
}
|
||||
}
|
||||
|
||||
export default Collection2;
|
||||
31
app/packages/collection2/lib.js
Normal file
31
app/packages/collection2/lib.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export function flattenSelector(selector) {
|
||||
// If selector uses $and format, convert to plain object selector
|
||||
if (Array.isArray(selector.$and)) {
|
||||
selector.$and.forEach(sel => {
|
||||
Object.assign(selector, flattenSelector(sel));
|
||||
});
|
||||
|
||||
delete selector.$and
|
||||
}
|
||||
|
||||
const obj = {}
|
||||
|
||||
Object.entries(selector).forEach(([key, value]) => {
|
||||
// Ignoring logical selectors (https://docs.mongodb.com/manual/reference/operator/query/#logical)
|
||||
if (!key.startsWith("$")) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (value.$eq !== undefined) {
|
||||
obj[key] = value.$eq
|
||||
} else if (Array.isArray(value.$in) && value.$in.length === 1) {
|
||||
obj[key] = value.$in[0]
|
||||
} else if (Object.keys(value).every(v => !(typeof v === "string" && v.startsWith("$")))) {
|
||||
obj[key] = value
|
||||
}
|
||||
} else {
|
||||
obj[key] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
33
app/packages/collection2/package.js
Normal file
33
app/packages/collection2/package.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* global Package */
|
||||
|
||||
Package.describe({
|
||||
name: "aldeed:collection2",
|
||||
summary: "Automatic validation of Meteor Mongo insert and update operations on the client and server",
|
||||
version: "3.5.0",
|
||||
documentation: "../../README.md",
|
||||
git: "https://github.com/aldeed/meteor-collection2.git"
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
'lodash.isempty': '4.4.0',
|
||||
'lodash.isequal': '4.5.0',
|
||||
'lodash.isobject': '3.0.2',
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.versionsFrom(['1.12.1', '2.3']);
|
||||
api.use('mongo');
|
||||
api.imply('mongo');
|
||||
api.use('minimongo');
|
||||
api.use('ejson');
|
||||
api.use('raix:eventemitter@1.0.0');
|
||||
api.use('ecmascript');
|
||||
api.use('tmeasday:check-npm-versions@1.0.2');
|
||||
|
||||
// Allow us to detect 'insecure'.
|
||||
api.use('insecure@1.0.7', {weak: true});
|
||||
|
||||
api.mainModule('collection2.js');
|
||||
|
||||
api.export('Collection2');
|
||||
});
|
||||
Reference in New Issue
Block a user