1 |
|
2 |
|
3 | var Hoek = require('hoek');
|
4 | var Boom = require('boom');
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | var internals = {};
|
11 |
|
12 |
|
13 | exports = module.exports = internals.Store = function (document) {
|
14 |
|
15 | this.load(document || {});
|
16 | };
|
17 |
|
18 |
|
19 | internals.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 |
|
29 |
|
30 | internals.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 |
|
37 | internals.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 |
|
71 |
|
72 | internals.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 |
|
80 |
|
81 | internals.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 |
|
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 |
|
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 |
|
127 | exports._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 |
|
160 |
|
161 | internals.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 |
|
193 |
|
194 | internals.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 |
|
206 |
|
207 | if (node === null ||
|
208 | node === undefined ||
|
209 | typeof node !== 'object') {
|
210 | return null;
|
211 | }
|
212 |
|
213 |
|
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 |
|
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 |
|
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 |
|
351 |
|
352 | return null;
|
353 | };
|