/liquidfire-spectre

A place for entities.

Primary LanguageJavaScript

liquidfire:Spectre

Where else would you go to find entities? What is an entity? It's easy;

an entity is an object that represents a record/document in a database.

Isn't that a model?

I think you mean "isn't that a data model?" Most modern frameworks use the term "model" to define what is truly an "entity." A model is where you house your "business logic." It is what makes your app go. Many times a model will work with or on an entity, but not always.

The Layers

Before we dive in too much, lets look at the the layers we will be working with:

Stores

In order to create or query for an Entity, you must go through a Store. The store layer handles all communication between you and whatever data store you are using. It currently only supports Mongodb, but adding support for anything is really easy.

Entities

The entities themselves are the objects that represent the record/document in the database (or whatever, really). They are meant to house the state of a particular thing (like a user, automobile, whatevs).

How do these work together?

Let me show you an entity (and entity store) in action.

I'm going to assume you have created an app using Sockets or Alfred or something similar.

1. Create entities/user/User.js and drop this in:

define(['altair/facades/declare',
        'apollo/_HasSchemaMixin'
], function (declare, _HasSchemaMixin) {


    return declare([_HasSchemaMixin], {

    });

});

2. Create entities/user/schema.json and drop this in:

{
    "name":       "Users",
    "tableName":  "users",
    "properties": {

        "_id": {
            "type":    "primary",
            "options": {
                "label": "Id"
            }
        },

        "firstName": {
            "type":    "string",
            "options": {
                "label": "First Name"
            }
        },

        "lastName": {
            "type":    "string",
            "options": {
                "label": "Last Name"
            }
        },

        "email": {
            "type":    "string",
            "options": {
                "label": "email"
            }
        }

    }
}

Using the store to find an entity (async)

this.entity('User').then(function (store) {

    //the User store is where you'll find all your users. A store as database agnostic
    return store.find().where('state', '==', 'CO').sortBy('firstName', 'ASC').thenBy('lastName', 'DESC').skip(10).limit(10).execute();

}).then(function (user) {

    if(!user) {
        throw new Error('user not found!');
    } else {

        //since entities use apollo/_HasSchemaMixin, the familiar get/set/setValues/getValues/etc. are available.
        return user.set('firstName', 'tay')
                   .save(); //every entity is extended with save(), it returns a Promise.


    }

}).then(function (user) {

    //the first name is now updated
    console.log(user.get('firstName'), 'updated');

});

##Creating your first entity An entity is a generic AMD module that mixes in apollo/_HasSchemaMixin.

this.entity('User').then(function (store) {

    var user = store.create({
        firstName: 'tay',
        lastName: 'ro'
    });

    user.set('email', 'tay@ro.com');

    console.log(user.values);

});

##Overridding the store Lets say you want a utility method on your store to do something more than just find() and create(). What if we wanted a findAdminUsers(). This is a lazy example, don't use it for reals.

Create stores/User.js and drop this in:

define(['altair/facades/declare',
    'liquidfire/modules/spectre/db/Store'
], function (declare, Store) {

    return declare([Store], {

        findAdminUsers: function () {

            return this.find().where('isAdmin', '===', true);

        }

    });

});

Now you can do the following:

this.entity('User').then(function (store) {

    return store.findAdminUsers().execute();

}).then(function (users) {

    return users.each().step(function (user) {

        console.log(user.get('firstName'), 'is an admin user');

    });

});

Iterating over search results

this.entity('User').then(function (store) {

    return store.find().execute();

}).then(function (users) {

    return users.each().step(function (user) {

        console.log(user.get('firstName'), 'is an admin user');

    });

});

##Custom Statement You can customize how a database Statement works from a store pretty easily. Here is a real world example of a custom Store with a custom Statement.

define(['altair/facades/declare',
        'lodash',
        'liquidfire/modules/spectre/db/Store',
        'altair/cartridges/database/Statement',
        'altair/cartridges/database/cursors/Array'
], function (declare, _, Store, Statement, ArrayCursor) {


    return declare([Store], {

        /**
         * Helper to find tickets by auction (and temporarily uses the legacy rest adapter to fetch results)
         *
         * @param auction
         * @returns {Statement}
         */
        findByAuction: function (auction) {

            var statement = new Statement(function (q) {

                return this.nexus('handbid:LegacyRest').rest().get('tickets', { auctionKey: auction.get('key') }).then(function (response) {

                    //convert array of ticket objects to ticket Entities (with .get(), .set(), .save(), .delete(), etc.)
                    var tickets = _.map(response, this.create, this),
                        cursor = new ArrayCursor(tickets, statement); //a statement always returns an array

                    return cursor;

                }.bind(this));

            }.bind(this));

            return statement;

        }


    });

});

Quick REST endpoints using the SearchModel

When you need to create an endpoint to search entities with all the fancy skip, limit, search term, etc. you can use liquidfire:Spectre/models/Search to get very far very fast. Inside your controller, you can:

startup: function (e) {

    //mixin dependencies
    this.defered = this.all({
        _search:            this.model('liquidfire:Spectre/models/Search', null, { parent: this }) //using `this` as parent makes this.entity() behave relative to our controller
    }).then(function (deps) {

        declare.safeMixin(this, deps);
        
        return this;

    }.bind(this));
    
    return this.inherited(arguments);
    
},

users: function (e) {
    return this._search.findFromEvent('User', e);

}

That's it. Now you can call /v1/rest/your/endpoint and pass values in the query string to customize your results. Example: `/v1/rest/users?perPage=20&page=2&sortField=name&sortDirection=DESC

  • perPage: how many results to return at once (defaults to 10, max 100)
  • page: the page we are on
  • sortField: the field to sort on
  • sortDirection: ASC or DESC
  • searchField: anything passed to searchValue will search against this field
  • searchValue: the search string

Using the search model without an event

When you need to perform a search in a consistent way, the search model is perfect.

var search = this.model('liquidfire:Spectre/models/Search', null, { parent: this });

//no options are required, this list is just to show everything you can do
search.find('User', {
    page: 0,
    perPage: 10,
    sort: {
        lastName: 'ASC'
    },
    query: {
        state: 'CO'
    },
    //the above is equivalent to the following
    searchField: 'state',
    searchValue: 'CO'
});

Entities in your App

If you are building an app (through Alfred or Sockets), you can use the Lifecycle class to load all your stores right in startup().

{
    startup: function () {

        //get all our stores ready
        this.mixin({
            parts:     this.entity('Part'),
            sets:      this.entity('Set'),
            variants:  this.entity('Variant'),
            vendors:   this.entity('Vendor'),
            users:     this.entity('User')
        });

        return this.inherited(arguments);
    }
    
}

Then, from one of your Controllers you can do the following.

{
    
    someAction: function (id, cb) {
    
        this.parts.findOne().where('_id', '===', id).execute().then(function (part) {
        
            cb(null, part ? part.geSocketValues() : null);
        
        }).otherwise(function (err) {
        
            cb(err.message || err);
        
        });
    
    }

}

Controller Utilities

When you are using a controller based solution (Sockets, Alfred, etc.), you find yourself creating/updating/searching/deleting entities often. From your controller, you have the following methods.

//type, values[,options]
this.createEntity('User', {
    firstName: 'Test',
    lastName: 'User'
}, { ... }).then(function (user) { ... }).otherwise(function (err) {

    //cb(_.isArray(err) ? err[0] : err.message || err);

});

//type, id, values[, options]
this.updateEntity('User', '3', { firstName: 'New Name' }).then(...)


//type, options (passthrough to `liquidfire:Spectre/models/Search`)
this.searchEntities('User', {
    query: { email: 'test@test.com' }
    transform: function (entity) { return entity.getSocketValues(); }
}).then(...)