UNPKG

9.21 kBJavaScriptView Raw
1// Load modules
2
3var Hoek = require('hoek');
4var Boom = require('boom');
5
6
7
8// Declare internals
9
10var internals = {};
11
12
13exports = module.exports = internals.Store = function (document) {
14
15 this.load(document || {});
16};
17
18
19internals.Store.prototype.load = function (document) {
20
21 var err = internals.Store.validate(document);
22 Hoek.assert(!err, err);
23
24 this._tree = Hoek.clone(document);
25};
26
27
28// Get a filtered value
29
30internals.Store.prototype.get = function (key, criteria, applied) {
31
32 var node = this._get(key, criteria, applied);
33 return internals.walk(node, criteria, applied);
34};
35
36
37internals.Store.prototype._get = function (key, criteria, applied) {
38
39 var self = this;
40
41 criteria = criteria || {};
42
43 var path = [];
44 if (key !== '/') {
45 var invalid = key.replace(/\/(\w+)/g, function ($0, $1) {
46
47 path.push($1);
48 return '';
49 });
50
51 if (invalid) {
52 return undefined;
53 }
54 }
55
56 var node = internals.filter(self._tree, criteria, applied);
57 for (var i = 0, il = path.length; i < il && node; ++i) {
58 if (typeof node !== 'object') {
59 node = undefined;
60 break;
61 }
62
63 node = internals.filter(node[path[i]], criteria, applied);
64 }
65
66 return node;
67};
68
69
70// Get a meta for node
71
72internals.Store.prototype.meta = function (key, criteria) {
73
74 var node = this._get(key, criteria);
75 return (typeof node === 'object' ? node.$meta : undefined);
76};
77
78
79// Return node or value if no filter, otherwise apply filters until node or value
80
81internals.filter = function (node, criteria, applied) {
82
83 if (!node ||
84 typeof node !== 'object' ||
85 (!node.$filter && !node.$value)) {
86
87 return node;
88 }
89
90 if (node.$value) {
91 return internals.filter(node.$value, criteria, applied);
92 }
93
94 // Filter
95
96 var filter = node.$filter;
97 var criterion = Hoek.reach(criteria, filter);
98
99 if (criterion !== undefined) {
100 if (node.$range) {
101 for (var i = 0, il = node.$range.length; i < il; ++i) {
102 if (criterion <= node.$range[i].limit) {
103 exports._logApplied(applied, filter, node, node.$range[i]);
104 return internals.filter(node.$range[i].value, criteria, applied);
105 }
106 }
107 }
108 else if (node[criterion] !== undefined) {
109 exports._logApplied(applied, filter, node, criterion);
110 return internals.filter(node[criterion], criteria, applied);
111 }
112
113 // Falls-through for $default
114 }
115
116 if (node.hasOwnProperty('$default')) {
117 exports._logApplied(applied, filter, node, '$default');
118 return internals.filter(node.$default, criteria, applied);
119 }
120
121 exports._logApplied(applied, filter, node);
122 return undefined;
123};
124
125
126// Exported to make testing easier
127exports._logApplied = function (applied, filter, node, criterion) {
128
129 if (!applied) {
130 return;
131 }
132
133 var record = {
134 filter: filter
135 };
136
137 if (criterion) {
138 if (typeof criterion === 'object') {
139 if (criterion.id) {
140 record.valueId = criterion.id;
141 }
142 else {
143 record.valueId = (typeof criterion.value === 'object' ? '[object]' : criterion.value.toString());
144 }
145 }
146 else {
147 record.valueId = criterion.toString();
148 }
149 }
150
151 if (node && node.$id) {
152 record.filterId = node.$id;
153 }
154
155 applied.push(record);
156};
157
158
159// Applies criteria on an entire tree
160
161internals.walk = function (node, criteria, applied) {
162
163 if (!node ||
164 typeof node !== 'object') {
165
166 return node;
167 }
168
169 if (node.hasOwnProperty('$value')) {
170 return internals.walk(node.$value, criteria, applied);
171 }
172
173 var parent = (node instanceof Array ? [] : {});
174
175 var keys = Object.keys(node);
176 for (var i = 0, il = keys.length; i < il; ++i) {
177 var key = keys[i];
178 if (key === '$meta' || key === '$id') {
179 continue;
180 }
181 var child = internals.filter(node[key], criteria, applied);
182 var value = internals.walk(child, criteria, applied);
183 if (value !== undefined) {
184 parent[key] = value;
185 }
186 }
187
188 return parent;
189};
190
191
192// Validate tree structure
193
194internals.Store.validate = function (node, path) {
195
196 path = path || '';
197
198 var error = function (reason) {
199
200 var e = Boom.badRequest(reason);
201 e.path = path || '/';
202 return e;
203 };
204
205 // Valid value
206
207 if (node === null ||
208 node === undefined ||
209 typeof node !== 'object') {
210 return null;
211 }
212
213 // Invalid object
214
215 if (node instanceof Error ||
216 node instanceof Date ||
217 node instanceof RegExp) {
218
219 return error('Invalid node object type');
220 }
221
222 // Invalid keys
223
224 var found = {};
225 var keys = Object.keys(node);
226 for (var i = 0, il = keys.length; i < il; ++i) {
227 var key = keys[i];
228 if (key[0] === '$') {
229 if (key === '$filter') {
230 found.filter = true;
231 var filter = node[key];
232 if (!filter) {
233 return error('Invalid empty filter value');
234 }
235
236 if (typeof filter !== 'string') {
237 return error('Filter value must be a string');
238 }
239
240 if (!filter.match(/^\w+(?:\.\w+)*$/)) {
241 return error('Invalid filter value ' + node[key]);
242 }
243 }
244 else if (key === '$range') {
245 found.range = true;
246 if (node.$range instanceof Array === false) {
247 return error('Range value must be an array');
248 }
249
250 if (!node.$range.length) {
251 return error('Range must include at least one value');
252 }
253
254 var lastLimit = undefined;
255 for (var r = 0, rl = node.$range.length; r < rl; ++r) {
256 var range = node.$range[r];
257 if (typeof range !== 'object') {
258 return error('Invalid range entry type');
259 }
260
261 if (!range.hasOwnProperty('limit')) {
262 return error('Range entry missing limit');
263 }
264
265 if (typeof range.limit !== 'number') {
266 return error('Range limit must be a number');
267 }
268
269 if (lastLimit !== undefined && range.limit <= lastLimit) {
270 return error('Range entries not sorted in ascending order - ' + range.limit + ' cannot come after ' + lastLimit);
271 }
272
273 lastLimit = range.limit;
274
275 if (!range.hasOwnProperty('value')) {
276 return error('Range entry missing value');
277 }
278
279 var err = internals.Store.validate(range.value, path + '/$range[' + range.limit + ']');
280 if (err) {
281 return err;
282 }
283 }
284 }
285 else if (key === '$default') {
286 found.default = true;
287 var err = internals.Store.validate(node.$default, path + '/$default');
288 if (err) {
289 return err;
290 }
291 }
292 else if (key === '$meta') {
293 found.meta = true;
294 }
295 else if (key === '$id') {
296 if (!node.$id ||
297 typeof node.$id !== 'string') {
298
299 return error('Id value must be a non-empty string');
300 }
301
302 found.id = true;
303 }
304 else if (key === '$value') {
305 found.value = true;
306 var err = internals.Store.validate(node.$value, path + '/$value');
307 if (err) {
308 return err;
309 }
310 }
311 else {
312 return error('Unknown $ directive ' + key);
313 }
314 }
315 else {
316 found.key = true;
317 var value = node[key];
318 var err = internals.Store.validate(value, path + '/' + key);
319 if (err) {
320 return err;
321 }
322 }
323 }
324
325 // Invalid directive combination
326 if (found.value && (found.key || found.range || found.default || found.filter)) {
327 return error('Value directive can only be used with meta or nothing');
328 }
329
330 if (found.default && !found.filter) {
331 return error('Default value without a filter');
332 }
333
334 if (found.filter && !found.default && !found.key && !found.range) {
335 return error('Filter without any values');
336 }
337
338 if (found.filter && found.default && !found.key && !found.range) {
339 return error('Filter with only a default');
340 }
341
342 if (found.range && !found.filter) {
343 return error('Range without a filter');
344 }
345
346 if (found.range && found.key) {
347 return error('Range with non-ranged values');
348 }
349
350 // Valid node
351
352 return null;
353};