UNPKG

14.9 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 err {Error} 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 return 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
221kettle.dataSource.defaultiseOptions = function (componentOptions, options, directModel, isSet) {
222 options = options || {};
223 options.directModel = directModel;
224 options.operation = isSet ? "set" : "get";
225 options.reverse = isSet ? true : false;
226 options.writeMethod = options.writeMethod || componentOptions.writeMethod || "PUT"; // TODO: parameterise this, only of interest to HTTP DataSource
227 options.notFoundIsEmpty = options.notFoundIsEmpty || componentOptions.notFoundIsEmpty;
228 return options;
229};
230
231// 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
232// of the transform chain. The so-called getImpl/setImpl should be replaced with sources of "just another" transform element, but in this case
233// one which transforms out of or into nothing to acquire the initial/final payload.
234
235/** Operate the core "transforming promise workflow" of a dataSource's `get` method. Gets the "initial payload" from the dataSource's `getImpl` method
236 * and then pushes it through the transform chain to arrive at the final payload.
237 * @param that {Component} The dataSource itself
238 * @param directModel {Object} The direct model expressing the "coordinates" of the model to be fetched
239 * @param options {Object} A structure of options configuring the action of this get request - many of these will be specific to the particular concrete DataSource
240 * @return {Promise} A promise for the final resolved payload
241 */
242
243kettle.dataSource.get = function (that, directModel, options) {
244 options = kettle.dataSource.defaultiseOptions(that.options, options, directModel);
245 var initPayload = that.getImpl(options, directModel);
246 var promise = fluid.promise.fireTransformEvent(that.events.onRead, initPayload, options);
247 kettle.dataSource.registerStandardPromiseHandlers(that, promise, options);
248 return promise;
249};
250
251/** Operate the core "transforming promise workflow" of a dataSource's `set` method. Pushes the user's payload backwards through the
252 * transforming promise chain (in the opposite direction to that applied on `get`, and then applies it to the dataSource's `setImpl` method.
253 * Any return from this is then pushed forwards through a limited range of the transforms (typically, e.g. just decoding it as JSON)
254 * on its way back to the user.
255 * @param that {Component} The dataSource itself
256 * @param directModel {Object} The direct model expressing the "coordinates" of the model to be written
257 * @param model {Object} The payload to be written to the dataSource
258 * @param options {Object} A structure of options configuring the action of this set request - many of these will be specific to the particular concrete DataSource
259 * @return {Promise} A promise for the final resolved payload (not all DataSources will provide any for a `set` method)
260 */
261
262kettle.dataSource.set = function (that, directModel, model, options) {
263 options = kettle.dataSource.defaultiseOptions(that.options, options, directModel, true); // shared and writeable between all participants
264 var transformPromise = fluid.promise.fireTransformEvent(that.events.onWrite, model, options);
265 var togo = fluid.promise();
266 transformPromise.then(function (transformed) {
267 var innerPromise = that.setImpl(options, directModel, transformed);
268 innerPromise.then(function (setResponse) { // Apply limited transforms to a SET response payload
269 var options2 = kettle.dataSource.defaultiseOptions(that.options, fluid.copy(options), directModel);
270 options2.filterNamespaces = that.options.setResponseTransforms;
271 var retransformed = fluid.promise.fireTransformEvent(that.events.onRead, setResponse, options2);
272 fluid.promise.follow(retransformed, togo);
273 }, function (error) {
274 togo.reject(error);
275 });
276 });
277 kettle.dataSource.registerStandardPromiseHandlers(that, togo, options);
278 return togo;
279};
280
281
282/**
283 * 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
284 */
285
286fluid.defaults("kettle.dataSource.CouchDB", {
287 mergePolicy: {
288 "rules": "nomerge"
289 },
290 rules: {
291 writePayload: {
292 value: ""
293 },
294 readPayload: {
295 "": "value"
296 }
297 },
298 listeners: {
299 onRead: {
300 funcName: "kettle.dataSource.CouchDB.read",
301 args: ["{that}", "{arguments}.0"], // resp
302 namespace: "CouchDB",
303 priority: "after:encoding"
304 }
305 }
306});
307
308fluid.defaults("kettle.dataSource.CouchDB.writable", {
309 listeners: {
310 onWrite: {
311 funcName: "kettle.dataSource.CouchDB.write",
312 args: ["{that}", "{arguments}.0", "{arguments}.1"], // model, options
313 namespace: "CouchDB",
314 priority: "after:encoding"
315 }
316 }
317});
318
319fluid.makeGradeLinkage("kettle.dataSource.CouchDB.linkage", ["kettle.dataSource.writable", "kettle.dataSource.CouchDB"], "kettle.dataSource.CouchDB.writable");
320
321/**
322 * Convert a dataSource payload from CouchDB-encoded form -
323 *
324 * i) Decode a Couch error response into a promise failure
325 *
326 * ii) Transform the output from CouchDB using `that.options.rules.readPayload`. The default rules reverse the default
327 * "value" encoding used by `kettle.dataSource.CouchDB.write` (see below).
328 *
329 * @param {resp} JSON-parsed response as received from CouchDB.
330 */
331kettle.dataSource.CouchDB.read = function (that, resp) {
332 // if undefined, pass that through as per dataSource (just for consistency in FS-backed tests)
333 var togo;
334 if (resp === undefined) {
335 togo = undefined;
336 } else {
337 if (resp.error) {
338 var error = {
339 isError: true,
340 statusCode: resp.statusCode,
341 message: resp.error + ": " + resp.reason
342 };
343 togo = fluid.promise();
344 togo.reject(error);
345 } else {
346 togo = fluid.model.transformWithRules(resp, that.options.rules.readPayload);
347 }
348 }
349 return togo;
350};
351
352/**
353 * Convert `model` data for storage in CouchDB using the model transformation rules outlined in
354 * `that.options.rules.writePayload`. By default, the entirety of the model is wrapped in a `value` element to avoid
355 * collisions with top-level CouchDB variables such as `_id` and `_rev`.
356 *
357 * @param {model} The data to be stored.
358 * @param {options} The `dataSource` options (see above).
359 */
360kettle.dataSource.CouchDB.write = function (that, model, options) {
361 var directModel = options.directModel;
362 var doc = fluid.model.transformWithRules(model, that.options.rules.writePayload);
363 var original = that.get(directModel, {filterNamespaces: ["encoding"], notFoundIsEmpty: true});
364 var togo = fluid.promise();
365 original.then(function (originalDoc) {
366 if (originalDoc) {
367 doc._id = originalDoc._id;
368 doc._rev = originalDoc._rev;
369 }
370 togo.resolve(doc);
371 });
372 return togo;
373};
374