Skip to content
/ odot Public

persistent, code reloading, interactive object space for node

License

Notifications You must be signed in to change notification settings

natecain/odot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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"
> <Ctrl-C> // 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
> <Ctrl-C>


Third session:

$ node example/trainset.js
> console.log(o.train.regular.speed) => 15
> <Ctrl-C>



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 node@thomassmith.com or the Node mailing list.

About

persistent, code reloading, interactive object space for node

Resources

License

Stars

Watchers

Forks

Packages

No packages published