UNPKG

5.62 kBJavaScriptView Raw
1//
2
3
4
5const _ = require('lodash');
6const Ajv = require('ajv');
7const blueprintSchema = require('../schema.json');
8const uuid = require('uuid').v4;
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 ) {
34 const input = JSON.parse(text);
35 this.validateInput(input);
36 const parsed = 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 ) {
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 ) {
91 const sequence = [
92 parsed.filter(({ waitFor }) => _.difference(waitFor, ['<ROOT>']).length === 0),
93 ];
94 let subreqsWithUnresolvedDeps = parsed.filter(({ waitFor }) =>
95 _.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) =>
100 _.difference(waitFor, this._allSubrequestIds(seq)).length === 0;
101 while (subreqsWithUnresolvedDeps && subreqsWithUnresolvedDeps.length) {
102 const noDeps = subreqsWithUnresolvedDeps.filter(sub =>
103 dependencyIsResolved(sub, sequence));
104 if (noDeps.length === 0) {
105 throw new Error('Waiting for unresolvable request. Abort.');
106 }
107 sequence.push(noDeps);
108 subreqsWithUnresolvedDeps = _.difference(subreqsWithUnresolvedDeps, noDeps);
109 }
110 return sequence;
111 }
112
113 /**
114 * Calculates all the subrequest IDs present in a tree.
115 *
116 * @param {SubrequestsTree} sequence
117 * The subrequests in the tree.
118 *
119 * @return {string[]}
120 * The list of subrequest IDs.
121 *
122 * @private
123 */
124 static _allSubrequestIds(sequence ) {
125 const output = new Set(['<ROOT>']);
126 sequence.forEach(subrequests => subrequests
127 .forEach(subrequest => output.add(subrequest.requestId)));
128 return Array.from(output);
129 }
130
131 /**
132 * Validates the user input.
133 *
134 * @param {Object[]} parsed
135 * The collection of input subrequests to validate.
136 *
137 * @throws {Error}
138 * Throws an error if the input is not valid.
139 *
140 * @return {void}
141 */
142 static validateInput(parsed ) {
143 const valid = validate(parsed);
144 if (!valid) {
145 const errors = JSON.stringify(validate.errors, null, 2);
146 throw new Error(`The provided blueprint is not valid: ${errors}.`);
147 }
148 }
149
150 /**
151 * Validates the tree.
152 *
153 * @param {Subrequest[]} parsed
154 * The collection of input subrequests to validate.
155 *
156 * @return {boolean}
157 * True if the collection is valid. False otherwise.
158 */
159 static isValidTree(parsed ) {
160 // Even if the type says this is a valid tree we need to make sure the user
161 // input is correct.
162 const isValidItem = (item ) => ([
163 'requestId',
164 'waitFor',
165 'uri',
166 'action',
167 'headers',
168 ].reduce((all, key) => all
169 && (typeof item[key] !== 'undefined')
170 && typeof item.requestId === 'string'
171 && Array.isArray(item.waitFor)
172 && typeof item.uri === 'string'
173 && typeof item.headers === 'object'
174 && (typeof item.body === 'undefined' || typeof item.body === 'object')
175 && ['view', 'create', 'update', 'replace', 'delete', 'exists', 'discover']
176 .indexOf(item.action) !== -1, true));
177 return parsed.reduce((valid, rawItem) => valid && isValidItem(rawItem), true);
178 }
179};