UNPKG

18.7 kBJavaScriptView Raw
1'use strict'
2
3var EventLite = require('event-lite')
4
5// Local modules.
6var memoryAdapter = require('./adapter/adapters/memory')
7var AdapterSingleton = require('./adapter/singleton')
8var validate = require('./record_type/validate')
9var ensureTypes = require('./record_type/ensure_types')
10var promise = require('./common/promise')
11var internalRequest = require('./request')
12var middlewares = internalRequest.middlewares
13
14// Static re-exports.
15var Adapter = require('./adapter')
16var common = require('./common')
17var assign = common.assign
18var methods = common.methods
19var events = common.events
20
21
22/**
23 * This is the default export of the `fortune` package. It implements a
24 * [subset of `EventEmitter`](https://www.npmjs.com/package/event-lite), and it
25 * has a few static properties attached to it that may be useful to access:
26 *
27 * - `Adapter`: abstract base class for the Adapter.
28 * - `adapters`: included adapters, defaults to memory adapter.
29 * - `errors`: custom error types, useful for throwing errors in I/O hooks.
30 * - `methods`: a hash that maps to string constants. Available are: `find`,
31 * `create`, `update`, and `delete`.
32 * - `events`: names for events on the Fortune instance. Available are:
33 * `change`, `sync`, `connect`, `disconnect`, `failure`.
34 * - `message`: a function which accepts the arguments (`id`, `language`,
35 * `data`). It has properties keyed by two-letter language codes, which by
36 * default includes only `en`.
37 * - `Promise`: assign this to set the Promise implementation that Fortune
38 * will use.
39 */
40function Fortune (recordTypes, options) {
41 if (!(this instanceof Fortune))
42 return new Fortune(recordTypes, options)
43
44 this.constructor(recordTypes, options)
45}
46
47
48// Inherit from EventLite class.
49Fortune.prototype = new EventLite()
50
51
52/**
53 * Create a new instance, the only required input is record type definitions.
54 * The first argument must be an object keyed by name, valued by definition
55 * objects.
56 *
57 * Here are some example field definitions:
58 *
59 * ```js
60 * {
61 * // Top level keys are names of record types.
62 * person: {
63 * // Data types may be singular or plural.
64 * name: String, // Singular string value.
65 * luckyNumbers: Array(Number), // Array of numbers.
66 *
67 * // Relationships may be singular or plural. They must specify which
68 * // record type it refers to, and may also specify an inverse field
69 * // which is optional but recommended.
70 * pets: [ Array('animal'), 'owner' ], // Has many.
71 * employer: [ 'organization', 'employees' ], // Belongs to.
72 * likes: Array('thing'), // Has many (no inverse).
73 * doing: 'activity', // Belongs to (no inverse).
74 *
75 * // Reflexive relationships are relationships in which the record type,
76 * // the first position, is of the same type.
77 * following: [ Array('person'), 'followers' ],
78 * followers: [ Array('person'), 'following' ],
79 *
80 * // Mutual relationships are relationships in which the inverse,
81 * // the second position, is defined to be the same field on the same
82 * // record type.
83 * friends: [ Array('person'), 'friends' ],
84 * spouse: [ 'person', 'spouse' ]
85 * }
86 * }
87 * ```
88 *
89 * The above shows the shorthand which will be transformed internally to a
90 * more verbose data structure. The internal structure is as follows:
91 *
92 * ```js
93 * {
94 * person: {
95 * // A singular value.
96 * name: { type: String },
97 *
98 * // An array containing values of a single type.
99 * luckyNumbers: { type: Number, isArray: true },
100 *
101 * // Creates a to-many link to `animal` record type. If the field `owner`
102 * // on the `animal` record type is not an array, this is a many-to-one
103 * // relationship, otherwise it is many-to-many.
104 * pets: { link: 'animal', isArray: true, inverse: 'owner' },
105 *
106 * // The `min` and `max` keys are open to interpretation by the specific
107 * // adapter, which may introspect the field definition.
108 * thing: { type: Number, min: 0, max: 100 },
109 *
110 * // Nested field definitions are invalid. Use `Object` type instead.
111 * nested: { thing: { ... } } // Will throw an error.
112 * }
113 * }
114 * ```
115 *
116 * The allowed native types are `String`, `Number`, `Boolean`, `Date`,
117 * `Object`, and `Buffer`. Note that the `Object` type should be a JSON
118 * serializable object that may be persisted. The only other allowed type is
119 * a `Function`, which may be used to define custom types.
120 *
121 * A custom type function should accept one argument, the value, and return a
122 * boolean based on whether the value is valid for the type or not. It may
123 * optionally have a method `compare`, used for sorting in the built-in
124 * adapters. The `compare` method should have the same signature as the native
125 * `Array.prototype.sort`.
126 *
127 * A custom type function must inherit one of the allowed native types. For
128 * example:
129 *
130 * ```js
131 * function Integer (x) { return (x | 0) === x }
132 * Integer.prototype = new Number()
133 * ```
134 *
135 * The options object may contain the following keys:
136 *
137 * - `adapter`: configuration array for the adapter. The default type is the
138 * memory adapter. If the value is not an array, its settings will be
139 * considered omitted.
140 *
141 * ```js
142 * {
143 * adapter: [
144 * // Must be a class that extends `Fortune.Adapter`, or a function
145 * // that accepts the Adapter class and returns a subclass. Required.
146 * Adapter => { ... },
147 *
148 * // An options object that is specific to the adapter. Optional.
149 * { ... }
150 * ]
151 * }
152 * ```
153 *
154 * - `hooks`: keyed by type name, valued by an array containing an `input`
155 * and/or `output` function at indices `0` and `1` respectively.
156 *
157 * A hook function takes at least two arguments, the internal `context`
158 * object and a single `record`. A special case is the `update` argument for
159 * the `update` method.
160 *
161 * There are only two kinds of hooks, before a record is written (input),
162 * and after a record is read (output), both are optional. If an error occurs
163 * within a hook function, it will be forwarded to the response. Use typed
164 * errors to provide the appropriate feedback.
165 *
166 * For a create request, the input hook may return the second argument
167 * `record` either synchronously, or asynchronously as a Promise. The return
168 * value of a delete request is inconsequential, but it may return a value or
169 * a Promise. The `update` method accepts a `update` object as a third
170 * parameter, which may be returned synchronously or as a Promise.
171 *
172 * An example hook to apply a timestamp on a record before creation, and
173 * displaying the timestamp in the server's locale:
174 *
175 * ```js
176 * {
177 * recordType: [
178 * (context, record, update) => {
179 * switch (context.request.method) {
180 * case 'create':
181 * record.timestamp = new Date()
182 * return record
183 * case 'update': return update
184 * case 'delete': return null
185 * }
186 * },
187 * (context, record) => {
188 * record.timestamp = record.timestamp.toLocaleString()
189 * return record
190 * }
191 * ]
192 * }
193 * ```
194 *
195 * Requests to update a record will **NOT** have the updates already applied
196 * to the record.
197 *
198 * Another feature of the input hook is that it will have access to a
199 * temporary field `context.transaction`. This is useful for ensuring that
200 * bulk write operations are all or nothing. Each request is treated as a
201 * single transaction.
202 *
203 * - `documentation`: an object mapping names to descriptions. Note that there
204 * is only one namepspace, so field names can only have one description.
205 * This is optional, but useful for the HTML serializer, which also emits
206 * this information as micro-data.
207 *
208 * ```js
209 * {
210 * documentation: {
211 * recordType: 'Description of a type.',
212 * fieldName: 'Description of a field.',
213 * anotherFieldName: {
214 * en: 'Two letter language code indicates localized description.'
215 * }
216 * }
217 * }
218 * ```
219 *
220 * - `settings`: internal settings to configure.
221 *
222 * ```js
223 * {
224 * settings: {
225 * // Whether or not to enforce referential integrity. This may be
226 * // useful to disable on the client-side.
227 * enforceLinks: true,
228 *
229 * // Name of the application used for display purposes.
230 * name: 'My Awesome Application',
231 *
232 * // Description of the application used for display purposes.
233 * description: 'media type "application/vnd.micro+json"'
234 * }
235 * }
236 * ```
237 *
238 * The return value of the constructor is the instance itself.
239 *
240 * @param {Object} [recordTypes]
241 * @param {Object} [options]
242 * @return {Fortune}
243 */
244Fortune.prototype.constructor = function Fortune (recordTypes, options) {
245 var self = this
246 var plainObject = {}
247 var message = common.message
248 var adapter, method, stack, flows, type, hooks, i, j
249
250 if (recordTypes === void 0) recordTypes = {}
251 if (options === void 0) options = {}
252
253 if (!('adapter' in options)) options.adapter = [ memoryAdapter(Adapter) ]
254 if (!('settings' in options)) options.settings = {}
255 if (!('hooks' in options)) options.hooks = {}
256 if (!('enforceLinks' in options.settings))
257 options.settings.enforceLinks = true
258
259 // Bind middleware methods to instance.
260 flows = {}
261 for (method in methods) {
262 stack = [ middlewares[method], middlewares.include, middlewares.end ]
263
264 for (i = 0, j = stack.length; i < j; i++)
265 stack[i] = bindMiddleware(self, stack[i])
266
267 flows[methods[method]] = stack
268 }
269
270 hooks = options.hooks
271
272 // Validate hooks.
273 for (type in hooks) {
274 if (!recordTypes.hasOwnProperty(type)) throw new Error(
275 'Attempted to define hook on "' + type + '" type ' +
276 'which does not exist.')
277 if (!Array.isArray(hooks[type]))
278 throw new TypeError('Hook value for "' + type + '" type ' +
279 'must be an array.')
280 }
281
282 // Validate record types.
283 for (type in recordTypes) {
284 if (type in plainObject)
285 throw new Error('Can not define type name "' + type +
286 '" which is in Object.prototype.')
287
288 validate(recordTypes[type])
289 if (!hooks.hasOwnProperty(type)) hooks[type] = []
290 }
291
292 /*!
293 * Adapter singleton that is coupled to the Fortune instance.
294 *
295 * @type {Adapter}
296 */
297 adapter = new AdapterSingleton({
298 adapter: options.adapter,
299 recordTypes: recordTypes,
300 hooks: hooks,
301 message: message
302 })
303
304 self.options = options
305 self.hooks = hooks
306 self.recordTypes = recordTypes
307 self.adapter = adapter
308
309 // Internal properties.
310 Object.defineProperties(self, {
311 // 0 = not started, 1 = started, 2 = done.
312 connectionStatus: { value: 0, writable: true },
313
314 message: { value: message },
315 flows: { value: flows }
316 })
317}
318
319
320/**
321 * This is the primary method for initiating a request. The options object
322 * may contain the following keys:
323 *
324 * - `method`: The method is a either a function or a constant, which is keyed
325 * under `Fortune.common.methods` and may be one of `find`, `create`,
326 * `update`, or `delete`. Default: `find`.
327 *
328 * - `type`: Name of a type. **Required**.
329 *
330 * - `ids`: An array of IDs. Used for `find` and `delete` methods only. This is
331 * optional for the `find` method.
332 *
333 * - `include`: A 3-dimensional array specifying links to include. The first
334 * dimension is a list, the second dimension is depth, and the third
335 * dimension is an optional tuple with field and query options. For example:
336 * `[['comments'], ['comments', ['author', { ... }]]]`.
337 *
338 * - `options`: Exactly the same as the [`find` method](#adapter-find)
339 * options in the adapter. These options do not apply on methods other than
340 * `find`, and do not affect the records returned from `include`. Optional.
341 *
342 * - `meta`: Meta-information object of the request. Optional.
343 *
344 * - `payload`: Payload of the request. **Required** for `create` and `update`
345 * methods only, and must be an array of objects. The objects must be the
346 * records to create, or update objects as expected by the Adapter.
347 *
348 * - `transaction`: if an existing transaction should be re-used, this may
349 * optionally be passed in. This must be ended manually.
350 *
351 * The response object may contain the following keys:
352 *
353 * - `meta`: Meta-info of the response.
354 *
355 * - `payload`: An object containing the following keys:
356 * - `records`: An array of records returned.
357 * - `count`: Total number of records without options applied (only for
358 * responses to the `find` method).
359 * - `include`: An object keyed by type, valued by arrays of included
360 * records.
361 *
362 * The resolved response object should always be an instance of a response
363 * type.
364 *
365 * @param {Object} options
366 * @return {Promise}
367 */
368Fortune.prototype.request = function (options) {
369 var self = this
370 var connectionStatus = self.connectionStatus
371 var Promise = promise.Promise
372
373 if (connectionStatus === 0)
374 return self.connect()
375 .then(function () { return internalRequest.call(self, options) })
376
377 else if (connectionStatus === 1)
378 return new Promise(function (resolve, reject) {
379 // Wait for changes to connection status.
380 self.once(events.failure, function () {
381 reject(new Error('Connection failed.'))
382 })
383 self.once(events.connect, function () {
384 resolve(internalRequest.call(self, options))
385 })
386 })
387
388 return internalRequest.call(self, options)
389}
390
391
392/**
393 * The `find` method retrieves record by type given IDs, querying options,
394 * or both. This is a convenience method that wraps around the `request`
395 * method, see the `request` method for documentation on its arguments.
396 *
397 * @param {String} type
398 * @param {*|*[]} [ids]
399 * @param {Object} [options]
400 * @param {Array[]} [include]
401 * @param {Object} [meta]
402 * @return {Promise}
403 */
404Fortune.prototype.find = function (type, ids, options, include, meta) {
405 var obj = { method: methods.find, type: type }
406
407 if (ids) obj.ids = Array.isArray(ids) ? ids : [ ids ]
408 if (options) obj.options = options
409 if (include) obj.include = include
410 if (meta) obj.meta = meta
411
412 return this.request(obj)
413}
414
415
416/**
417 * The `create` method creates records by type given records to create. This
418 * is a convenience method that wraps around the `request` method, see the
419 * request `method` for documentation on its arguments.
420 *
421 * @param {String} type
422 * @param {Object|Object[]} records
423 * @param {Array[]} [include]
424 * @param {Object} [meta]
425 * @return {Promise}
426 */
427Fortune.prototype.create = function (type, records, include, meta) {
428 var options = { method: methods.create, type: type,
429 payload: Array.isArray(records) ? records : [ records ] }
430
431 if (include) options.include = include
432 if (meta) options.meta = meta
433
434 return this.request(options)
435}
436
437
438/**
439 * The `update` method updates records by type given update objects. See the
440 * [Adapter.update](#adapter-update) method for the format of the update
441 * objects. This is a convenience method that wraps around the `request`
442 * method, see the `request` method for documentation on its arguments.
443 *
444 * @param {String} type
445 * @param {Object|Object[]} updates
446 * @param {Array[]} [include]
447 * @param {Object} [meta]
448 * @return {Promise}
449 */
450Fortune.prototype.update = function (type, updates, include, meta) {
451 var options = { method: methods.update, type: type,
452 payload: Array.isArray(updates) ? updates : [ updates ] }
453
454 if (include) options.include = include
455 if (meta) options.meta = meta
456
457 return this.request(options)
458}
459
460
461/**
462 * The `delete` method deletes records by type given IDs (optional). This is a
463 * convenience method that wraps around the `request` method, see the `request`
464 * method for documentation on its arguments.
465 *
466 * @param {String} type
467 * @param {*|*[]} [ids]
468 * @param {Array[]} [include]
469 * @param {Object} [meta]
470 * @return {Promise}
471 */
472Fortune.prototype.delete = function (type, ids, include, meta) {
473 var options = { method: methods.delete, type: type }
474
475 if (ids) options.ids = Array.isArray(ids) ? ids : [ ids ]
476 if (include) options.include = include
477 if (meta) options.meta = meta
478
479 return this.request(options)
480}
481
482
483/**
484 * This method does not need to be called manually, it is automatically called
485 * upon the first request if it is not connected already. However, it may be
486 * useful if manually reconnect is needed. The resolved value is the instance
487 * itself.
488 *
489 * @return {Promise}
490 */
491Fortune.prototype.connect = function () {
492 var self = this
493 var Promise = promise.Promise
494
495 if (self.connectionStatus === 1)
496 return Promise.reject(new Error('Connection is in progress.'))
497
498 else if (self.connectionStatus === 2)
499 return Promise.reject(new Error('Connection is already done.'))
500
501 self.connectionStatus = 1
502
503 return new Promise(function (resolve, reject) {
504 Object.defineProperty(self, 'denormalizedFields', {
505 value: ensureTypes(self.recordTypes),
506 writable: true,
507 configurable: true
508 })
509
510 self.adapter.connect().then(function () {
511 self.connectionStatus = 2
512 self.emit(events.connect)
513 return resolve(self)
514 }, function (error) {
515 self.connectionStatus = 0
516 self.emit(events.failure)
517 return reject(error)
518 })
519 })
520}
521
522
523/**
524 * Close adapter connection, and reset connection state. The resolved value is
525 * the instance itself.
526 *
527 * @return {Promise}
528 */
529Fortune.prototype.disconnect = function () {
530 var self = this
531 var Promise = promise.Promise
532
533 if (self.connectionStatus !== 2)
534 return Promise.reject(new Error('Instance has not been connected.'))
535
536 self.connectionStatus = 1
537
538 return new Promise(function (resolve, reject) {
539 return self.adapter.disconnect().then(function () {
540 self.connectionStatus = 0
541 self.emit(events.disconnect)
542 return resolve(self)
543 }, function (error) {
544 self.connectionStatus = 2
545 self.emit(events.failure)
546 return reject(error)
547 })
548 })
549}
550
551
552// Useful for dependency injection. All instances of Fortune have the same
553// common internal dependencies.
554Fortune.prototype.common = common
555
556
557// Assign useful static properties to the default export.
558assign(Fortune, {
559 Adapter: Adapter,
560 adapters: {
561 memory: memoryAdapter(Adapter)
562 },
563 errors: common.errors,
564 message: common.message,
565 methods: methods,
566 events: events
567})
568
569
570// Set the `Promise` property.
571Object.defineProperty(Fortune, 'Promise', {
572 enumerable: true,
573 get: function () {
574 return promise.Promise
575 },
576 set: function (value) {
577 promise.Promise = value
578 }
579})
580
581
582// Internal helper function.
583function bindMiddleware (scope, method) {
584 return function (x) {
585 return method.call(scope, x)
586 }
587}
588
589
590module.exports = Fortune