Home > odot

odot

Odot is a project mainly written in ..., based on the MIT license.

persistent, code reloading, interactive object space for node

DESCRIPTION

odot is a persistent, interactive object space with code reloading. It is based on Node.

EXAMPLE

Three files (in example/ directory):

trainset.js - Defines train set with trains and stations train.js - Default values and functions for trains station.js - Default values and functions for stations

First session:

$ node example/trainset.js

alpha = o.Station('alpha') beta = o.Station('beta') beta.mile = 60 // Override default of 0 set in station.js regular = o.Train('regular') console.log(regular.speed) => 10 // Is default specified in train.js express = o.Train('express') express.speed = 20 regular.station = alpha // train.js specifies one train->station relation express.station = alpha express.travel(beta) => "express now at beta after 3 hours travel" // Make change to train.js to change travel() function output recode() // Dynamically reloads code from train.js and station.js regular.travel(beta) => "regular goes to beta, travel time 6 hours"

// Automatic persistence on exit to o.json file in working directory

Second session:

$ node example/trainset.js

// Automatic load of database from o.json file in working directory beta = o.station.beta // or o.station['beta'] console.log(beta.distance) => 60 // Data is preserved console.log(beta.train[0].id) => 'express' // Many relation station->train regular = o.train.regular console.log(regular.station.id) => 'beta' // One relation train->station regular.speed = 15 reset() // Resets entire database back to last save point or state at load regular = o.train.regular // Remember to re-reference to get reset state console.log(regular.speed) => 10 regular.speed = 15 save() // Persists current state of database (like a commit) reset() // Resets to the last save point, which was after speed set to 15 regular = o.train.regular console.log(regular.speed) => 15

Third session:

$ node example/trainset.js

console.log(o.train.regular.speed) => 15

INSTALLATION

If you use npm:

npm install odot

If you do not use npm, clone this repository or download the latest version using the GitHub repository Downloads link.

DB SCRIPT

You must write a script to define your database. You launch your interactive session by executing this script with Node.

First, import the odot module:

var odot = require('odot') map = odot.map;

If you did not install via npm, require your checked out or extracted odot directory instead of 'odot'.

For each collection in your database, provide the path of a prototype script:

map('train', 'train.js'); map('station', 'station.js');

Finally, load the db and enter the interactive repl prompt (see WITHOUT PROMPT for how to use the database without a repl prompt).

odot.repl()

PROTOTYPE SCRIPTS

Prototype scripts define the default variables and functions of prototypes. Prototypes undergird all the objects of particular collections in your database. A very basic example (example/station.js):

var mile = 0; // Set default value of on station prototype

many('train'); // Create a many relation with trains

function name() { // Custom function for printing out station name return 'station ' + this.id + ' at ' + this.mile; }

All variables and functions defined become those of the collection prototype. Special function calls allow you to setup relations with objects of other collections in the database (see RELATIONS below).

Prototype scripts are dynamic. You can continue editing them while the node process and/or prompt is up, then, when you want the latest code loaded into your node process, use the recode() (or rc(), alias to same) method. This will load the latest code from all your prototype scripts into the prototypes undergirding the objects in your database, giving the objects new defaults and behaviors without restarting the process.

RELATIONS

You can manually add functions to your prototype scripts to reference objects of other collections in your database. However, since this is a common pattern, prototype setup methods are provided to let you define 'one' or 'many' relations.

For example, in train.js:

one('station')

Gives all train objects a station member that is transparently set or accessed by:

regular = o.Train('regular') alpha = o.Station('alpha') regular.station = alpha regular.station // => alpha

You can also define a relation to one or more objects of another collection. For example, in station.js:

many('train')

Gives all station objects a train member that is an array of train objects:

regular = o.Train('regular') express = o.Train('express') alpha = o.Station('alpha') alpha.train = [regular, express] alpha.train // => [regular, express]

The array returned is a real JavaScript array that is created at the time of call, therefore you can use the full set of array methods on it, but anything you do to the array does not affect the actual relations held by the object. To add or remove trains individually from the station's many relation:

slow = o.Train('slow'); alpha.add(slow) alpha.train // => [regular, express, slow] alpha.remove(express) alpha.train // => [regular, slow]

Relationships are not automatically managed bi-directionally, so you must make corresponding changes on the other objects if you want the relations to be bi-directional:

alpha.remove(regular) // Remove train from station, but not station from train regular.station = null // Totally clear either a one or many relation on train alpha.train // => [slow] regular.station // => null

All relations are persisted. There are no automatic pluralizations of collection names, even in the many case. Any prototype can define as many one and many relations as desired. add() and remove() work even when multiple many relations are defined.

EPHEMERAL PROPERTIES

If you have properties of a collection's objects you don't want persisted, declare them as ephemeral in the prototype script for that collection:

ephemeral('conductor');

In this example from train.js, you can set the name of a conductor on the train that is valid for the current process. But once the process is restarted (or save() or reset() is called), the conductor property will be null. An example real use for this would be an HTTP connection object, which is transient, may not be persistable without error, and won't be useful after you restart the process.

CALCULABLE PROPERTIES

If you have a situation where a property can be derived from other properties in an object, but you might also set the property directly, use the calculable() prototype script helper method.

Taking the train example, let's say that there is a property of weight per car, which is the total train weight divided by the number of cars. Sometimes this might be set directly as in:

regular.wpc = 60 // tons

However, for other trains the actual weight and number of cars might be known:

express.weight = 500 // tons express.cars = 10

In this case, the weight per car can be derived by dividing the weight by the number of cars. Ideally, in either case, you want the wpc property to return the weight per car, whether it is directly set or derived:

regular.wpc // => 60 express.wpc // => 50

To do this, create a function in the train prototype script for calculating the potentially derivable property and then assign it to a property using the calculable() prototype script helper function:

function weightPerCar() { if (this.weight && this.cars) { return this.weight / this.cars; } }

calculable('wpc', weightPerCar); // A settable or calculable property

OBJECTS AS PROTOTYPES

To use an existing object as a prototype for another object in the same collection, pass it to the creation function as the second argument:

express = o.Train('express') express.speed = 20 express1 = o.Train('express1', express) express1.engineer = 'Alice' express2 = o.Train('express2', express) express2.engineer = 'Bob'

The database persists and reconstitutes the prototypical inheritance as needed.

DB FILE

The database file used to persist the database object state is a simple JSON file. It uses indenting and newlines to make it easy to read, edit, diff, and store with source control systems like svn and git.

WITHOUT REPL PROMPT

If you do not want to start an interactive repl prompt, but want to use an odot database from a Node application, use this in your application instead of odot.repl():

var o = odot.load();

No prompt will appear, but otherwise the API is exactly the same as demonstrated for the repl interactive prompt.

LIVE MAPPING

You can continue to use map() to add a new collection after load() or repl(). Example:

map('car', 'car.js');

SECURITY

All mapped code is assumed to be fully trusted. Do not place third party code into your prototypes without reviewing it. Database files are written to the working directory per the umask of your user and are therefore subject to access by other users according to file system permissions.

OTHER APPROACHES

There are other approaches to quick, in-process databases for Node you should consider if you want document-orientation rather than object-orientation and other features like append-only disk logs, including:

  • nStore (https://github.com/creationix/nStore)
  • node-dirty (https://github.com/felixge/node-dirty)

FEEDBACK

Welcome at [email protected] or the Node mailing list.

Previous:middleman-blog