jinaga
Version:
Data management for web and mobile applications.
239 lines • 11.5 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DistributionEngine = void 0;
const description_1 = require("../specification/description");
const skeleton_1 = require("../specification/skeleton");
const specification_1 = require("../specification/specification");
const storage_1 = require("../storage");
class DistributionEngine {
constructor(distributionRules, store) {
this.distributionRules = distributionRules;
this.store = store;
}
canDistributeToAll(targetFeeds, namedStart, user) {
return __awaiter(this, void 0, void 0, function* () {
// TODO: Minimize the number hits to the database.
const reasons = [];
for (const targetFeed of targetFeeds) {
const feedResult = yield this.canDistributeTo(targetFeed, namedStart, user);
if (feedResult.type === 'failure') {
reasons.push(feedResult.reason);
}
}
if (reasons.length > 0) {
return {
type: 'failure',
reason: reasons.join('\n\n')
};
}
else {
return {
type: 'success'
};
}
});
}
canDistributeTo(targetFeed, namedStart, user) {
return __awaiter(this, void 0, void 0, function* () {
const start = targetFeed.given.map(g => namedStart[g.name]);
const targetSkeleton = (0, skeleton_1.skeletonOfSpecification)(targetFeed);
const reasons = [];
for (const rule of this.distributionRules.rules) {
for (const ruleFeed of rule.feeds) {
const ruleSkeleton = (0, skeleton_1.skeletonOfSpecification)(ruleFeed);
const permutations = permutationsOf(start, ruleSkeleton, targetSkeleton);
for (const permutation of permutations) {
if (skeletonsEqual(ruleSkeleton, targetSkeleton)) {
// If this rule applies to any user, then we can distribute.
if (rule.user === null) {
return {
type: 'success'
};
}
// If there is no user logged in, then we cannot distribute.
if (user === null) {
if (reasons.length === 0) {
reasons.push(`User is not logged in.`);
}
}
else {
// The projection must be a singular label.
if (rule.user.projection.type !== 'fact') {
throw new Error('The projection must be a singular label.');
}
const label = rule.user.projection.label;
// If the user specification is deterministic, then pick the labeled given.
if ((0, specification_1.specificationIsIdentity)(rule.user)) {
const userReference = executeDeterministicSpecification(rule.user, label, permutation);
// If the user matches the given, then we can distribute to the user.
const authorized = (0, storage_1.factReferenceEquals)(user)(userReference);
if (authorized) {
return {
type: 'success'
};
}
}
else {
// Find the set of users to whom we can distribute this feed.
const users = yield this.store.read(permutation, rule.user);
const results = users.map(user => user.tuple[label]);
// If any of the results match the user, then we can distribute to the user.
const authorized = results.some((0, storage_1.factReferenceEquals)(user));
if (authorized) {
return {
type: 'success'
};
}
}
reasons.push(`The user does not match ${(0, description_1.describeSpecification)(rule.user, 0)}`);
}
}
}
}
}
if (reasons.length === 0) {
reasons.push("No rules apply to this feed.");
}
return {
type: 'failure',
reason: `Cannot distribute to ${(0, description_1.describeSpecification)(targetFeed, 0)}${reasons.join('\n')}`
};
function executeDeterministicSpecification(specification, label, permutation) {
// If the label is a given, then return the associated fact reference.
const givenIndex = specification.given.findIndex(g => g.name === label);
if (givenIndex !== -1) {
const userReference = permutation[givenIndex];
return userReference;
}
// Find the match with the unknown matching the projected label.
const match = specification.matches.find(m => m.unknown.name === label);
if (!match) {
throw new Error(`The user specification must have a match with an unknown labeled '${label}'.`);
}
// Find the right-hand side of the path condition in that match.
const referencedLabels = match.conditions
.filter(specification_1.isPathCondition)
.map(c => c.labelRight);
if (referencedLabels.length !== 1) {
throw new Error(`The user specification must have exactly one path condition with an unknown labeled '${label}'.`);
}
const referencedLabel = referencedLabels[0];
// Find the given that the match references.
const index = specification.given.findIndex(g => g.name === referencedLabel);
if (index === -1) {
throw new Error(`The user specification must have a given labeled '${label}'.`);
}
const userReference = permutation[index];
return userReference;
}
});
}
}
exports.DistributionEngine = DistributionEngine;
function skeletonsEqual(ruleSkeleton, targetSkeleton) {
// Compare the sets of facts.
if (!compareSets(ruleSkeleton.facts, targetSkeleton.facts, factsEqual)) {
return false;
}
// Do not compare the sets of inputs.
// The matching permutation has already been calculated.
// Compare the sets of edges.
if (!compareSets(ruleSkeleton.edges, targetSkeleton.edges, edgesEqual)) {
return false;
}
// Compare the sets of not-exists conditions.
if (!compareSets(ruleSkeleton.notExistsConditions, targetSkeleton.notExistsConditions, notExistsConditionsEqual)) {
return false;
}
// Do not compare the sets of outputs.
// They don't affect the rows that match the specification.
return true;
}
function factsEqual(ruleFact, targetFact) {
if (ruleFact.factType !== targetFact.factType) {
return false;
}
if (ruleFact.factIndex !== targetFact.factIndex) {
return false;
}
return true;
}
function edgesEqual(ruleEdge, targetEdge) {
if (ruleEdge.edgeIndex !== targetEdge.edgeIndex) {
return false;
}
if (ruleEdge.predecessorFactIndex !== targetEdge.predecessorFactIndex) {
return false;
}
if (ruleEdge.successorFactIndex !== targetEdge.successorFactIndex) {
return false;
}
if (ruleEdge.roleName !== targetEdge.roleName) {
return false;
}
return true;
}
function notExistsConditionsEqual(ruleCondition, targetCondition) {
if (!compareSets(ruleCondition.edges, targetCondition.edges, edgesEqual)) {
return false;
}
// Do not compare nested existential conditions.
// These are not executed while generating feeds.
return true;
}
function compareSets(a, b, equals) {
if (a.length !== b.length) {
return false;
}
for (const item of a) {
if (!b.some(bItem => equals(item, bItem))) {
return false;
}
}
return true;
}
function permutationsOf(start, ruleSkeleton, targetSkeleton) {
function permute(ruleInputs, targetInputs) {
// If there are no more rule inputs, then end the recursion.
// We have found one matching permutation.
if (ruleInputs.length === 0) {
return [[]];
}
const [ruleInput, ...remainingRuleInputs] = ruleInputs;
const ruleFact = ruleSkeleton.facts.find(f => f.factIndex === ruleInput.factIndex);
if (!ruleFact) {
throw new Error(`Rule fact index ${ruleInput.factIndex} was not found.`);
}
// Find all of the target inputs that match the first rule input.
return targetInputs.flatMap((targetInput, targetIndex) => {
const targetFact = targetSkeleton.facts.find(f => f.factIndex === targetInput.factIndex);
if (!targetFact) {
throw new Error(`Target fact index ${targetInput.factIndex} was not found.`);
}
if (targetFact.factType !== ruleFact.factType) {
return [];
}
// Remove the target input from the set of candidates.
const remainingTargetInputs = targetInputs.slice(0, targetIndex)
.concat(targetInputs.slice(targetIndex + 1));
// Recursively find all permutations of the remainder.
return permute(remainingRuleInputs, remainingTargetInputs)
.map(permutation => {
permutation[ruleInput.inputIndex] = start[targetInput.inputIndex];
return permutation;
});
});
}
const permutations = permute(ruleSkeleton.inputs, targetSkeleton.inputs);
return permutations;
}
//# sourceMappingURL=distribution-engine.js.map