Giter VIP home page Giter VIP logo

seraph-model's Introduction

seraph_model provides some convenient functions for storing and retrieving typed nodes from a neo4j database. It is intended to work with seraph.

### Quick example
var db = require('seraph')('http://localhost:7474')
var model = require('seraph-model');

var User = model(db, 'user');

User.save({ name: 'Jon', city: 'Bergen' }, function(err, saved) {
  if (err) throw err;

  User.findAll(function(err, allUsers) {
    // allUsers -> [{ name: 'Jon', city: 'Bergen', id: 0 }]
  });
  User.where({ city: 'Bergen' }, function(err, usersFromBergen) {
    // usersFromBergen -> all user objects with city == bergen
  });
})

seraph-model 0.5.2 works with Neo4j-2.0.0-M05 and Neo4j-2.0.0-M06.

To check if it works with your version, you should check out the repo, and change the Neo4j version at the start of the tests to the version you're running

Documentation

How to do things

Model instance methods

## Creating a new model

seraph_model(seraphDbObject, modelTypeName)

You can create a new model by calling the function returned by requiring seraph_model. There are no instances of this model, only objects, which are passed to the model itself in order to perform work on it. Much like seraph itself.

It works by indexing each object under a nodes index. Each different model is simply an item in that index, with all of the instances of that model attached to it.

Each model is also indexed by its id upon saving the first time. This ensures that when reading models, you do not read models of other types.

var db = require('seraph')('http://localhost:7474');
var Beer = require('seraph_model')(db, 'beer');

Beer.save({name: 'Pacific Ale', brewery: 'Stone & Wood'}, function(err, beer) {
  // saved!
});
## Adding preparers

Preparers are functions that are called upon an object to transform it before saving it. A preparer is a function that takes an object and a callback, and calls back with an error and the updated object.

Preparers can also do validation. If a preparer returns an error, it will be passed to the save callback as if it were a validation error. However, if you just want to do validation and not mutate the object, use a validator instead.

Preparers are called before validators.

You can manually prepare an object by using the model.prepare function.

Example

var prepareFileSize = function(object, callback) {
  fs.stat(object.file, function(err, stat) {
    if (err) return callback('There was an error finding the file size');
    object.filesize = stat.size;
    callback(null, object);
  });
}

model.on('prepare', prepareFileSize);

model.save({file: 'foo.txt'}, function(err, object) {
  // object -> { file: 'foo.txt', filesize: 521, id: 0 }
});

mode.save({file: 'nonexistant.txt'}, function(err, object) {
  // err -> 'There was an error finding the file size'
});
## Adding validators __Validators__ are functions that are called with an object before it is saved. If they call back with anything that is not falsy, the saving process is halted, and the error from the validator function is returned to the save callback.

Validators are called after preparers.

You can manually validate an object by using the model.validate function.

Example

var validateAge = function(person, callback) {
  if (object.age >= 21) {
    callback();
  } else {
    callback('You must be 21 or older to sign up!');
  }
}

model.on('validate', validateAge);

model.save({ name: 'Jon', age: 23 }, function(err, person) {
  // person -> { name: 'Jon', age: 23, id: 0 }
});

model.save({ name: 'Jordan', age: 17 }, function(err, person) {
  // err -> 'You must be 21 or older to sign up!'
});
## Adding indexes

You can add any number of indexes to add an object to upon saving by using the addIndex function. Objects are only indexed the first time they are saved, but you can manually index an object by calling the index function.

They keys and values passed to addIndex can be computed, but that is optional. If they are computed, you must pass the resultant key or value to a callback, rather than returning it (this gives you the opportunity to do asynchronous calculations at this point).

You also have the option of passing a function to determine weather or not the index is used at all.

Example

With static keys/values

model.addIndex('wobblebangs', 'bangs', 'wobbly');

With computed value

model.addIndex('uniquely_identified_stuff', 'stuff', function(obj, cb) {
  cb(null, createUuid());
});

With computed key and value

model.addIndex('things',
  function(obj, cb) { cb(null, obj.model); },
  function(obj, cb) { cb(null, obj.id); });

With conditional indexing

model.addIndex('some_stuff', 'things', 'cool', function(obj, cb) {
  var isCoolEnough = obj.temperature < 20;
  cb(null, isCoolEnough); //objs with `temperature` >= 20 are not indexed
});
## Save events

There's a few events you can listen on:

  • beforeSave fired after preparation and validation, but before saving.
  • afterSave fired after saving and indexing.
model.on('beforeSave', function(obj) {
  console.log(obj, "is about to be saved");
})
## Setting a properties whitelist

Fields are a way of whitelisting which properties are allowed on an object to be saved. Upon saving, all properties which are not in the whitelist are stripped. Composited properties are automatically whitelisted.

beer.fields = ['name', 'brewery', 'style'];

beer.save({
  name: 'Rye IPA', 
  brewery: 'Lervig', 
  style: 'IPA',
  country: 'Norway'
}, function(err, theBeer) {
  // theBeer -> { name: 'Rye IPA', brewery: 'Lervig', style: 'IPA', id: 0 }
})
## Composition of Models.

Composition allows you to relate two models so that you can save nested objects faster, and atomically. When two models are composed, even though you might be saving 10 objects, only 2 api calls (saving & indexing) will be made, just as if you were only saving 1.

With this, you can also nest objects, which can make your life a bit easier when saving large objects.

Composited objects will also be implicitly retrieved when reading from the database, to infinite depth. The number of read API calls is variable, and will expand depending on the level and complexity of your compositions.

example

var beer = model(db, "Beer");
var hop = model(db, "Hop");

beer.compose(hop, 'hops', 'contains_hop');

var pliny = {
  name: 'Pliny the Elder',
  brewery: 'Russian River',
  hops: [
    { name: 'Columbus', aa: '13.9%' },
    { name: 'Simcoe', aa: '12.3%' },
    { name: 'Centennial', aa: '8.0%' }
  ]
};

// Since objects were listed on the 'hops' key that I specified, they will be 
// saved with the `hop` model, and then related back to my beer.
beer.save(pliny, function(err, saved) {
  // if any of the hops or the beer failed validation with their model, err
  // will be populated and nothing will be saved.
  
  console.log(saved); 
  /* -> { brewery: 'Russian River',
          name: 'Pliny the Elder',
          id: 11,
          hops: 
           [ { name: 'Columbus', aa: '13.9%', id: 12 },
             { name: 'Simcoe', aa: '12.3%', id: 13 },
             { name: 'Centennial', aa: '8.0%', id: 14 } ] }
  */

  db.relationships(saved, function(err, rels) {
    console.log(rels) // -> [ { start: 11, end: 12, type: 'contains_hop', properties: {}, id: 0 },
                      // { start: 11, end: 13, type: 'contains_hop', properties: {}, id: 1 },
                      // { start: 11, end: 14, type: 'contains_hop', properties: {}, id: 2 } ]
  });

  // Read directly with seraph
  db.read(saved, function(err, readPlinyFromDb) {
    console.log(readPliny)
    /* -> { brewery: 'Russian River',
            name: 'Pliny the Elder',
            id: 11 }
    */
  })

  // Read with model, and you get compositions implicitly.
  beer.read(saved, function(err, readPliny) {
    console.log(readPliny)
    /* -> { brewery: 'Russian River',
            name: 'Pliny the Elder',
            id: 11,
            hops: 
             [ { name: 'Columbus', aa: '13.9%', id: 12 },
               { name: 'Simcoe', aa: '12.3%', id: 13 },
               { name: 'Centennial', aa: '8.0%', id: 14 } ] }
    */
  });

  hop.read(14, function(err, hop) {
    console.log(hop); // -> { name: 'Centennial', aa: '8.0%', id: 14 }
  });
});

You can use the regular model.save function to update a model with compositions on it. If the compositions differ from the previous version of the model, the relationships to the previously composed nodes will be deleted but the nodes themselves will not be. If you want to update the base model but don't want the overhead that the compositions involves, you can use model.save with excludeCompositions set to true. See the model.save docs for more info.

A couple of alternatives for updating compositions exist: model.push for pushing a single object to a composition without having to first read the model from the database, and model.saveComposition for updating an entire composition in one go.

model.compose(composedModel, key, relationshipName[, opts])

Add a composition.

  • composedModel — the model which is being composed
  • key — the key on an object being saved which will contained the composed models.
  • relationshipName — the name of the relationship that is created between a root model and its composed models. These relationships are always outgoing.
  • opts - an object with a set of options. possible options are documented below.

composition options

  • many (default = false) — whether this is a *-to-many relationship. If truthy, the this composition will always be represented as an array on the base object.
  • orderBy (default = null) - how this composition should be ordered. This can be set to either the name of a property on the composed node to order with (ascending), or an object with the name of the property value and the order direction. Possible values might include: 'age', {property: 'age', desc: true}, {property: 'age', desc: false}.

model.readComposition(objectOrId, compositionKey, callback)

Read a single composition from a model.

  • objectOrId — an id or an object that contains an id that refers to a model.
  • compositionKey – the composition which to retrieve.
  • callback — callback for result, format (err, resultingComp). resulingComp will either be an array of composed objects or a single object if there was only one

Example (from the above context)

beer.readComposition(pliny, 'hops', function(err, hops) {
  console.log(hops); 
  /* [ { name: 'Columbus', aa: '13.9%', id: 12 },
      { name: 'Simcoe', aa: '12.3%', id: 13 },
      { name: 'Centennial', aa: '8.0%', id: 14 } ]  */
});
## Setting a unique key or index

In neo4j, you can enforce uniqueness of nodes by associating them with an index. There's two ways of doing this with seraph-model: by specifying a key in the model to index upon, or by specifying the index yourself. See the examples below for specifying a unique index for a node.

Note that there is one in particular "gotcha" with enforced uniqueness on composed models: in the event that you try to add a new object and there is already an object indexed the same way, an error will be thrown. Unfortunately, due to a bug with neo4j's batch API, and the fact that composed models always save in a batch, this means that a statusCode of 500 will be returned. There is in fact no good way to determine that such an error is, in fact, the result of a conflict, yet.

Unique Key

Specifying a unique key will automatically index your node under a new index, using that key in each saved model. The index is named after your model's type property. For example, a model with model.type = 'car' will be added under the index cars. The index name is automatically pluralized from the model type name.

If you specified the key as model, then each time an object is saved it is indexed (in this example) in the cars index, under the key model, with the value of whatever model was set to.

Setting a unique key also automatically adds a validator checking that the indexed key was set on every object that is saved. An object will not be able to save without that key being set.

For example:

var Car = model(db, 'car');
Car.setUniqueKey('model');
Car.save({make: 'Citroën', model: 'DS4'}, function(err, ds4) {
  // ds4 -> { id: 1, make: 'Citroën', model: 'DS4' }
  // node 1 is now indexed in neo4j under `cars(model="DS4")`
  Car.save({make: 'Toyota', model: 'DS4'}, function(err, otherDs4) {
    // err.statusCode -> 409 (conflict)
  });
});

Car.save({make: 'Subaru'}, function(err, subaru) {
  // err -> 'The `model` key is not set, but is required to save this object'
});

You can also specify that instead of returning a conflict error, that you want to just return the old object when you attempt to save a new one at the same index. For example:

var Tag = model(db, 'tag');
Tag.setUniqueKey('tag');
Tag.save({tag: 'finnish'}, function(err, tag) {
  // tag -> { id: 1, tag: 'finnish' }
  
  // presumably later on, someone wants to save the same tag 
  Tag.save({tag: 'finnish'}, function(err, tag) {
    // instead of saving another tag 'finnish', the first one was returned
    // tag -> { id: 1, tag: 'finnish' }
  });
});

Unique Index

In case you want your unique index to be a little more involved than just using a value from the model, you can define your own unique index. The function you use to do this is model.setUniqueIndex, and it takes similar arguments to model.addIndex.

Here's an example with the Car model shown above, which uses both the make and the model to uniquely index.

var Car = model(db, 'car');
Car.setUniqueIndex('cars', 'make_and_model', function(car, cb) {
  if (!car.make || !car.model) cb("A car should have both a make and a model!");
  else cb(null, car.make + ' ' + car.model);
});

Car.save({make: 'Citroën', model: 'DS4'}, function(err, ds4) {
  db.index.read('cars', 'make_and_model', 'Citroën DS4', function(err, car) { 
    // `ds4` was indexed under 'Citroën DS4'.
    assert.deepEqual(ds4, car);
  });
});
## Computed fields

Computed fields are fields on a model that exist transiently (they aren't stored in the database) and can be composed of other fields on the object or external information. You specify the field that you want to be computed, and the function that should be used to compute the value of that field for the model, and it will automatically be computed every time the model is read (and removed from the model just before saving). You can use the addComputedField to add a computed field.

Example:

var Car = model(db, 'car');
Car.addComputedField('name', function(car) {
  return car.make + ' ' + car.model;
});
Car.addComputedField('popularity', function(car, cb) {
  fetchPopularityRating(car.make, car.model, function(err, rating) {
    if (err) return cb(err);
    cb(null, rating.numberOfOwners);
  });
});

Car.save({ make: 'Citroën', model: 'DS4' }, function(err, car) {
  // car.name = 'Citroën DS4'
  // car.popularity = 8599
});
## Schemas

Schemas are a way of defining some constraints on a model and enforcing them while saving. An example of a schema might be:

var User = model(db, 'user');
User.schema = {
  name: { type: String, required: true },
  email: { type: String, match: emailRegex, required: true },
  age: { type: Number, min: 16, max: 85 },
  expiry: Date
}

Setting a schema will automatically use the keys of the schema as the model's fields property.

Each of the constraints and their behaviour are explained below.

### `type`

A type property on the schema indicates the type that this property should be. Upon saving, seraph-model will attempt to coerce properties that have a type specified into that type.

#### `'date'` or `Date`

Expects a date, and coerces it to a number using Date.getTime. Values will be parsed using Moment.js' date parser.

Examples of coercion:

new Date("2013-02-08T09:30:26")    ->   1360315826000
"2013-02-08T09:30:26"              ->   1360315826000
1360315826000                      ->   1360315826000
#### `'string'` or `String`

Expects a string. Values will be coerced to a string using .toString().

#### `'number'` or `Number`

Expects a number. Values will be coerced to a number. If the coercion results in NaN, validation will fail.

#### `'boolean'` or `Boolean`

Expects a boolean. Values that are not already a boolean will be coerced based on their truthiness (i.e. !!value), with the exception of '0' which is coerced to false.

#### `'array'` or `Array`

Expects an array. If the value is not an array, it will be coerced to an array by inserting the value into an array and returning that.

Examples of coercion:

[1,2,3]     -> [1,2,3]
'cat'       -> ['cat']
#### Other types

You can give your own types to check against. If type is set to a string value that is not one of the above, the value's type is checked with typeof value == type. If type is a function, the value's type is checked with value instanceof type.

### `default`

Supply a default value for this property. If the property is undefined or null upon saving, the property will be set to this value.

Default value: undefined. Example values: 'Anononymous User', 500, [1, 2, 3].

Example:

User.schema = {
  name: { default: 'Anonymous User' }
}
### `trim`

Trim leading/trailing whitespace from a string.

Default value: false. Example values: true, false.

### `uppercase`

Transform a string to uppercase.

Default value: false. Example values: true, false.

### `lowercase`

Transform a string to lowercase.

Default value: false. Example values: true, false.

### `required`

Ensure this property exists. Validation will fail if it null or undefined.

Default value: false. Example values: true, false.

### `match`

Values should match this regular expression. Validation will value if the value does not.

Default value: undefined. Example values: /^user/i, new RegExp("^user", "i").

### `enum`

Values should be one of the values in the enum. Validation will fail if the value is not in the enum.

Default value: undefined. Example values: ['male', 'female'], [10, 20, 30].

### `min`

Values should be greater than or equal to the given number. Validation will fail if the value is less.

Default value: undefined. Example values: 10, 0.05.

### `max`

Values should be less than or equal to the given number. Validation will fail if the value is greater.

Default value: undefined. Example values: 100, 0.95.

#### `model.save(object, [excludeCompositions,] callback(err, savedObject))`

Saves or updates an object in the database. The steps for doing this are:

  1. object is prepared using model.prepare
  2. object is validated using model.validate. If validation fails, the callback is called immediately with an error.
  3. object is saved using seraph.save
  4. object is indexed as this type of model using seraph.index

If excludeCompositions is truthy, any composed models attached to object will not be altered in the database (they will be ignored), and the object which is returned will exclude compositions.

#### `model.push(rootId, compName, object(s), callback(err, savedObject(s)))`

Pushes a single object as a composed model on the model represented by rootId. This does not read the database first so there is no danger of a race condition.

#### `model.saveComposition(rootId, compName, objects, callback(err, savedObjects))`

Updates a composition set on a model. The models composed under compName on the model will be replaced with those specified by objects. This can be a partial update if you have an already existing array of composited objects.

#### `model.read(idOrObject, callback(err, model))`

Reads a model from the database given an id or an object containing the id. model is either the returned object or false if it was not found.

#### `model.exists(idOrObject, callback(err, doesExist))`

Check if a model exists.

#### `model.findAll(callback(err, allOfTheseModels))`

Finds all of the objects that were saved with this type.

#### `model.where(callback(err, matchingModels))`

This is a operationally similar to seraph.find, but is restricted to searching for other objects indexed as this kind of model. See the quick example for an example of this in action.

#### `model.prepare(object, callback(err, preparedObject))`

Prepares an object by using the model.preparers array of functions to mutate it. For more information, see Adding preparers

#### `model.validate(object, callback(err, preparedObject))`

Validates that an object is ready for saving by calling each of the functions in the model.validators array. For more information, see Adding validators

#### `model.fields`

This is an array of property names which acts as a whitelist for property names in objects to be saved. If it is set, any properties in objects to be saved that are not included in this array are stripped. Composited properties are automatically whitelisted. See Setting a properties whitelist for more information and examples.

#### `model.setUniqueKey(keyName, [returnOldOnConflict = false])`

Sets the key to uniquely index this model on. Will also enforce that this key exists when you try to save a model.

See the using a unique key section for more information and examples.

#### model.setUniqueIndex(indexName, key|keyResolver, value|valueResolver, [shouldIndex = undefined], [returnOldOnConflict = false])'

Sets the index to use for enforcing uniqueness on this model.

See the the using a unique index section for more information and examples, or the indexes section for an explanation of the key/value resolvers and the shouldIndex argument.

#### `model.useTimestamps([createdField = 'created', [updatedField = 'updated'])`

If called, the model will add a created and updated timestamp field to each model that is saved. These are unix timestamps based on the server's time.

You can also use the model.touch(node, callback) function to update the updated timestamp without changing any of the node's properties. This is useful if you're updating composed models seperately but still want the base model to be updated.

By default, timestamps are a unix timestamp. You can change this by altering your model's makeTimestamp function. The function should return a value representing the current time.

Seraph-model provides two of these functions for your convenience by default, accessible on model.timestampFactories. They are:

  • epochSeconds: unix timestamp (default)
  • epochMilliseconds: unix offset (more accurate unix timestamp using ms)

To use one of these, just assign it yourself like this:

model.makeTimestamp = model.timestampFactories.epochMilliseconds;
#### `model.addComputedField(fieldName, computer)`

Add a computed field to a model.

#### `model.cypherStart()`

Returns the appropriate START point for a cypher query for this kind of model. Example:

var beer = model(db, 'Beer');

beer.cypherStart(); // -> 'node:nodes(type = "Beer")'

You can then use this in a seraph find or query call. Example:

db.find({type: 'IPA'}, false, beer.cypherStart(), function(err, beers) {
  // beers -> all beers with type == 'IPA'
});

seraph-model's People

Contributors

deestan avatar jonpacker avatar maghoff avatar rorymadden avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.