1 | import {escapeKey, unescapeKey, makePathMatcher} from './utils/paths.js';
|
2 |
|
3 | import _ from 'lodash';
|
4 |
|
5 |
|
6 |
|
7 | const EMPTY_ANNOTATIONS = {};
|
8 | Object.freeze(EMPTY_ANNOTATIONS);
|
9 |
|
10 |
|
11 | export 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 |
|
97 | export 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 |
|
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 |
|
166 | export 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);}
|
174 | get value() {return this._tree.getObject(this.path);}
|
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 |
|
210 | export default Reference;
|