1 |
|
2 |
|
3 | var EventLite = require('event-lite')
|
4 |
|
5 | // Local modules.
|
6 | var memoryAdapter = require('./adapter/adapters/memory')
|
7 | var AdapterSingleton = require('./adapter/singleton')
|
8 | var validate = require('./record_type/validate')
|
9 | var ensureTypes = require('./record_type/ensure_types')
|
10 | var promise = require('./common/promise')
|
11 | var internalRequest = require('./request')
|
12 | var middlewares = internalRequest.middlewares
|
13 |
|
14 | // Static re-exports.
|
15 | var Adapter = require('./adapter')
|
16 | var common = require('./common')
|
17 | var assign = common.assign
|
18 | var methods = common.methods
|
19 | var 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 | */
|
40 | function 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.
|
49 | Fortune.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 | */
|
244 | Fortune.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 | */
|
368 | Fortune.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 | */
|
404 | Fortune.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 | */
|
427 | Fortune.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 | */
|
450 | Fortune.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 | */
|
472 | Fortune.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 | */
|
491 | Fortune.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 | */
|
529 | Fortune.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.
|
554 | Fortune.prototype.common = common
|
555 |
|
556 |
|
557 | // Assign useful static properties to the default export.
|
558 | assign(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.
|
571 | Object.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.
|
583 | function bindMiddleware (scope, method) {
|
584 | return function (x) {
|
585 | return method.call(scope, x)
|
586 | }
|
587 | }
|
588 |
|
589 |
|
590 | module.exports = Fortune
|