UNPKG

6.04 kBJavaScriptView Raw
1import {escapeKey, unescapeKey, makePathMatcher} from './utils/paths.js';
2
3import _ from 'lodash';
4
5/* eslint-disable no-use-before-define */
6
7const EMPTY_ANNOTATIONS = {};
8Object.freeze(EMPTY_ANNOTATIONS);
9
10
11export class Handle {
12 constructor(tree, path, annotations) {
13 this._tree = tree;
14 this._path = path.replace(/^\/*/, '/').replace(/\/$/, '') || '/';
15 if (annotations) {
16 this._annotations = annotations;
17 Object.freeze(annotations);
18 }
19 }
20
21 get $ref() {return this;}
22 get key() {
23 if (!this._key) this._key = unescapeKey(this._path.replace(/.*\//, ''));
24 return this._key;
25 }
26 get path() {return this._path;}
27 get _pathPrefix() {return this._path === '/' ? '' : this._path;}
28 get parent() {
29 return new Reference(this._tree, this._path.replace(/\/[^/]*$/, ''), this._annotations);
30 }
31
32 get annotations() {
33 return this._annotations || EMPTY_ANNOTATIONS;
34 }
35
36 child() {
37 if (!arguments.length) return this;
38 const segments = [];
39 for (const key of arguments) {
40 if (key === undefined || key === null) return;
41 segments.push(escapeKey(key));
42 }
43 return new Reference(
44 this._tree, `${this._pathPrefix}/${segments.join('/')}`,
45 this._annotations
46 );
47 }
48
49 children() {
50 if (!arguments.length) return this;
51 const escapedKeys = [];
52 for (let i = 0; i < arguments.length; i++) {
53 const arg = arguments[i];
54 if (_.isArray(arg)) {
55 const mapping = {};
56 const subPath = this._pathPrefix + (escapedKeys.length ? `/${escapedKeys.join('/')}` : '');
57 const rest = _.slice(arguments, i + 1);
58 for (const key of arg) {
59 const subRef =
60 new Reference(this._tree, `${subPath}/${escapeKey(key)}`, this._annotations);
61 const subMapping = subRef.children.apply(subRef, rest);
62 if (subMapping) mapping[key] = subMapping;
63 }
64 return mapping;
65 }
66 if (arg === undefined || arg === null) return;
67 escapedKeys.push(escapeKey(arg));
68 }
69 return new Reference(
70 this._tree, `${this._pathPrefix}/${escapedKeys.join('/')}`, this._annotations);
71 }
72
73 peek(callback) {
74 return this._tree.truss.peek(this, callback);
75 }
76
77 match(pattern) {
78 return makePathMatcher(pattern).match(this.path);
79 }
80
81 test(pattern) {
82 return makePathMatcher(pattern).test(this.path);
83 }
84
85 isEqual(that) {
86 if (!(that instanceof Handle)) return false;
87 return this._tree === that._tree && this.toString() === that.toString() &&
88 _.isEqual(this._annotations, that._annotations);
89 }
90
91 belongsTo(truss) {
92 return this._tree.truss === truss;
93 }
94}
95
96
97export class Query extends Handle {
98 constructor(tree, path, spec, annotations) {
99 super(tree, path, annotations);
100 this._spec = this._copyAndValidateSpec(spec);
101 const queryTerms = _(this._spec)
102 .map((value, key) => `${key}=${encodeURIComponent(JSON.stringify(value))}`)
103 .sortBy()
104 .join('&');
105 this._string = `${this._path}?${queryTerms}`;
106 Object.freeze(this);
107 }
108
109 // Vue-bound
110 get ready() {
111 return this._tree.isQueryReady(this);
112 }
113
114 get constraints() {
115 return this._spec;
116 }
117
118 annotate(annotations) {
119 return new Query(
120 this._tree, this._path, this._spec, _.assign({}, this._annotations, annotations));
121 }
122
123 _copyAndValidateSpec(spec) {
124 if (!spec.by) throw new Error('Query needs "by" clause: ' + JSON.stringify(spec));
125 if (('at' in spec) + ('from' in spec) + ('to' in spec) > 1) {
126 throw new Error(
127 'Query must contain at most one of "at", "from", or "to" clauses: ' + JSON.stringify(spec));
128 }
129 if (('first' in spec) + ('last' in spec) > 1) {
130 throw new Error(
131 'Query must contain at most one of "first" or "last" clauses: ' + JSON.stringify(spec));
132 }
133 if (!_.some(['at', 'from', 'to', 'first', 'last'], clause => clause in spec)) {
134 throw new Error(
135 'Query must contain at least one of "at", "from", "to", "first", or "last" clauses: ' +
136 JSON.stringify(spec));
137 }
138 spec = _.clone(spec);
139 if (spec.by !== '$key' && spec.by !== '$value') {
140 if (!(spec.by instanceof Reference)) {
141 throw new Error('Query "by" value must be a reference: ' + spec.by);
142 }
143 let childPath = spec.by.toString();
144 if (!_.startsWith(childPath, this._path)) {
145 throw new Error(
146 'Query "by" value must be a descendant of target reference: ' + spec.by);
147 }
148 childPath = childPath.slice(this._path.length).replace(/^\/?/, '');
149 if (!_.includes(childPath, '/')) {
150 throw new Error(
151 'Query "by" value must not be a direct child of target reference: ' + spec.by);
152 }
153 spec.by = childPath.replace(/.*?\//, '');
154 }
155 Object.freeze(spec);
156 return spec;
157 }
158
159
160 toString() {
161 return this._string;
162 }
163}
164
165
166export class Reference extends Handle {
167
168 constructor(tree, path, annotations) {
169 super(tree, path, annotations);
170 Object.freeze(this);
171 }
172
173 get ready() {return this._tree.isReferenceReady(this);} // Vue-bound
174 get value() {return this._tree.getObject(this.path);} // Vue-bound
175 toString() {return this._path;}
176
177 annotate(annotations) {
178 return new Reference(this._tree, this._path, _.assign({}, this._annotations, annotations));
179 }
180
181 query(spec) {
182 return new Query(this._tree, this._path, spec, this._annotations);
183 }
184
185 set(value) {
186 this._checkForUndefinedPath();
187 return this._tree.update(this, 'set', {[this.path]: value});
188 }
189
190 update(values) {
191 this._checkForUndefinedPath();
192 return this._tree.update(this, 'update', values);
193 }
194
195 override(value) {
196 this._checkForUndefinedPath();
197 return this._tree.update(this, 'override', {[this.path]: value});
198 }
199
200 commit(updateFunction) {
201 this._checkForUndefinedPath();
202 return this._tree.commit(this, updateFunction);
203 }
204
205 _checkForUndefinedPath() {
206 if (this.path === '/undefined') throw new Error('Invalid path for operation: ' + this.path);
207 }
208}
209
210export default Reference;