UNPKG

5.63 kBJavaScriptView Raw
1// @flow
2
3import type { Subrequest, SubrequestsTree } from '../types/BlueprintManager';
4
5const _ = require('lodash');
6const Ajv = require('ajv');
7const uuid = require('uuid').v4;
8const blueprintSchema = require('../schema.json');
9
10// Compile the schema in the global scope so we can avoid multiple computations.
11const ajv = new Ajv({ allErrors: true });
12const validate = ajv.compile(blueprintSchema);
13
14/**
15 * @classdesc
16 * The BlueprintManager parses a tree of requests and validates it.
17 *
18 * @class BlueprintManager
19 */
20module.exports = class BlueprintManager {
21 /**
22 * Parses the input data and produces a blueprint tree.
23 *
24 * @param {string} text
25 * The text received in the request.
26 *
27 * @throws {Error}
28 * Throws an error when the user input is not valid.
29 *
30 * @return {Object[]}
31 * The linked list
32 */
33 static parse(text: string): SubrequestsTree {
34 const input = JSON.parse(text);
35 this.validateInput(input);
36 const parsed: Array<Subrequest> = input.map(this.fillDefaults);
37
38 return this.isValidTree(parsed)
39 // Find the execution sequences.
40 ? this.buildExecutionSequence(parsed)
41 : [];
42 }
43
44 /**
45 * Fill the defaults.
46 *
47 * @param {Object} rawItem
48 * The object to turn into a Subrequest.
49 *
50 * @return {Subrequest}
51 * The complete Subrequest.
52 */
53 static fillDefaults(rawItem: Object): Subrequest {
54 rawItem.requestId = rawItem.requestId || uuid();
55 if (typeof rawItem.body !== 'undefined') {
56 rawItem.body = JSON.parse(rawItem.body);
57 }
58 const headersObject = rawItem.headers || {};
59 rawItem.headers = Object.keys(headersObject).reduce((carry, key) => {
60 carry.set(key, headersObject[key]);
61 return carry;
62 }, new Map());
63 rawItem.waitFor = rawItem.waitFor || ['<ROOT>'];
64 rawItem._resolved = false;
65 // Detect if there is an encoded token. If so, then decode the URI.
66 if (
67 rawItem.uri
68 && rawItem.uri.indexOf('%7B%7B') !== -1
69 && rawItem.uri.indexOf('%7D%7D') !== -1
70 ) {
71 rawItem.uri = decodeURIComponent(rawItem.uri);
72 }
73
74 return rawItem;
75 }
76
77 /**
78 * Builds the execution sequence.
79 *
80 * Builds an array where each position contains the IDs of the requests to be
81 * executed. All the IDs in the same position in the sequence can be executed
82 * in parallel.
83 *
84 * @param {Subrequest[]} parsed
85 * The parsed requests.
86 *
87 * @return {SubrequestsTree}
88 * The sequence of IDs grouped by execution order.
89 */
90 static buildExecutionSequence(parsed: Array<Subrequest>): SubrequestsTree {
91 const sequence: SubrequestsTree = [
92 parsed.filter(({ waitFor }) => _.difference(waitFor, ['<ROOT>']).length === 0),
93 ];
94 let subreqsWithUnresolvedDeps = parsed
95 .filter(({ waitFor }) => _.difference(waitFor, ['<ROOT>']).length !== 0);
96 // Checks if a subrequest has its dependency resolved.
97 // const dependencyIsResolved = ({ waitFor }) => sequence[sequencePosition]
98 // .some(({ requestId }) => requestId === waitFor);
99 const dependencyIsResolved = ({ waitFor }, seq) => _.difference(
100 waitFor,
101 this._allSubrequestIds(seq)
102 ).length === 0;
103 while (subreqsWithUnresolvedDeps && subreqsWithUnresolvedDeps.length) {
104 const noDeps = subreqsWithUnresolvedDeps.filter(sub => dependencyIsResolved(sub, sequence));
105 if (noDeps.length === 0) {
106 throw new Error('Waiting for unresolvable request. Abort.');
107 }
108 sequence.push(noDeps);
109 subreqsWithUnresolvedDeps = _.difference(subreqsWithUnresolvedDeps, noDeps);
110 }
111 return sequence;
112 }
113
114 /**
115 * Calculates all the subrequest IDs present in a tree.
116 *
117 * @param {SubrequestsTree} sequence
118 * The subrequests in the tree.
119 *
120 * @return {string[]}
121 * The list of subrequest IDs.
122 *
123 * @private
124 */
125 static _allSubrequestIds(sequence: SubrequestsTree): Array<string> {
126 const output = new Set(['<ROOT>']);
127 sequence.forEach(subrequests => subrequests
128 .forEach(subrequest => output.add(subrequest.requestId)));
129 return Array.from(output);
130 }
131
132 /**
133 * Validates the user input.
134 *
135 * @param {Object[]} parsed
136 * The collection of input subrequests to validate.
137 *
138 * @throws {Error}
139 * Throws an error if the input is not valid.
140 *
141 * @return {void}
142 */
143 static validateInput(parsed: Array<Object>): void {
144 const valid = validate(parsed);
145 if (!valid) {
146 const errors = JSON.stringify(validate.errors, null, 2);
147 throw new Error(`The provided blueprint is not valid: ${errors}.`);
148 }
149 }
150
151 /**
152 * Validates the tree.
153 *
154 * @param {Subrequest[]} parsed
155 * The collection of input subrequests to validate.
156 *
157 * @return {boolean}
158 * True if the collection is valid. False otherwise.
159 */
160 static isValidTree(parsed: Array<Subrequest>): boolean {
161 // Even if the type says this is a valid tree we need to make sure the user
162 // input is correct.
163 const isValidItem = (item: Subrequest): boolean => ([
164 'requestId',
165 'waitFor',
166 'uri',
167 'action',
168 'headers',
169 ].reduce((all, key) => all
170 && (typeof item[key] !== 'undefined')
171 && typeof item.requestId === 'string'
172 && Array.isArray(item.waitFor)
173 && typeof item.uri === 'string'
174 && typeof item.headers === 'object'
175 && (typeof item.body === 'undefined' || typeof item.body === 'object')
176 && ['view', 'create', 'update', 'replace', 'delete', 'exists', 'discover']
177 .indexOf(item.action) !== -1, true));
178 return parsed.reduce((valid, rawItem) => valid && isValidItem(rawItem), true);
179 }
180};