1 | /*!
|
2 | Kettle Core DataSource definitions - portable to browser and node.js
|
3 |
|
4 | Copyright 2012-2013 OCAD University
|
5 |
|
6 | Licensed under the New BSD license. You may not use this file except in
|
7 | compliance with this License.
|
8 |
|
9 | You may obtain a copy of the License at
|
10 | https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
|
11 | */
|
12 |
|
13 | ;
|
14 |
|
15 | var fluid = fluid || require("infusion"),
|
16 | jsonlint = jsonlint || (require && require("jsonlint")),
|
17 | kettle = fluid.registerNamespace("kettle"),
|
18 | JSON5 = JSON5 || require("json5");
|
19 |
|
20 |
|
21 | /** Some common content encodings - suitable to appear as the "encoding" subcomponent of a dataSource **/
|
22 |
|
23 | fluid.defaults("kettle.dataSource.encoding.JSON", {
|
24 | gradeNames: "fluid.component",
|
25 | invokers: {
|
26 | parse: "kettle.dataSource.parseJSON",
|
27 | render: "kettle.dataSource.stringifyJSON"
|
28 | },
|
29 | contentType: "application/json"
|
30 | });
|
31 |
|
32 | fluid.defaults("kettle.dataSource.encoding.JSON5", {
|
33 | gradeNames: "fluid.component",
|
34 | invokers: {
|
35 | parse: "kettle.dataSource.parseJSON5",
|
36 | render: "kettle.dataSource.stringifyJSON5"
|
37 | },
|
38 | contentType: "application/json5"
|
39 | });
|
40 |
|
41 | fluid.defaults("kettle.dataSource.encoding.formenc", {
|
42 | gradeNames: "fluid.component",
|
43 | invokers: {
|
44 | parse: "node.querystring.parse({arguments}.0)",
|
45 | render: "node.querystring.stringify({arguments}.0)"
|
46 | },
|
47 | contentType: "application/x-www-form-urlencoded"
|
48 | });
|
49 |
|
50 | fluid.defaults("kettle.dataSource.encoding.none", {
|
51 | gradeNames: "fluid.component",
|
52 | invokers: {
|
53 | parse: "fluid.identity",
|
54 | render: "fluid.identity"
|
55 | },
|
56 | contentType: "text/plain"
|
57 | });
|
58 |
|
59 |
|
60 | /** Definitions for parsing JSON using jsonlint to render errors **/
|
61 |
|
62 | kettle.dataSource.JSONParseErrors = [];
|
63 |
|
64 | kettle.dataSource.accumulateJSONError = function (str, hash) {
|
65 | var error = "JSON parse error at line " + hash.loc.first_line + ", col " + hash.loc.last_column + ", found: \'" + hash.token + "\' - expected: " + hash.expected.join(", ");
|
66 | kettle.dataSource.JSONParseErrors.push(error);
|
67 | };
|
68 |
|
69 | // Adapt to shitty integration model of JISON-based parsers - beware that any other user of this module will find it permanently corrupted
|
70 | // TODO: Unfortunately the parser has no error recovery states - this can only ever accumulate a single error
|
71 | jsonlint.parser.parseError = jsonlint.parser.lexer.parseError = kettle.dataSource.accumulateJSONError;
|
72 |
|
73 | /** Given a String to be parsed as JSON, which has already failed to parse by JSON.parse, reject the supplied promise with
|
74 | * a readable diagnostic. If jsonlint was not loaded, simply return the original diagnostic.
|
75 | * @param {String} string - The string to be parsed
|
76 | * @param {Error} err - The exception provided by JSON.parse on the string
|
77 | * @param {Promise} promise - The promise to be rejected with a readable diagnostic
|
78 | */
|
79 | kettle.dataSource.renderJSONDiagnostic = function (string, err, promise) {
|
80 | if (!jsonlint) { // TODO: More principled context detection
|
81 | promise.reject(err.toString());
|
82 | }
|
83 | kettle.dataSource.JSONParseErrors = [];
|
84 | var errors = [];
|
85 | try {
|
86 | jsonlint.parse(string);
|
87 | } catch (e) {
|
88 | errors.push(e);
|
89 | } // Cannot override the core exception throwing code within the shitty parser - at jsonlint.js line 157
|
90 | errors = errors.concat(kettle.dataSource.JSONParseErrors);
|
91 | promise.reject({
|
92 | message: errors.join("\n")
|
93 | });
|
94 | };
|
95 |
|
96 | kettle.dataSource.parseJSON = function (string) {
|
97 | var togo = fluid.promise();
|
98 | if (!string) {
|
99 | togo.resolve(undefined);
|
100 | } else {
|
101 | try {
|
102 | togo.resolve(JSON.parse(string));
|
103 | } catch (err) {
|
104 | kettle.dataSource.renderJSONDiagnostic(string, err, togo);
|
105 | }
|
106 | }
|
107 | return togo;
|
108 | };
|
109 |
|
110 | kettle.dataSource.stringifyJSON = function (obj) {
|
111 | return obj === undefined ? "" : JSON.stringify(obj, null, 4);
|
112 | };
|
113 |
|
114 | kettle.dataSource.parseJSON5 = function (string) {
|
115 | var togo = fluid.promise();
|
116 | if (!string) {
|
117 | togo.resolve(undefined);
|
118 | } else {
|
119 | try {
|
120 | togo.resolve(JSON5.parse(string));
|
121 | } catch (err) {
|
122 | togo.reject({
|
123 | message: err.message || err
|
124 | });
|
125 | }
|
126 | }
|
127 | return togo;
|
128 | };
|
129 |
|
130 | kettle.dataSource.stringifyJSON5 = function (obj) {
|
131 | return obj === undefined ? "" : JSON5.stringify(obj, null, 4);
|
132 | };
|
133 |
|
134 | /**
|
135 | * The head of the hierarchy of dataSource components. These abstract
|
136 | * over the process of read and write access to data, following a simple CRUD-type semantic, indexed by
|
137 | a coordinate model (directModel) and which may be asynchronous.
|
138 | * Top-level methods are:
|
139 | * get(directModel[, callback|options] - to get the data from data resource
|
140 | * set(directModel, model[, callback|options] - to set the data (only if writable option is set to `true`)
|
141 | */
|
142 | fluid.defaults("kettle.dataSource", {
|
143 | gradeNames: ["fluid.component", "{that}.getWritableGrade"],
|
144 | mergePolicy: {
|
145 | setResponseTransforms: "replace"
|
146 | },
|
147 | events: {
|
148 | // events "onRead" and "onWrite" are operated in a custom workflow by fluid.fireTransformEvent to
|
149 | // process dataSource payloads during the get and set process. Each listener
|
150 | // receives the data returned by the last.
|
151 | onRead: null,
|
152 | onWrite: null,
|
153 | onError: null
|
154 | },
|
155 | components: {
|
156 | encoding: {
|
157 | type: "kettle.dataSource.encoding.JSON"
|
158 | }
|
159 | },
|
160 | listeners: {
|
161 | onRead: {
|
162 | func: "{encoding}.parse",
|
163 | namespace: "encoding"
|
164 | },
|
165 | onWrite: {
|
166 | func: "{encoding}.render",
|
167 | namespace: "encoding"
|
168 | }
|
169 | },
|
170 | invokers: {
|
171 | get: {
|
172 | funcName: "kettle.dataSource.get",
|
173 | args: ["{that}", "{arguments}.0", "{arguments}.1"] // directModel, options/callback
|
174 | },
|
175 | // getImpl: must be implemented by a concrete subgrade
|
176 | getWritableGrade: {
|
177 | funcName: "kettle.dataSource.getWritableGrade",
|
178 | args: ["{that}", "{that}.options.writable", "{that}.options.readOnlyGrade"]
|
179 | }
|
180 | },
|
181 | // In the case of parsing a response from a "set" request, only transforms of these namespaces will be applied
|
182 | setResponseTransforms: ["encoding"],
|
183 | charEncoding: "utf8", // choose one of node.js character encodings
|
184 | writable: false
|
185 | });
|
186 |
|
187 | // TODO: Move this system over to "linkage" too
|
188 | /* Use the peculiar `readOnlyGrade` member defined on every concrete DataSource to compute the name of the grade that should be
|
189 | * used to operate its writable variant if the `writable: true` options is set
|
190 | */
|
191 | kettle.dataSource.getWritableGrade = function (that, writable, readOnlyGrade) {
|
192 | if (!readOnlyGrade) {
|
193 | fluid.fail("Cannot evaluate writable grade without readOnlyGrade option");
|
194 | }
|
195 | if (writable) {
|
196 | return fluid.model.composeSegments(readOnlyGrade, "writable");
|
197 | }
|
198 | };
|
199 |
|
200 | fluid.defaults("kettle.dataSource.writable", {
|
201 | gradeNames: ["fluid.component"],
|
202 | invokers: {
|
203 | set: {
|
204 | funcName: "kettle.dataSource.set",
|
205 | args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // directModel, model, options/callback
|
206 | }
|
207 | // setImpl: must be implemented by a concrete subgrade
|
208 | }
|
209 | });
|
210 |
|
211 | // Registers the default promise handlers for a dataSource operation -
|
212 | // i) If the user has supplied a function in place of method <code>options</code>, register this function as a success handler
|
213 | // ii) if the user has supplied an onError handler in method <code>options</code>, this is registered - otherwise
|
214 | // we register the firer of the dataSource's own onError method.
|
215 |
|
216 | kettle.dataSource.registerStandardPromiseHandlers = function (that, promise, options) {
|
217 | promise.then(typeof(options) === "function" ? options : null,
|
218 | options.onError ? options.onError : that.events.onError.fire);
|
219 | };
|
220 |
|
221 | /** Apply default members to the options governing this dataSource request. Called when first receiving either a `get` or `set` request to the
|
222 | * top-level driver.
|
223 | * @param {Object} componentOptions - The DataSource's component options
|
224 | * @param {Object} [options] - [optional] Any additional options for this request. If supplied, this object reference will be written to in order to
|
225 | * assemble the returned options
|
226 | * @param {Object} directModel - The direct model supplied to the DataSource API for this request
|
227 | * @param {Booleanish} [isSet] - [optional] `true` if this is a `set` DataSource request
|
228 | * @return {Object} The fully merged request options in the same object reference as `options` if it was set
|
229 | */
|
230 |
|
231 | kettle.dataSource.defaultiseOptions = function (componentOptions, options, directModel, isSet) {
|
232 | options = options || {};
|
233 | options.directModel = directModel;
|
234 | options.operation = isSet ? "set" : "get";
|
235 | options.reverse = isSet ? true : false;
|
236 | options.writeMethod = options.writeMethod || componentOptions.writeMethod || "PUT"; // TODO: parameterise this, only of interest to HTTP DataSource
|
237 | options.notFoundIsEmpty = options.notFoundIsEmpty || componentOptions.notFoundIsEmpty;
|
238 | return options;
|
239 | };
|
240 |
|
241 | // TODO: Strategy note on these core engines - we need/plan to remove the asymmetry between the "concrete DataSource" (e.g. file or URL) and elements
|
242 | // of the transform chain. The so-called getImpl/setImpl should be replaced with sources of "just another" transform element, but in this case
|
243 | // one which transforms out of or into nothing to acquire the initial/final payload.
|
244 |
|
245 | /** Operate the core "transforming promise workflow" of a dataSource's `get` method. Gets the "initial payload" from the dataSource's `getImpl` method
|
246 | * and then pushes it through the transform chain to arrive at the final payload.
|
247 | * @param {Component} that - The dataSource itself
|
248 | * @param {Object} directModel - The direct model expressing the "coordinates" of the model to be fetched
|
249 | * @param {Object} [options] - [optional] A structure of options configuring the action of this get request - many of these will be specific to the particular concrete DataSource
|
250 | * @return {Promise} A promise for the final resolved payload
|
251 | */
|
252 | kettle.dataSource.get = function (that, directModel, options) {
|
253 | options = kettle.dataSource.defaultiseOptions(that.options, options, directModel);
|
254 | var initPayload = that.getImpl(options, directModel);
|
255 | var promise = fluid.promise.fireTransformEvent(that.events.onRead, initPayload, options);
|
256 | kettle.dataSource.registerStandardPromiseHandlers(that, promise, options);
|
257 | return promise;
|
258 | };
|
259 |
|
260 | /** Operate the core "transforming promise workflow" of a dataSource's `set` method. Pushes the user's payload backwards through the
|
261 | * transforming promise chain (in the opposite direction to that applied on `get`, and then applies it to the dataSource's `setImpl` method.
|
262 | * Any return from this is then pushed forwards through a limited range of the transforms (typically, e.g. just decoding it as JSON)
|
263 | * on its way back to the user.
|
264 | * @param {Component} that - The dataSource itself
|
265 | * @param {Object} directModel - The direct model expressing the "coordinates" of the model to be written
|
266 | * @param {Object} model - The payload to be written to the dataSource
|
267 | * @param {Object} [options] - [optional] A structure of options configuring the action of this set request - many of these will be specific to the particular concrete DataSource
|
268 | * @return {Promise} A promise for the final resolved payload (not all DataSources will provide any for a `set` method - the semantic of this is DataSource specific)
|
269 | */
|
270 | kettle.dataSource.set = function (that, directModel, model, options) {
|
271 | options = kettle.dataSource.defaultiseOptions(that.options, options, directModel, true); // shared and writeable between all participants
|
272 | var transformPromise = fluid.promise.fireTransformEvent(that.events.onWrite, model, options);
|
273 | var togo = fluid.promise();
|
274 | transformPromise.then(function (transformed) {
|
275 | var innerPromise = that.setImpl(options, directModel, transformed);
|
276 | innerPromise.then(function (setResponse) { // Apply limited transforms to a SET response payload
|
277 | var options2 = kettle.dataSource.defaultiseOptions(that.options, fluid.copy(options), directModel);
|
278 | options2.filterNamespaces = that.options.setResponseTransforms;
|
279 | var retransformed = fluid.promise.fireTransformEvent(that.events.onRead, setResponse, options2);
|
280 | fluid.promise.follow(retransformed, togo);
|
281 | }, function (error) {
|
282 | togo.reject(error);
|
283 | });
|
284 | });
|
285 | kettle.dataSource.registerStandardPromiseHandlers(that, togo, options);
|
286 | return togo;
|
287 | };
|
288 |
|
289 |
|
290 | /**
|
291 | * A mixin grade for a data source suitable for communicating with the /{db}/{docid} URL space of CouchDB for simple CRUD-style reading and writing
|
292 | */
|
293 |
|
294 | fluid.defaults("kettle.dataSource.CouchDB", {
|
295 | mergePolicy: {
|
296 | "rules": "nomerge"
|
297 | },
|
298 | rules: {
|
299 | writePayload: {
|
300 | value: ""
|
301 | },
|
302 | readPayload: {
|
303 | "": "value"
|
304 | }
|
305 | },
|
306 | listeners: {
|
307 | onRead: {
|
308 | funcName: "kettle.dataSource.CouchDB.read",
|
309 | args: ["{that}", "{arguments}.0"], // resp
|
310 | namespace: "CouchDB",
|
311 | priority: "after:encoding"
|
312 | }
|
313 | }
|
314 | });
|
315 |
|
316 | fluid.defaults("kettle.dataSource.CouchDB.writable", {
|
317 | listeners: {
|
318 | onWrite: {
|
319 | funcName: "kettle.dataSource.CouchDB.write",
|
320 | args: ["{that}", "{arguments}.0", "{arguments}.1"], // model, options
|
321 | namespace: "CouchDB",
|
322 | priority: "after:encoding"
|
323 | }
|
324 | }
|
325 | });
|
326 |
|
327 | fluid.makeGradeLinkage("kettle.dataSource.CouchDB.linkage", ["kettle.dataSource.writable", "kettle.dataSource.CouchDB"], "kettle.dataSource.CouchDB.writable");
|
328 |
|
329 | /**
|
330 | * Convert a dataSource payload from CouchDB-encoded form -
|
331 | *
|
332 | * i) Decode a Couch error response into a promise failure
|
333 | *
|
334 | * ii) Transform the output from CouchDB using `that.options.rules.readPayload`. The default rules reverse the default
|
335 | * "value" encoding used by `kettle.dataSource.CouchDB.write` (see below).
|
336 | * @param {Component} that - The dataSource component, used to read the payload read transform option
|
337 | * @param {Object} resp - JSON-parsed response as received from CouchDB
|
338 | * @return {Object} The transformed return payload
|
339 | */
|
340 | kettle.dataSource.CouchDB.read = function (that, resp) {
|
341 | // if undefined, pass that through as per dataSource (just for consistency in FS-backed tests)
|
342 | var togo;
|
343 | if (resp === undefined) {
|
344 | togo = undefined;
|
345 | } else {
|
346 | if (resp.error) {
|
347 | var error = {
|
348 | isError: true,
|
349 | statusCode: resp.statusCode,
|
350 | message: resp.error + ": " + resp.reason
|
351 | };
|
352 | togo = fluid.promise();
|
353 | togo.reject(error);
|
354 | } else {
|
355 | togo = fluid.model.transformWithRules(resp, that.options.rules.readPayload);
|
356 | }
|
357 | }
|
358 | return togo;
|
359 | };
|
360 |
|
361 | /**
|
362 | * Convert `model` data for storage in CouchDB using the model transformation rules outlined in
|
363 | * `that.options.rules.writePayload`. By default, the entirety of the model is wrapped in a `value` element to avoid
|
364 | * collisions with top-level CouchDB variables such as `_id` and `_rev`.
|
365 | *
|
366 | * @param {Component} that - The dataSource component, used to read the payload write transform option
|
367 | * @param {Object} model - The data to be stored
|
368 | * @param {Object} options - The dataSource's request options (see above)
|
369 | * @return {Promise} A promise which resolves to the transformed, written payload
|
370 | */
|
371 | kettle.dataSource.CouchDB.write = function (that, model, options) {
|
372 | var directModel = options.directModel;
|
373 | var doc = fluid.model.transformWithRules(model, that.options.rules.writePayload);
|
374 | var original = that.get(directModel, {filterNamespaces: ["encoding"], notFoundIsEmpty: true});
|
375 | var togo = fluid.promise();
|
376 | original.then(function (originalDoc) {
|
377 | if (originalDoc) {
|
378 | doc._id = originalDoc._id;
|
379 | doc._rev = originalDoc._rev;
|
380 | }
|
381 | togo.resolve(doc);
|
382 | }, togo.reject);
|
383 | return togo;
|
384 | };
|