Skip to content

Data Proxy

ahanusa edited this page Jan 28, 2019 · 32 revisions

The Data Proxy is the actor within the peasy-js framework that is responsible for data storage and retrieval, and serves as an abstraction layer for data stores that encompass (but not limited to) the following:

Possible implementations

  • Relational Databases - SQLite, MySQL, Oracle, SQL Server, etc.
  • Document (NoSQL) Databases - MongoDB, VelocityDB, etc.
  • Services - HTTP, SOAP, etc.
  • Cache Stores - Redis, Azure, etc.
  • Queues - RabbitMQ, MSMQ, etc.
  • File System
  • In-memory data stores for testing

Abstracting the data store layer allows you to swap data proxy implementations within your business services to deliver solutions that are scalable, testable, and support a multitude of deployment scenarios, all while being able to have them subjected to the command execution pipeline.

Public functions

getAll()

Asynchronously returns all values from a data source and is especially useful for lookup data.

getById(id)

Accepts the id of the entity that you want to query and asynchronously returns an object.

insert(object)

Accepts an object and asynchronously inserts it into the data store. This method should return a new object instance with updated state.

update(object)

Accepts an object and asynchronously updates it in the data store. This method should return a new object instance with updated state.

destroy(id)

Accepts the id of the entity that you want deleted and asynchronously deletes it from the data store.

Creating a Data Proxy

Creating a data proxy is simple. Create a constructor function or class with the functions which you choose to support. These functions will then be consumed and invoked by a command based on the successful result of rule executions.

Below are sample implementations written using different javascript flavors:

MongoDB implementation sample (ES5)

The following code sample shows what an implementation of data proxy serving as a customer data store might look like using MongoDB and ES5 syntax.

Please note that this code might be written more efficiently and has been scaled back for brevity and serves only as an example:

var mongodb = require('mongodb').MongoClient;
var objectId = require('mongodb').ObjectID;

var customerDataProxy = function() {

  var connectionString = 'mongodb://localhost:12345/orderEntry';

  return {
    getAll: getAll,
    getById: getById,
    insert: insert,
    update: update,
    destroy: destroy
  };

  function getAll(done) {
    return new Promise((resolve, reject) => {
      mongodb.connect(connectionString, function(err, db) {
        if (err) { return reject(err); }
        var collection = db.collection('customers');
        collection.find({}).toArray(function(err, data) {
          db.close();
          resolve(data);
        });
      });
    });
  };

  function getById(id, done) {
    return new Promise((resolve, reject) => {
      mongodb.connect(connectionString, function(err, db) {
        if (err) { return reject(err); }
        var collection = db.collection('customers');
        var oId = new objectId(id);
        collection.findOne({_id: oId}, function(err, data) {
          db.close();
          resolve(data);
        });
      });
    });
  };

  function insert(data, done) {
    return new Promise((resolve, reject) => {
      mongodb.connect(connectionString, function(err, db) {
        if (err) { return reject(err); }
        var collection = db.collection('customers');
        collection.insert(data, function(err, data) {
          db.close();
          resolve(data);
        });
      });
    });
  };

  function update(data, done) {
    return new Promise((resolve, reject) => {
      mongodb.connect(connectionString, function(err, db) {
        if (err) { return reject(err); }
        var collection = db.collection('customers');
        collection.update({_id: objectId(data._id)}, data, function(err, data) {
          db.close();
          resolve(data);
        });
      });
    });
  };

  function destroy(id, done) {
    return new Promise((resolve, reject) => {
      mongodb.connect(connectionString, function(err, db) {
        if (err) { return reject(err); }
        var collection = db.collection('customers');
        collection.remove({_id: objectId(id)}, function(err, data) {
          db.close();
          resolve();
        });
      });
    });
  };

};

In-memory data store implementation sample (ES6)

Below is an example of an in-memory data proxy implementation using ES6 syntax.

Please note that this code might be written more efficiently and has been scaled back for brevity and serves only as an example:

class PersonDataProxy {

  constructor() {
    this._data = [];
  }

  getById(id) {
    var person = this._findBy(id);
    return Promise.resolve(person);
  }

  getAll() {
    return Promise.resolve(this._data);
  }

  insert(data) {
    data.id = this._data.length + 1;
    var newPerson = Object.assign({}, data);
    this._data.push(newPerson);
    return Promise.resolve(data);
  }

  update(data) {
    var person = this._findBy(data.id);
    Object.assign(person, data);
    return Promise.resolve(data);
  }

  destroy(id) {
    var person = this._findBy(id);
    var index = this._data.indexOf(person);
    this._data.splice(index, 1);
    return Promise.resolve();
  }

  _findBy(id) {
    var person = this._data.filter((function(p) {
      return p.id === id;
    }))[0];
    return person;
  }

}

HTTP implementation sample (TypeScript)

The following code sample shows what an implementation of an HTTP data proxy using TypeScript might look like. This proxy could be used to invoke HTTP operations against an endpoint representing people.

Note that unlike the previous ES5 and ES6 implementations, we must implement all of the data proxy methods of the IDataProxy interface.

Also note that this code might be written more efficiently and has been scaled back for brevity and serves only as an example:

import { Person } from './contracts';
import { IDataProxy } from 'peasy-js';
import axios from 'axios';

export class PersonHttpDataProxy implements IDataProxy<Person, number> {

  private baseUri: string = "https://myapi/people";

  getAll(): Promise<Person[]> {
    return axios.get(this.baseUri).then(result => result.data);
  }

  getById(id: number): Promise<Person> {
    return axios.get(`${this.baseUri}/${id}`).then(result => result.data);
  }

  insert(data: Person): Promise<Person> {
    return axios.post(this.baseUri, data).then(result => result.data);
  }

  update(data: Person): Promise<Person> {
    return axios.put(`${this.baseUri}/${data.id}`, data).then(result => result.data);
  }

  destroy(id: number): Promise<void> {
    return axios.delete(`${this.baseUri}/${id}`).then(result => result.data);
  }

}

Swappable Data Proxies

Because business services have a dependency upon the data proxy abstraction, this means that data proxies can be swapped out and replaced with different implementations. The ability to swap data proxies provides the following benefits:

In many production systems, backend node.js applications are often written to communicate directly with a database. This configuration can lead to bottlenecks and poor performance over time as client consumption increases. peasy-js allows you to easily remedy this type of situation by swapping a data proxy that communicates directly with a database with one that scales more efficiently.

For example, instead of injecting data proxies that communicate directly with a database, you might inject data proxies that communicate with a cache or queue. For retrieval, the cache might return cached data, and for storage, you might update the cache or a queue and have a backend process/service monitoring it for changes. The process/service would then be responsible for data storage manipulation against a persistent data store (database, file system, etc).

Another possible solution would be to expose CRUD operations via HTTP services, and inject a data proxy capable of performing CRUD actions against your HTTP services into your business services. A side benefit of having clients use HTTP services is that you could gain the benefits of HTTP Caching, almost for free, which can provide scalability gains.

Because business services rely on data proxy implementations for data store abstraction, introducing solutions that scale become trivial.

Multiple deployment scenarios

Because data proxies are swappable, you are able to reconfigure data storage without having to refactor your code. Let's take a scenario where an organization has deployed a node.js application that directly communicates with a database. As time passes, the organization receives third party pressure to expose the database to the outside world via web services (HTTP, SOAP, etc.).

After the web services are created, it is decided that the node.js application should dogfood the web services and no longer communicate directly with the database. Consuming business services that abstract data stores easily allow swapping out the database data proxies with proxies that communicate with the web services.

Another example might be supporting an online/offline mode in the client. In this scenario, business and validation logic must always remain, however, the datastore might need to persist data to http services or a local cache, respectively. Simply injecting the correct data proxy implementation into a business service in the time of need could help to easily achieve this workflow almost effortlessly.

Testability

Unit testing should be fast. Really fast. Databases are slow and come with a slew of issues when testing your code via automated unit tests. This is another scenario where swappable data proxies provide great benefit. By creating mock data proxies, you can easily inject them into your business services and focus on testing initialization logic, validation and business rule logic, and command logic.