UNPKG

16.1 kBJavaScriptView Raw
1/*!
2Kettle Core DataSource definitions - portable to browser and node.js
3
4Copyright 2012-2013 OCAD University
5
6Licensed under the New BSD license. You may not use this file except in
7compliance with this License.
8
9You may obtain a copy of the License at
10https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
11*/
12
13"use strict";
14
15var 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
23fluid.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
32fluid.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
41fluid.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
50fluid.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
62kettle.dataSource.JSONParseErrors = [];
63
64kettle.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
71jsonlint.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 */
79kettle.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
96kettle.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
110kettle.dataSource.stringifyJSON = function (obj) {
111 return obj === undefined ? "" : JSON.stringify(obj, null, 4);
112};
113
114kettle.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
130kettle.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 */
142fluid.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 */
191kettle.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
200fluid.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
216kettle.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
231kettle.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 */
252kettle.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 */
270kettle.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
294fluid.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
316fluid.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
327fluid.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 */
340kettle.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 */
371kettle.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};