0.4.1 • Published 10 years ago

jsrel v0.4.1

Weekly downloads
103
License
-
Repository
github
Last release
10 years ago

JSRel

description

JavaScript synchronous RDB (Relational database) without SQL

Available in modern browsers, Node.js and Titanium(NEW!).

This ISN'T ORM, but SQL-less RDB implemented in JavaScript!

Get it!

    $ npm install jsrel

or

    $ curl https://raw.github.com/shinout/jsrel/master/install-jsrel.sh | sh

API at a glance

First, define the schema

var JSRel = require("jsrel");
var db = JSRel.create("dbname", {schema: 
  { user: { name : true, is_activated: "on", $uniques: "name"},
    book: { title: true, price: 1, author: "user", $indexes: "title" },
}});

Second, insert data

if (!db.loaded) { // if loaded from saved data, omits this section
  var u1 = db.ins('user', {name: 'shinout'});
  var u2 = db.ins('user', {name: 'xxxxx', is_activated: false});
  var b1 = db.ins('book', {title: 'how to jsrel', price: 10, author: u1});
  var b2 = db.ins('book', {title: 'JSRel API doc', price: 20, author_id: u1.id});
}

Find them!

var users = db.find('user', {is_activated: true});

Get one!

var shinout = db.one('user', {name: "shinout"});

Greater Than, Less Equal!

var booksGreaterThan5  = db.find('book', { price: {gt: 5} } );
var booksLessEqual15   = db.find('book', { price: {le: 15} } );

Like xxx%

var booksLikeJS = db.find('book', { title: {like$: "JS"} } );

Join!

var usersJoinBooks = db.find('user', {is_activated: true}, {join: "book"});

OrderBy! Offset! Limit!

var users = db.find('user', null, {order: "name", limit : 10, offset : 3} );

Perpetuation

db.save();

Export / Import

var str = db.export();
    var newDB = JSRel.import("newID", str);

dump as SQL!

var sql = db.toSQL();

suitable applications

  • rich client applications
  • tiny serverside applications
  • client caching
  • mock DB

NOT suitable for applications which require scalability.

motivation

Thinking about the separation of the Model layer.

If we connect to DB asynchronously, we must handle lots of callbacks in a model method.

model.getUserBooks = function(name, callback) {
  db.find("user", {name: name}, function(err, users) {
    db.find("book", {user_id: users[0].id}, callback);
  });
};

If we access to DB synchoronously, we can easily write human-readable model APIs.

model.getUserBooks = function(name) {
  var user  = db.find("user", {name: "xxyy"})[0];
  return db.find("book", {user_id: user.id});
};

Also, synchoronous codes have an advantage of error handling.

###for those who dislike Blocking APIs###

Why not making it standalone using WebWorker (browsers) or child_process.fork() (Node.js)? Then the whole calculation process doesn't affect the main event loop and we can get the result asynchronously.

I prepared another JavaScript library for this purpose.

standalone.

Then, we can access model methods like

model.getUserBooks("user01", function(err, result) {
})

by defining

model.getUserBooks = function(name) {
  var user  = db.find("user", {name: "xxyy"})[0];
  if (!user) return [];
  return db.find("book", {user_id: user.id});
};

That is, try/catch and asynchronous APIs are automatically created via standalone.

See make it standalone for detailed usage.

installation

    $ npm install jsrel

for development in Titanium or web browsers,

    $ curl https://raw.github.com/shinout/jsrel/master/install-jsrel.sh | sh

in browsers,

<script type="text/javascript" src="/path/to/SortedList.js"></script>
<script type="text/javascript" src="/path/to/jsrel.js"></script>

in Node.js or Titanium,

var JSRel = require('jsrel');

is the way to load the library.

In browsers, the variable "JSRel" is set to global.

In Web Worker,

importScripts('/pathto/SortedList.js', '/pathto/jsrel.js');

See also make it standalone.

dependencies

JSRel internally uses SortedList When installed with npm, it is automatically packed to node_modules/sortedlist Otherwise, it is recommended to run the following command to prepare jsrel and sortedlist.

    $ curl https://raw.github.com/shinout/jsrel/master/install-jsrel.sh | sh

In Titanium, you have to set jsrel.js and SortedList.js at the top of Resources directory.

JSRel API documentation

JSRel

  • JSRel.use(uniqId, options)
  • JSRel.create(uniqId, options)
  • JSRel.createIfNotExists(uniqId, options)
  • JSRel.import(uniqId, data_str, options)
  • JSRel.$import(uniqId, data_str, options)
  • JSRel.uniqIds
  • JSRel.isNode
  • JSRel.isBrowser
  • JSRel.isTitanium

instance of JSRel (jsrel)

  • jsrel.table(tableName)
  • jsrel.save()
  • jsrel.export(noCompress)
  • jsrel.$export(noCompress)
  • jsrel.on(eventName, func, options)
  • jsrel.off(eventName, func)
  • jsrel.toSQL(options)
  • jsrel.origin()
  • jsrel.drop()
  • jsrel.id
  • jsrel.name
  • jsrel.tables
  • jsrel.schema
  • jsrel.loaded
  • jsrel.created

instance of JSRel Table (table)

  • table.columns
  • table.ins(obj)
  • table.upd(obj, options)
  • table.find(query, options)
  • table.one(id)
  • table.one(query, options)
  • table.del(id)
  • table.del(query)

shortcut

  • jsrel.ins(tableName, ...)
  • jsrel.upd(tableName, ...)
  • jsrel.find(tableName, ...)
  • jsrel.one(tableName, ...)
  • jsrel.del(tableName, ...)

JSRel.use(uniqId, options)

Creates instance if not exist. Gets previously created instance if already exists.

uniqId is the identifier of the instance, used for storing the data to external system (file system, localStorage and so on). options is as follows.

SCHEMA JSON

{
  tableName1: tableDescription,
  tableName2: { 
    columnName1 : columnDescription,
    columnName2 : columnDescription
  }
}

table description

column description

JSRel.create(uniqId, options)

Creates instance if not exist, like JSRel.use. Throws an error if already exists, unlike JSRel.use. Arguments are the same as JSRel.use except options.reset, which is invalid in JSRel.create()

JSRel.createIfNotExists(uniqId, options)

Creates instance if not exist. Gets previously created instance if already exists. options is optional when loading an existing database, and required when creating a new database. Actually, this is the alias for JSRel.use(uniqId, options)

JSRel.import(uniqId, data_str, options)

Imports data_str and creates a new instance with uniqId. data_str must be a stringified JSON generated by jsrel.export().

Returns instance of JSRel.

JSRel.$import(uniqId, data_str, options)

Alias for JSRel.import(). As "import" is a reserved word in JavaScript, we first named this function "$import". However, CoffeeScript enables us to use reserved words, then we can also use JSRel.import as the alias.

JSRel.isNode

(ReadOnly boolean) if Node.js, true.

JSRel.isBrowser

(ReadOnly boolean) if the executing environment has "localStorage" and "sessionStorage" in global scope, true.

JSRel.isTitanium

(ReadOnly boolean) if Titanium, true.

instanceof JSRel (shown as jsrel)

jsrel.table(tableName)

Returns a table object whose name is tableName (registered from the schema). If absent, throws an exception.

jsrel.save()

Saves current data to the storage. Returns jsrel

jsrel.export(noCompress)

Exports current data as the format above. Returns data. If noCompress is given, it returns uncompressed data.

jsrel.$export(noCompress)

Alias for jsrel.export() as "export" is a reserved word in JavaScript. In CoffeeScript, jsrel.export() can be safely called.

jsrel.on(eventName, func, options)

Registers hook functions. eventName is the name of the event to bind the function func.

events

options

jsrel.off(eventName, func)

Unregister hook functions registered in eventName. If a function func is registered in eventName hooks, it is removed. If func is null, all functions registered in eventName is removed.

jsrel.toSQL(options)

Gets SQL string from the current schema and data.

options

jsrel.origin()

Gets the last savedata.

Unless jsrel.save() has been called at least once, null is returned.

  var savedata = jsrel.origin();
  var newdb = JSRel.import("new_db", savedata);

jsrel.drop(tableName1, tableName2, ...)

Drops given tables. If dependencies exist, jsrel follows the following rules.

  1. throw an error if the given table group contains another reference table
  2. set all the ids of referred columns to null

jsrel.id

(ReadOnly) gets id

jsrel.name

(ReadOnly) gets name

jsrel.tables

(ReadOnly) gets list of registered tables

[table1, table2, ...]

jsrel.schema

(ReadOnly) gets a canonical schema of the database, the same format as schema passed to JSRel.use

Be careful that this property is dynamically created for every access.

var schema = db.schema; // created dynamically
var schema2 = db.schema; // created dynamically
schema  ===  schema2 // false, but deeply equal

var db2 = JSRel.use("db2", {schema: schema});  // the same structure as db

jsrel.loaded

(ReadOnly) boolean: true if loaded or imported from stored data, false otherwise.

db = JSRel.use("/path/to/file", {schema: {tbl1: {name:true}, tbl2: {n: 1}}});

// if not loaded, then inputs initialized data
if (!db.loaded) {
  db.ins("tbl1", {name: "shinout"});
  db.ins("tbl1", {name: "nishiko"});
  db.ins("tbl2", {n: 37});
  db.ins("tbl2", {n: -21});
}

jsrel.created

(ReadOnly) boolean: true if created, false otherwise.

jsrel.created === !jsrel.loaded

db = JSRel.use("/path/to/file", {schema: {tbl1: {name:true}, tbl2: {n: 1}}});

// if created, then inputs initialized data
if (db.created) {
  db.ins("tbl1", {name: "shinout"});
  db.ins("tbl1", {name: "nishiko"});
  db.ins("tbl2", {n: 37});
  db.ins("tbl2", {n: -21});
}

instanceof JSRel.Table (shown as table)

table.columns

(ReadOnly) gets registered columns in the table

[column1, column2, ...]

table.ins(obj)

Registers a new record. obj must be compatible with columns of the table. Otherwise it throws an exception. Returns an instance of the record. It is NOT the same as the given argument, as the new object contains "id".

Before insertion, Type checking is performed. JSRel tries to cast the data.

record object

Record objects have all columns registered in the table.

In addition, they have id, ins_at, upd_at in their key. These are all automatically set.

ins_at and upd_at are timestamp values and cannot be inserted.

id is auto-incremented unique integer.

We can specify id in insertion.

table.ins({id: 11, name: "iPhone"});

When the table already has the same id, an exception is thrown.

relation handling in insertion

OK, let's think upon the following schema.

var schema = { user: {
    nickName : true,
    fitstName: false,
    lastName : false
  },
  card: {
    title : true,
    body  : true
  },
  user_card {
    user: "user",
    card: "card",
    owner: {type : "user", required: false}
    $uniques: { user_card: ["user", "card"] }
  }
}

First, inserts users and cards.

var jsrel = JSRel.use('sample', {schema: schema});
    
var uTable = jsrel.table('user');
var shinout = uTable.ins({nickName: "shinout"});
var nishiko = uTable.ins({nickName: "nishiko"});
var cTable = jsrel.table('card');
var rabbit = uTable.ins({title: "rabbit", body: "It jumps!"});
var pot    = uTable.ins({title: "pot", body: "a tiny yellow magic pot"});

Then, inserts these relations.

var ucTable = jsrel.table('user_card');
ucTable.ins({ user: shinout, card: rabbit });

We can also insert these relation like

ucTable.ins({ user_id: nishiko.id, card_id: pot.id });
ucTable.ins({ user_id: 1, card_id: 2 }); // 1: shinout, 2: pot

Remember that user_id and card_id are automatically generated and it represent the id column of each instance. When we pass an invalid id to these columns, an exception is thrown.

ucTable.ins({ user_id: 1, card_id: 5 }); // 1: shinout, 5: undefined!

When a relation column is not required, we can pass null.

ucTable.ins({ user: nishiko, card_id: 1, owner_id: null });

When duplicated, xxxx_id priors to xxxx (where xxxx is the name of the original column).

ucTable.ins({ user: nishiko, user_id: 1, card_id: 1 }); // user_id => 1

inserting relations

obj.rel_table = [relObj1, relObj2, ...];
table.ins(obj);

relObj1, relObj2 are also inserted to table "rel_table" containing the new id as the external key.

If the main table is related to the rel_table multiply, you must specify the column like

obj["rel_table.relcolumn"] = [relObj1, relObj2, ...];
table.ins(obj);

table.upd(obj, options)

Updates an existing record. obj must contains id key. Only the valid keys (compatible with columns) in obj is updated. Throws no exceptions when you passes invalid keys. Throws an exception when you an invalid value with a valid key.

Returns an instance of the updated record. It is NOT the same as the given argument.

relation updates

updating related tables

obj.rel = {id: 3 }
obj.rel_id = 1
// in this case, relObj is prior to rel_id
table.upd(obj, {append: append});

var rel_id = table.one(obj, {id: obj.id}, {select: "rel_id"}); // 3. not 1
obj.rel_table = [relObj1, relObj2, ...];
table.upd(obj, {append: append});

if relObj contains "id" column, updating the object. Otherwise, inserting the object. If options.append is false or not given, already existing related objects are deleted.

If the main table is related to the rel_table multiply, you must specify the column like

obj["rel_table.relcolumn"] = [relObj1, relObj2, ...];
table.upd(obj, {append: append});

table.find(query, options)

Selects records. Returns a list of records. query is an object to describe how to fetch records.

query examples

options is as follows.

order description

results

Returns list of instances

[ {id: 1, name: "shinout"}, {id: 2, name: "nishiko"}, ...]

join description

sample data

Fetching N:1 related objects

var result = db.table('user').find({name: "shinout"}, {join: JOIN_VALUE});

Fetching 1:N related objects

var result = db.table('group').find({name: "mindia"}, {join: JOIN_VALUE});

Fetching N:M related objects

var result = db.table('user').find({name: "shinout"}, {join: JOIN_VALUE});

table.one(id)

Gets one object by id.

table.one(query, options)

Gets one result by table.find().

table.del(id)

Deletes a record with a given id .

table.del(query)

Deletes records with a given query . query is the same argument as table.find(query).

relation handling in deletion

When a record is deleted, related records are also deleted.

Think upon the schema.

First, inserts users, cards and these relations.

var jsrel = JSRel.use('sample', {schema: schema});

var uTable = jsrel.table('user');
var cTable = jsrel.table('card');
var ucTable = jsrel.table('user_card');

var shinout = uTable.ins({nickName: "shinout"});
var nishiko = uTable.ins({nickName: "nishiko"});

var rabbit = uTable.ins({title: "rabbit", body: "It jumps!"});
var pot    = uTable.ins({title: "pot", body: "a tiny yellow magic pot"});

ucTable.ins({ user: shinout, card: rabbit });
ucTable.ins({ user: nishiko, card: rabbit });
ucTable.ins({ user: shinout, card: pot });

Next, delete shinout.

uTable.del(shinout);

Then, the dependent records ( shinout-rabbit, shinout-pot ) are also removed.

ucTable.find().length; // 1 (nishiko-rabbit)

shortcut

  • jsrel.ins(tableName, ...)
  • jsrel.upd(tableName, ...)
  • jsrel.find(tableName, ...)
  • jsrel.one(tableName, ...)
  • jsrel.del(tableName, ...)

are, select table via jsrel.table(tableName) in the first place. Then run the operation using the remaining arguments.

for example,

jsre.ins('user', {nickName: "shinout"});

is completely equivalent to

jsrel.table('user').ins({nickName: "shinout"});

make it standalone

standalone is a library to make a worker process / thread which can communicate with master.

Here are the basic concept.

master.js

standalone("worker.js", function(model) {

  model.getSongsByArtist("the Beatles", function(err, songs) {
    console.log(songs);
  });

});

worker.js

var db = JSRel.use("xxx", {schema: {
  artist: {name: true},
  song  : {title: true, artist: "artist"}
}});
var btls = db.ins("artist", {name: "the Beatles"});
db.ins("song", {title: "Help!", artist: btls});
db.ins("song", {title: "In My Life", artist: btls});

var model = {
  getSongsByArtist: function(name) {
    return db.find("artist", {name : name}, {join: "song", select : "song"});
  }
};
standalone(model);

In master.js, we can use "getSongsByArtist" asynchronously, catching possible errors in err.

In Node.js, standalone spawns a child process.

In browsers, standalone creates a WebWorker instance.

In Titanium, standalone is not supported.

environmental specific code

Because Node.js and WebWorker has a different requiring system, We must be careful of loading scripts.

in Node.js (worker.js)

var JSRel = require('jsrel');
var standalone = require('standalone');

This is enough.

in browsers (worker.js)

importScripts('/pathto/SortedList.js', '/pathto/jsrel.js', '/pathto/standalone.js');

Don't forget to import SortedList (which JSRel depends on).

LICENSE

(The MIT License)

Copyright (c) 2012 SHIN Suzuki shinout310@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0.4.1

10 years ago

0.4.0

10 years ago

0.3.9

10 years ago

0.3.8

10 years ago

0.3.7

10 years ago

0.3.6

10 years ago

0.3.5

10 years ago

0.3.4

10 years ago

0.3.3

10 years ago

0.3.2

10 years ago

0.3.1

10 years ago

0.3.0

10 years ago

0.2.4

10 years ago

0.2.5

10 years ago

0.2.3

11 years ago

0.2.2

11 years ago

0.2.1

11 years ago

0.2.0

11 years ago

0.1.15

11 years ago

0.1.14

11 years ago

0.1.13

11 years ago

0.1.12

11 years ago

0.1.10

11 years ago

0.1.9

11 years ago

0.1.7

12 years ago

0.1.6

12 years ago

0.1.5

12 years ago

0.1.4

12 years ago

0.1.3

12 years ago

0.1.0

12 years ago

0.0.8

12 years ago

0.0.7

12 years ago

0.0.6

12 years ago

0.0.5

12 years ago

0.0.4

12 years ago

0.0.3

12 years ago

0.0.2

12 years ago

0.0.1

12 years ago

0.0.0

12 years ago