Fortune.jsGitHub Repository
Fortune (index.js)

class Fortune#

This is the default export of the fortune package. It implements a subset of EventEmitter, and it has a few static properties attached to it that may be useful to access:

  • Adapter: abstract base class for the Adapter.
  • adapters: included adapters, defaults to memory adapter.
  • errors: custom error types, useful for throwing errors in I/O hooks.
  • methods: a hash that maps to string constants. Available are: find, create, update, and delete.
  • events: names for events on the Fortune instance. Available are: change, sync, connect, disconnect, failure.
  • message: a function which accepts the arguments (id, language, data). It has properties keyed by two-letter language codes, which by default includes only en.
  • Promise: assign this to set the Promise implementation that Fortune will use.

new Fortune([recordTypes], [options])Fortune#

Create a new instance, the only required input is record type definitions. The first argument must be an object keyed by name, valued by definition objects.

Here are some example field definitions:

{
  // Top level keys are names of record types.
  person: {
    // Data types may be singular or plural.
    name: String, // Singular string value.
    luckyNumbers: Array(Number), // Array of numbers.

    // Relationships may be singular or plural. They must specify which
    // record type it refers to, and may also specify an inverse field
    // which is optional but recommended.
    pets: [ Array('animal'), 'owner' ], // Has many.
    employer: [ 'organization', 'employees' ], // Belongs to.
    likes: Array('thing'), // Has many (no inverse).
    doing: 'activity', // Belongs to (no inverse).

    // Reflexive relationships are relationships in which the record type,
    // the first position, is of the same type.
    following: [ Array('person'), 'followers' ],
    followers: [ Array('person'), 'following' ],

    // Mutual relationships are relationships in which the inverse,
    // the second position, is defined to be the same field on the same
    // record type.
    friends: [ Array('person'), 'friends' ],
    spouse: [ 'person', 'spouse' ]
  }
}

The above shows the shorthand which will be transformed internally to a more verbose data structure. The internal structure is as follows:

{
  person: {
    // A singular value.
    name: { type: String },

    // An array containing values of a single type.
    luckyNumbers: { type: Number, isArray: true },

    // Creates a to-many link to `animal` record type. If the field `owner`
    // on the `animal` record type is not an array, this is a many-to-one
    // relationship, otherwise it is many-to-many.
    pets: { link: 'animal', isArray: true, inverse: 'owner' },

    // The `min` and `max` keys are open to interpretation by the specific
    // adapter, which may introspect the field definition.
    thing: { type: Number, min: 0, max: 100 },

    // Nested field definitions are invalid. Use `Object` type instead.
    nested: { thing: { ... } } // Will throw an error.
  }
}

The allowed native types are String, Number, Boolean, Date, Object, and Buffer. Note that the Object type should be a JSON serializable object that may be persisted. The only other allowed type is a Function, which may be used to define custom types.

A custom type function should accept one argument, the value, and return a boolean based on whether the value is valid for the type or not. It may optionally have a method compare, used for sorting in the built-in adapters. The compare method should have the same signature as the native Array.prototype.sort.

A custom type function must inherit one of the allowed native types. For example:

function Integer (x) { return (x | 0) === x }
Integer.prototype = new Number()

The options object may contain the following keys:

  • adapter: configuration array for the adapter. The default type is the memory adapter. If the value is not an array, its settings will be considered omitted.

    {
      adapter: [
        // Must be a class that extends `Fortune.Adapter`, or a function
        // that accepts the Adapter class and returns a subclass. Required.
        Adapter => { ... },
    
        // An options object that is specific to the adapter. Optional.
        { ... }
      ]
    }
    
  • hooks: keyed by type name, valued by an array containing an input and/or output function at indices 0 and 1 respectively.

    A hook function takes at least two arguments, the internal context object and a single record. A special case is the update argument for the update method.

    There are only two kinds of hooks, before a record is written (input), and after a record is read (output), both are optional. If an error occurs within a hook function, it will be forwarded to the response. Use typed errors to provide the appropriate feedback.

    For a create request, the input hook may return the second argument record either synchronously, or asynchronously as a Promise. The return value of a delete request is inconsequential, but it may return a value or a Promise. The update method accepts a update object as a third parameter, which may be returned synchronously or as a Promise.

    An example hook to apply a timestamp on a record before creation, and displaying the timestamp in the server's locale:

    {
      recordType: [
        (context, record, update) => {
          switch (context.request.method) {
            case 'create':
              record.timestamp = new Date()
              return record
            case 'update': return update
            case 'delete': return null
          }
        },
        (context, record) => {
          record.timestamp = record.timestamp.toLocaleString()
          return record
        }
      ]
    }
    

    Requests to update a record will NOT have the updates already applied to the record.

    Another feature of the input hook is that it will have access to a temporary field context.transaction. This is useful for ensuring that bulk write operations are all or nothing. Each request is treated as a single transaction.

  • documentation: an object mapping names to descriptions. Note that there is only one namepspace, so field names can only have one description. This is optional, but useful for the HTML serializer, which also emits this information as micro-data.

    {
      documentation: {
        recordType: 'Description of a type.',
        fieldName: 'Description of a field.',
        anotherFieldName: {
          en: 'Two letter language code indicates localized description.'
        }
      }
    }
    
  • settings: internal settings to configure.

    {
      settings: {
        // Whether or not to enforce referential integrity. This may be
        // useful to disable on the client-side.
        enforceLinks: true,
    
        // Name of the application used for display purposes.
        name: 'My Awesome Application',
    
        // Description of the application used for display purposes.
        description: 'media type "application/vnd.micro+json"'
      }
    }
    

The return value of the constructor is the instance itself.

Fortune.request(options)Promise#

This is the primary method for initiating a request. The options object may contain the following keys:

  • method: The method is a either a function or a constant, which is keyed under Fortune.common.methods and may be one of find, create, update, or delete. Default: find.

  • type: Name of a type. Required.

  • ids: An array of IDs. Used for find and delete methods only. This is optional for the find method.

  • include: A 3-dimensional array specifying links to include. The first dimension is a list, the second dimension is depth, and the third dimension is an optional tuple with field and query options. For example: [['comments'], ['comments', ['author', { ... }]]].

  • options: Exactly the same as the find method options in the adapter. These options do not apply on methods other than find, and do not affect the records returned from include. Optional.

  • meta: Meta-information object of the request. Optional.

  • payload: Payload of the request. Required for create and update methods only, and must be an array of objects. The objects must be the records to create, or update objects as expected by the Adapter.

  • transaction: if an existing transaction should be re-used, this may optionally be passed in. This must be ended manually.

The response object may contain the following keys:

  • meta: Meta-info of the response.

  • payload: An object containing the following keys:

    • records: An array of records returned.
    • count: Total number of records without options applied (only for responses to the find method).
    • include: An object keyed by type, valued by arrays of included records.

The resolved response object should always be an instance of a response type.

Fortune.find(type, [ids], [options], [include], [meta])Promise#

The find method retrieves record by type given IDs, querying options, or both. This is a convenience method that wraps around the request method, see the request method for documentation on its arguments.

Fortune.create(type, records, [include], [meta])Promise#

The create method creates records by type given records to create. This is a convenience method that wraps around the request method, see the request method for documentation on its arguments.

Fortune.update(type, updates, [include], [meta])Promise#

The update method updates records by type given update objects. See the Adapter.update method for the format of the update objects. This is a convenience method that wraps around the request method, see the request method for documentation on its arguments.

Fortune.delete(type, [ids], [include], [meta])Promise#

The delete method deletes records by type given IDs (optional). This is a convenience method that wraps around the request method, see the request method for documentation on its arguments.

Fortune.connect()Promise#

This method does not need to be called manually, it is automatically called upon the first request if it is not connected already. However, it may be useful if manually reconnect is needed. The resolved value is the instance itself.

Fortune.disconnect()Promise#

Close adapter connection, and reset connection state. The resolved value is the instance itself.


Adapter (adapter/index.js)

class Adapter#

Adapter is an abstract base class containing methods to be implemented. All records returned by the adapter must have the primary key id. The primary key MUST be a string or a number.

It has one static property, DefaultAdapter which is a reference to the memory adapter.

new Adapter()#

The Adapter should not be instantiated directly, since the constructor function accepts dependencies. The keys which are injected are:

  • recordTypes: an object which enumerates record types and their definitions.
  • options: the options passed to the adapter.
  • common: an object containing all internal utilities.
  • errors: same as static property on Fortune class.
  • keys: an object which enumerates reserved constants for record type
  • message: a function with the signature (id, language, data).

These keys are accessible on the instance (this).

An adapter may expose a features static property, which is an object that can contain boolean flags. These are used mainly for checking which additional features may be tested.

  • logicalOperators: whether or not and and or queries are supported.

Adapter.connect()Promise#

The responsibility of this method is to ensure that the record types defined are consistent with the backing data store. If there is any mismatch it should either try to reconcile differences or fail. This method SHOULD NOT be called manually, and it should not accept any parameters. This is the time to do setup tasks like create tables, ensure indexes, etc. On successful completion, it should resolve to no value.

Adapter.disconnect()Promise#

Close the database connection.

Adapter.create(type, records, [meta])Promise#

Create records. A successful response resolves to the newly created records.

IMPORTANT: the record must have initial values for each field defined in the record type. For non-array fields, it should be null, and for array fields it should be [] (empty array). Note that not all fields in the record type may be enumerable, such as denormalized inverse fields, so it may be necessary to iterate over fields using Object.getOwnPropertyNames.

Adapter.find(type, [ids], [options], [meta])Promise#

Find records by IDs and options. If IDs is undefined, it should try to return all records. However, if IDs is an empty array, it should be a no-op. The format of the options may be as follows:

{
  sort: { ... },
  fields: { ... },
  exists: { ... },
  match: { ... },
  range: { ... },

  // Limit results to this number. Zero means no limit.
  limit: 0,

  // Offset results by this much from the beginning.
  offset: 0,

  // The logical operator "and", may be nested. Optional feature.
  and: { ... },

  // The logical operator "or", may be nested. Optional feature.
  or: { ... },

  // Reserved field for custom querying.
  query: null
}

For the fields exists, match, and range, the logical operator should be "and". The query field may be used on a per adapter basis to provide custom querying functionality.

The syntax of the sort object is as follows:

{
  age: false, // descending
  name: true // ascending
}

Fields can be specified to be either included or omitted, but not both. Use the values true to include, or false to omit. The syntax of the fields object is as follows:

{
  name: true, // include this field
  age: true // also include this field
}

The exists object specifies if a field should exist or not (true or false). For array fields, it should check for non-zero length.

{
  name: true, // check if this fields exists
  age: false // check if this field doesn't exist
}

The syntax of the match object is straightforward:

{
  name: 'value', // exact match or containment if array
  friends: [ 'joe', 'bob' ] // match any one of these values
}

The range object is used to filter between lower and upper bounds. It should take precedence over match. For array fields, it should apply on the length of the array. For singular link fields, it should not apply.

{
  range: { // Ranges should be inclusive.
    age: [ 18, null ], // From 18 and above.
    name: [ 'a', 'd' ], // Starting with letters A through C.
    createdAt: [ null, new Date(2016, 0) ] // Dates until 2016.
  }
}

The return value of the promise should be an array, and the array MUST have a count property that is the total number of records without limit and offset.

Adapter.update(type, updates, [meta])Promise#

Update records by IDs. Success should resolve to the number of records updated. The updates parameter should be an array of objects that correspond to updates by IDs. Each update object must be as follows:

{
  // ID to update. Required.
  id: 1,

  // Replace a value of a field. Use a `null` value to unset a field.
  replace: { name: 'Bob' },

  // Append values to an array field. If the value is an array, all of
  // the values should be pushed.
  push: { pets: 1 },

  // Remove values from an array field. If the value is an array, all of
  // the values should be removed.
  pull: { friends: [ 2, 3 ] },

  // The `operate` field is specific to the adapter. This should take
  // precedence over all of the above. Warning: using this may bypass
  // field definitions and referential integrity. Use at your own risk.
  operate: null
}

Things to consider:

  • push and pull can not be applied to non-arrays.
  • The same value in the same field should not exist in both push and pull.

Adapter.delete(type, [ids], [meta])Promise#

Delete records by IDs, or delete the entire collection if IDs are undefined or empty. Success should resolve to the number of records deleted.

Adapter.beginTransaction()Promise#

Begin a transaction to write to the data store. This method is optional to implement, but useful for ACID. It should resolve to an object containing all of the adapter methods.

Adapter.endTransaction([error])Promise#

End a transaction. This method is optional to implement. It should return a Promise with no value if the transaction is completed successfully, or reject the promise if it failed.

Adapter.applyOperators(record, operators)Object#

Apply operators on a record, then return the record. If you make use of update operators, you should implement this method so that the internal implementation of update requests get records in the correct state. This method is optional to implement.


HTTP Server (fortune-http)

FortuneHTTP.createListener(instance, [options])Function#

Node.js only: This function implements a HTTP server for Fortune.

const http = require('http')
const fortuneHTTP = require('fortune-http')

const listener = fortuneHTTP(fortuneInstance, options)
const server = http.createServer((request, response) =>
  listener(request, response)
  .catch(error => {
    // error logging
  }))

It determines which serializer to use, assigns request headers to the meta object, reads the request body, and maps the response from the request method on to the HTTP response. The listener function ends the response and returns a promise that is resolved when the response is ended. The returned promise may be rejected with the error response, providing a hook for error logging.

The options object may be formatted as follows:

{
  // An array of HTTP serializers, ordered by priority. Defaults to ad hoc
  // JSON and form serializers if none are specified. If a serializer value
  // is not an array, its settings will be considered omitted.
  serializers: [
    [
      // A function that subclasses the HTTP Serializer.
      HttpSerializerSubclass,

      // Settings to pass to the constructor, optional.
      { ... }
    ]
  ],
  settings: {
    // By default, the listener will end the response, set this to `false`
    // if the response will be ended later.
    endResponse: true,

    // Use compression if the request `Accept-Encoding` header allows it.
    // Note that Buffer-typed responses will not be compressed. This option
    // should be disabled in case of a reverse proxy which handles
    // compression.
    useCompression: true,

    // Use built-in ETag implementation, which uses CRC32 for generating
    // weak ETags under the hood. This option should be disabled in case of
    // a reverse proxy which handles ETags.
    useETag: true,

    // Ensure that the request is sent at an acceptable rate, to prevent
    // abusive slow requests. This is given in terms of kilobits per second
    // (kbps). Default: `28.8`, based on slow modem speed.
    minimumRateKBPS: 28.8,

    // Ensure that requests can not be larger than a specific size, to
    // prevent abusive large requests. This is given in terms of megabytes
    // (MB). Default: `2`, based on unformatted 3.5" floppy disk capacity.
    // Use a falsy value to turn this off (not recommended).
    maximumSizeMB: 2,

    // How often to check for request rate in milliseconds (ms).
    // Default: 3000.
    rateCheckMS: 3000
  }
}

The main export contains the following keys:

  • Serializer: HTTP Serializer class.
  • JsonSerializer: JSON over HTTP serializer.
  • HtmlSerializer: HTML serializer.
  • FormDataSerializer: Serializer for multipart/formdata.
  • FormUrlEncodedSerializer: Serializer for application/x-www-form-urlencoded.
  • instantiateSerializer: an internal function with the signature (instance, serializer, options), useful if one needs to get an instance of the serializer without the HTTP listener.

HTTP Serializer (fortune-http)

class HttpSerializer#

Node.js only: HttpSerializer is an abstract base class containing methods to be implemented.

new HttpSerializer()#

The HttpSerializer should not be instantiated directly, since the constructor function accepts dependencies. The keys which are injected are:

  • methods: same as static property on Fortune class.
  • errors: same as static property on Fortune class.
  • keys: an object which enumerates reserved constants for record type definitions.
  • recordTypes: an object which enumerates record types and their definitions.
  • castValue: a function with the signature (value, type, options), useful for casting arbitrary values to a particular type.
  • options: the options passed to the serializer.
  • adapter: a reference to the adapter instance.
  • message: a function with the signature (id, language, data).
  • Promise: the Promise implementation.
  • settings: settings from the Fortune instance.
  • documentation: documentation from the Fortune instance.

These keys are accessible on the instance (this).

HttpSerializer.processRequest(contextRequest, request, response)Promise | Object#

This method is run first, and it is optional to implement. The default implementation is typically used so that it may interoperate with other serializers. The purpose is typically to read and mutate the request before anything else happens. For example, it can handle URI routing and query string parsing. The arguments that it accepts beyond the required contextRequest are the request and response arguments from the Node.js HTTP listener.

It should return either the context request or a promise that resolves to the context request. The expectation is that the request is mutated except for the payload, which should be handled separately.

HttpSerializer.processResponse(contextResponse, request, response)Promise | Object#

This gets run last. The purpose is typically to read and mutate the response at the very end, for example, stringifying an object to be sent over the network. The arguments that it accepts beyond the required contextResponse are the request and response arguments from the Node.js HTTP listener.

It should return either the context response or a promise that resolves to the context response. The expectation is that there is a key on the context response, payload, which is either a string or a buffer, or else Node.js doesn't know how to respond.

HttpSerializer.parsePayload(contextRequest, request, response)Promise | an array of Objects#

Parse a request payload for creating or updating records. This method should return either an array of records as expected from the adapter.create method, or an array of update object as expected from the adapter.update. method. It may also mutate the context request object.

static HttpSerializer.mediaType#

A serializer must have a static property mediaType, which MUST be a string.


WebSocket (fortune-ws)

FortuneWS.createServer(instance, [change], [options], [callback])Server#

Node.js only: This function returns a WebSocket server that implements the Fortune wire protocol. The options are the same as those documented in the documentation for the ws module.

The wire protocol is based on MessagePack. The client may send two kinds of requests: setting state within the connection, and making a request to the Fortune instance. Each client request MUST include an ID for correlating a response to a request. For example, requesting a state change would look like:

{ id: 'xxx', state: { ... } } // MessagePack encoded.

The format is identical in the response for a state change.

Making a request to the instance is similar, and has the same parameters as the request method:

{ id: 'xxx', request: { ... } } // MessagePack encoded.

When a request succeeds, the client receives the response like so:

{ id: 'xxx', response: { ... } } // MessagePack encoded.

The change callback function gets invoked either when a change occurs within the Fortune instance, or when the client requests a state change. If it's an internal change, it is invoked with the current state and changes, otherwise if it's a connection state change, it does not have a second argument. For an internal change, the return value of this function determines either what gets sent to the client, which may be falsy to send nothing. For connection state change, the return value should be what gets assigned over the current state. It may also return a Promise. For example:

function change (state, changes) {
  return new Promise((resolve, reject) => {
    if (!changes) {
      // Accept only changes to the `isListening` key.
      return resolve({ isListening: Boolean(state.isListening) })
    }
    // Determine what changes should be relayed to the client,
    // based on the current state.
    return resolve(state.isListening ? changes : null)
  })
}

The changes are relayed to the client like so:

{ changes: { ... } } // MessagePack encoded.

If any request fails, the client receives a message like so:

{ id: 'xxx', error: '...' } // MessagePack encoded.

The returned Server object has an additional key stateMap, which is a WeakMap keyed by WebSocket connection, and valued by connection state. This may be useful for external connection handlers.

Note that by default, this is a single-threaded implementation. In order to scale past a single instance, inter-process communication (IPC) is necessary.

FortuneWS.request(client, [options], [state])Promise#

Given a W3C WebSocket client, send a request using the Fortune wire protocol, and get a response back as a Promise. This will not create a client, it needs to be created externally, and this method will automatically wait if it is not connected yet. For example:

// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
var client = new WebSocket(url, protocols)
fortune.net.request(client, options)

The options object is exactly the same as that defined by fortune.request, and the state object is an arbitrary object to send to request a state change. Either options or state must be passed.

FortuneWS.sync(client, instance, [merge])Function#

Given a W3C WebSocket client and an instance of Fortune, try to synchronize records based on the changes data pushed from the server. This function returns the event listener function.

When a sync is completed, it emits the sync event with the changes data, or the failure event if something failed.

Optionally, a merge function may be passed, which accepts one argument, the remote changes, and is expected to return the changes to accept. This is useful for preventing remote changes from overriding local changes.