Merge branch 'version-2' into version-2-dev

This commit is contained in:
Stefan Zermatten
2022-04-08 17:59:20 +02:00
50 changed files with 1666 additions and 289 deletions

View File

@@ -133,6 +133,7 @@ let CreatureSchema = new SimpleSchema({
'computeErrors.$.details' : {
type: Object,
blackbox: true,
optional: true,
},
// Tabletop

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [

View File

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

View File

@@ -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',

View File

@@ -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: {

View File

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

View File

@@ -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({

View File

@@ -1,4 +1,9 @@
import SimpleSchema from 'simpl-schema';
import { set } from 'lodash';
set(Meteor.settings,
'packages.collection2.disableCollectionNamesInValidation',
true);
SimpleSchema.extendOptions([
'parseLevel',

View File

@@ -1,2 +1,3 @@
import './migrateTo.js';
import './validateDatabase.js';
import './getVersion.js';

View File

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

View 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;

View 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;
});
}

View File

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

View File

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

View File

@@ -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 = '';
},
}

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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(){

View File

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

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
node_modules

View 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.

View 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="
}
}
}

View 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

View 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;

View 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
}

View 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');
});