Postgres ORM Thing

For your node-pg interfacin'

Skip to Documentation

Easier Node Postgres

Paul Dirac was a theoretical physicist who made fundamental contributions to the early development of both quantum mechanics and quantum electrodynamics.

Dirac.js is an extendable and configurable database layer built on top of MoSQL and made for Node Postgres. It has a pluginable architecture and ships with sensible middleware so you can avoid repetitive tasks.

var dirac = require('dirac'); // Register a table with dirac dirac.register({ name: 'users' , schema: { id: { type: 'serial', primaryKey: true } , email: { type: 'text', unique: true } , createdAt: { type: 'timestamp', default: 'now()' } } }); // Connect to database dirac.init({ database: 'my_app' }); // Perform non-destructive database sync dirac.sync(); dirac.dals.users.findOne( 7, function( error, user ){ /* ... */ });

Destructive or non-destructive database syncing

JSON table and view definitions

Standard CRUD

Robust JSON queries

Pluginable Architecture

Documentation

Getting Started

Dirac.js is meant to be a solid starting point for your main database module. While you could have all of your logic in a single file, I like to group code by table as seen in the file structure below:

File Structure

  - lib/
  - db/
    - tables/
        - groups.js
        - users.js
        - users_groups.js
    - index.js
  - app.js

Your Main Database Module

db/index.js
var dirac = require('dirac'); var utils = require('../lib/utils'); var config = require('../config'); // Tell dirac to use all of the schemas in `/tables` dirac.use( dirac.dir( __dirname + '/tables' ) ); // Set the DB Config dirac.init( config.db ); // Export the dals namespace for easy access module.exports = utils.extend( {}, dirac.dals );

The `dirac.dals` namespace is where each table interface is accessed. In the example above, groups, users, and users_groups would be available under `dirac.dals`.

Using the Database Module

app.js
var db = require('./db'); // Get the Database up-to-date with current schema db.sync(); // Query where clause // See https://github.com/goodybag/mongo-sql/blob/master/docs/conditional-helpers.md // For all where clause options var $where = { created_at: { $gt: new Date('2012-01-01') } }; // Other query options // See https://github.com/goodybag/mongo-sql/blob/master/docs/query-types.md#type-select // for full options var options = { order: { id: 'desc' } , limit: 10 }; // Find all users created after specified date // order by id desc and limit 10 db.users.find( $where, options, function( error, users ){ if ( error ) process.exit(1); console.log( "Users:", users ); process.exit(0); });

Root API

The dirac is split up into two main API's: The root API and the DAL (Data Access Layer) API. The root API involves any non-table-specific functionality while the DAL API is table-specific.

dirac.init( options )

Connect to Postgres

Arguments
  • @param {Object} Options
    • @property `connStr`
    • @property `port` default: 5432
    • @property `host` default: 'localhost'
    • @property `database`

dirac.query( queryObject, callback )

Execute a MoSQL query

Arguments
dirac.query
var dirac = require('/dirac'); var $query = { type: 'select' , table: 'users' , columns: [ 'id', 'name' ] , where: { id: { $in: [ 1, 2, 3, 4 ] } } }; dirac.query( $query, function( error, users ){ /* ... */ });

dirac.raw( sql, values, callback )

Execute a SQL query

Arguments
  • @param {String} sql - SQL string
  • @param {Array} values - SQL values
  • @param {Function} callback( error, results )
dirac.raw
var dirac = require('/dirac'); var sql = 'select * from users where id = $1'; var values = [ 50 ]; dirac.raw( sql, values, function( error, users ){ /* ... */ });

dirac.register( callback )

Registers a new table or view with dirac. Will not actually create the table until dirac.sync() is called. Alternatively, you could call: dirac.dals.table.createIfNotExists() to manually add it. However, sync will resolve table dependencies and it will also save the database state so dirac can reason about your current table structure.

Arguments
  • @param {Object} table definition - Required Properties:
    • @property {String} name - Name of the table
    • @property {Object} schema - Schema of table (see MoSQL definition helper)
dirac.register
var dirac = require('/dirac'); // Register table dirac.register({ name: 'users' , schema: { id: { type: 'serial' , primaryKey: true } , name: { type: 'text' } } }); // Register View dirac.register({ name: 'bobs' , type: 'view' , query: { type: 'select' , table: 'users' , where: { name: { $ilike: 'bob' } } } }); dirac.init(/* ... */); dirac.dals.users.findOne( 7, function( error, user ){ /* ... */ });

dirac.dropAllTables( callback )

Run the command for dropping all registered tables.

Arguments
  • @param {Function} callback( error, results )

dirac.sync( [options], [callback] )

Get Database up-to-date with latest schemas

Arguments
  • @param {Object} options - Options for sync
    • @property {Boolean} force - If `true` will perform destructive sync
  • @param {Function} callback( error )

dirac.use( middleware )

Pass a function to dirac to be called whenever `dirac.init` is called. Useful for initializing before/after filters and adding database-wide properties to all schemas.

Arguments
  • @param {Function} middleware( dirac ) - - the function that will be called when dirac is initialized. It's passed a single parameter (the dirac module).
db/middleware/created-at.js
/** * Automatically adds created_at/updated_at fields to schemas */ var dirac = require('/dirac'); var utils = require('utils'); module.exports = function( options ){ utils.defaults( options, { createdAt: { name: 'created_at' , type: 'timestamp' , default: 'now()' } , updatedAt: { name: 'updated_at' , type: 'timestamp' , default: 'now()' } }); return function( dirac ){ // Adds fields to a DAL var addFields = function( dal ){ var schema = dirac.dals[ dal ].schema; // Add createdAt if it's not already there if ( !(options.createdAt.name in schema) ){ schema[ options.createdAt.name ] = options.createdAt; } // Add updatedAt if it's not already there if ( !(options.updatedAt.name in schema) ){ schema[ options.updatedAt.name ] = options.updatedAt; } }; // Before filter adds updatedAt = 'now()' to the update query var updateFilter = function( $query, schema, next ){ // Updates may be on values or updates var values = 'values' in $query ? $query.values : $query.updates; values[ options.updatedAt.name ] = 'now()'; next(); }; // Registers before filters to update updatedAt var addFilters = function( dal ){ dirac.dals[ dal ].before( 'update', updateFilter ); }; // Add fields to each DAL Object.keys( dirac.dals ).forEach( addFields ) // Add filters to each DAL Object.keys( dirac.dals ).forEach( addFilters ) }; };
db/index.js
/** * Register our database middleware */ var dirac = require('/dirac'); var middleware = { createdAt: require('./middleware/created-at') }; dirac.use( middleware.createdAt({ updatedAt: { name: 'last_updated' , type: 'timestamp' , default: 'now()' } }) ); // DAL registration // ... // ... // After init is called, all functions specified in use are called dirac.init( config.db );

dirac.saveCurrentDbState( version, [callback] )

Save an entry in the dirac_schemas table of the current DAL state in memory. This happens everytime you call `sync`

Arguments
  • @param {Object} options - Options for sync
    • @property {Boolean} force - If `true` will perform destructive sync
  • @param {Function} callback( error )

Table Documentation

dirac.dals.table.find( $query, [options], [callback] )

Select documents in `table_name`. The `$query` object is the `where` property of a MoSQL object. The `options` object is mixed into the main query object sent to dirac.dals.table.query and makes up the rest of the query.

Arguments
  • $query - MoSQL `where` object sets `where` clause
  • options - Anything else that would go in a MoSQL query ( limit, offset, groupBy, etc )
  • callback - function( error, results ){ }
dirac.dals.table.find
// Query where condition var $query = { rating: { $gte: 3.5 } , high_score: { $lt: 5000 } , name: { $in: [ 'Bob', 'Alice', 'Joe', 'Momma' ] } }; // Other options for the query var options = { columns: [ '*' // users.* , { // Get average user high_score type: 'average' // Name of the function , as: 'average_score' // Name of the column , expression: 'users.high_score' // Function argument } ] , offset: 50 , limit: 25 , order: { column: 'id', direction: 'desc' } , group: [ 'id', 'name' ] }; dirac.dals.users.find( $query, options, function( error, results ){ /* ... */ });

dirac.dals.table.findOne( $query, [options], [callback] )

Identical to find only it adds a limit: 1 to the options and will return an object rather than an array. Substitute an ID for $query.

Arguments
  • $query - MoSQL `where` object or an ID to be converted to: { id: VALUE }
  • options - Anything else that would go in a MoSQL query ( limit, offset, groupBy, etc )
  • callback - function( error, results ){ }
dirac.dals.table.findOne
// Find user with { id: 1 } dirac.dals.users.find( 1, function( error, user ){ /* ... */ });

dirac.dals.table.remove( $query, [options], [callback] )

Removes a document from the database. Substitute an ID for $query.

Arguments
  • $query - MoSQL `where` object or an ID to be converted to: { id: VALUE }
  • options - Anything else that would go in a MoSQL query ( limit, offset, groupBy, etc )
  • callback - function( error, results ){ }
dirac.dals.table.remove
// Remove user with { id: 1 } dirac.dals.users.remove( 1, function( error, user ){ /* ... */ });

dirac.dals.table.update( $query, $update, [options], [callback] )

Update documents in the database. Substitute an ID for $query.

Arguments
  • $query - MoSQL `where` object or an ID to be converted to: { id: VALUE }
  • $update - MoSQL `updates` object Object whose keys map to column names and values map to values
  • options - Anything else that would go in a MoSQL query ( limit, offset, groupBy, etc )
  • callback - function( error, results ){ }
dirac.dals.table.update
// Update all bobs to bills var $query = { name: { $ilike: 'Bob' } }; var $update = { name: 'Bill' }; var options = { returning: ['id', 'name'] }; dirac.dals.users.update( $query, $update, options, function( error, users ){ /* ... */ });

dirac.dals.table.insert( document(s), [options], [callback] )

Insert a single document or an array of documents.

Arguments
  • document - MoSQL `values` object a single document or array of documents
  • options - Anything else that would go in a MoSQL query ( limit, offset, groupBy, etc )
  • callback - function( error, results ){ }
dirac.dals.table.insert
// Update all bobs to bills var user1 = { name: 'bob' }; db.users.insert( user1, function( error, result ){ console.log( result.id ); }); // In this case, other_field will be null for the other targets db.users.insert([ { name: 'bob' } , { name: 'bill', other_field: true } , { name: 'alice' } ]);

dirac.dals.table.before( [method], handler )

Add a before filter to the DAL. Before filters are like middleware layers that get run before the query is executed. You can add as long as a chain as you'd like.

Arguments
  • method - Name of the DAL method
  • handler - ( $query, schema, callback ) Middleware function handler
    • $query - The Full MoSQL query object
    • schema - The schema for the current table
    • next - A function to tell dirac to go the next function in the chain (If you pass an argument to next, dirac assumes that it is an error and will send the value back to the consumers callbaack)
dirac.dals.table.before - Crappy validation
// Register a new table dirac.register({ name: 'books' , schema: { id: { type: 'serial', primaryKey: true } , name: { type: 'text' // Dirac doesn't know anything about this object // So we can use it for our own benefit , validation: { type: 'string' , max_length: 250 } } } }) // Before inserting books, validate the doc dirac.dals.books.before( 'insert', function( $query, schema, next ){ if ( typeof $query.values.name != schema.name.validation.type ) return next({ type: 'VALIDATION_ERROR', message: 'invalid type for `name`' }); if ( $query.values.name.length > schema.validation.max_length ) return next({ type: 'VALIDATION_ERROR', message: 'invalid length for `name`' }); /* ... */ });

dirac.dals.table.after( [method], handler )

Add an after filter to the DAL. After filters are like middleware layers that get run after the query is executed.

Arguments
  • method - Name of the DAL method
  • handler - ( $query, schema, callback ) Middleware function handler
    • results - Results from the query
    • $query - The Full MoSQL query object
    • schema - The schema for the current table
    • next - A function to tell dirac to go the next function in the chain (If you pass an argument to next, dirac assumes that it is an error and will send the value back to the consumers callbaack)
dirac.dals.table.after - Dumb Casting
// Register a new table dirac.register({ name: 'books' , schema: { id: { type: 'serial', primaryKey: true } , num_words: { type: 'text' // node-pg returns bigints as strings // Tell casting after filter to cast to a number , cast: 'number' } } }) // Crappy casting dirac.dals.books.after( 'find', function( results, $query, schema, next ){ var casts = {}; for ( var key in schema ){ if ( 'cast' in schema ) casts[ key ] = schema[ key ][ cast ]; } // Transform result set for ( var i = 0, l = results.length; i < l; ++i ){ for ( var key in casts ){ switch ( casts[ key ] ){ case 'int': results[ i ][ key ] = parseInt( results[ i ][ key ] ); break; case 'number': results[ i ][ key ] = parseFloat( results[ i ][ key ] ); break; case 'string': results[ i ][ key ] = "" + results[ i ][ key ]; break; default: break; } } } next(); });

Examples