Skip to content

Instantly share code, notes, and snippets.

@Morgul
Last active August 29, 2015 14:01
Show Gist options
  • Save Morgul/4a231679af1195b8efdd to your computer and use it in GitHub Desktop.
Save Morgul/4a231679af1195b8efdd to your computer and use it in GitHub Desktop.
Omega Models Fluent API

Low-Level Fluent API Proposal

Proposed API

A couple of design points:

  • OmegaDB itself is not stateful; instead, all configuration resides in the connection object.

CollectionSelector

cSel
	.db('...')             // Optional database selection (returns a new CollectionSelector)

	.ns('...')             // Optional namespace selection (returns a new CollectionSelector)
	.schema('...')         // Alias for 'ns'

	.collection('...')     // Collection selection (returns a new Query)
	.table('...')          // Alias for 'collection'
	.bucket('...')         // Alias for 'collection'

Connection

od(driver, config)              // Create a new connection with the given driver and config

	// Connection inherits CollectionSelector, and contains all the same methods; see CollectionSelector for details.

Query

query
	// Selecting data
	.filter(filter)        // Only return items that match the given filter
	.between(field, lowerVal, upperVal, [leftBoundOpen, rightBoundOpen])
						   // Selects all items that have `field` between `lowerVal` and `upperVal`. 
						   // Supports specifying whether the left bound or right bound is open. (Default for both is true.

	// Joins
	.as(alias)             // Create an alias for the current query. (useful when it is a subquery of another query)
						   // You can then refer to this query's fields using dot notation: "alias.fieldName"
	.join(query or collectionName, filter, ['left'|'inner'|'full'])
						   // Join the current query with the given query (or the given collection's contents)
	.union(query)          // Concatenates 'query' to the current query's results

	// Transformations
	.orderBy(field, [...]) // Sorts items by the specified field(s); prepend "-" to the field name to reverse the sort order
	.limit(limit)          // Limit the number of results
	.offset(offset)	       // Skip the first `offset` number of results
	.slice(start, [end])   // limit the sequence to only those items between 'start' and 'end'

	// Modification
	.delete(filter)	       // delete all items that match `filter`
	.insert(query | json | list(json) | function() { ... }, [upsert])  // Inserts new documents into the table.
	.update(query | json | function(item, key) { ... })  // Update documents with either the results of query, or of json (treated as a partial).
	.replace(query | json | function(item, key) { ... }) // Replace the documents with either the results of query, or json.

	// Special
	.map(function)         // Maps values through the mapping function
	.reduce(function)      // Reduces values through the reducing function

	.execute()             // Begins executing the current query, and returns a ResultSet

ResultSet

Using Events

resultSet
	.on('item', function(item){ ... });    // Emitted with each returned item
	.on('finished', function(){ ... });    // Emitted when the query finishes successfully
	.on('error', function(error){ ... });  // Emitted when the query generates an error

Using Callbacks

resultSet
	// Get a count of total items in the result set.
	.count(function(error, numItems){ ... });
	
	// Accumulate all items into an array, and handle them all in one callback:
	.all(function(errors, items){ ... });

	// Retrieve exactly one item, by index
	.nth(index, function(error, item){ ... });

	// Retrieve only the first item (same as `.nth(0, ...)`)
	.first(function(error, item){ ... });

	// Handle each item individually, with no item accumulation:
	.each(function(item, callback){ ...; callback(error); },
		function(errors){ ... });

	// Handle each item individually, yielding an array of transformed item values from each item handler call:
	.map(function(item, callback){ ...; callback(error, item); },
		function(errors, items){ ... });

	// Handle each item individually, yielding the final value of an accumulator value:
	.reduce({}, function(accum, item, callback){ ...; callback(error, accum); },
		function(errors, accum){ ... });

Using Promises

resultSet
	// Get a count of total items in the result set.
	.count()
		.then(function(error, numItems){ ... });

	// Accumulate all items into an array, and handle them all via the returned promise:
	.all()
		.then(function(items){ ... }, function(errors){ ... });

	// Retrieve exactly one item, by index
	.nth(index)
		.then(function(item){ ... }, function(error){ ... });

	// Retrieve only the first item (same as `.nth(0, ...)`)
	.first()
		.then(function(item){ ... }, function(error){ ... });

	// Handle each item individually, with no item accumulation:
	.each(function(item, callback){ ...; callback(error); })
		.then(function(){ ... }, function(errors){ ... });

	// Handle each item individually, yielding an array of transformed item values from each item handler call:
	.map(function(item, callback){ ...; callback(error, item); })
		.then(function(items){ ... }, function(errors){ ... });

	// Handle each item individually, yielding the final value of an accumulator value:
	.reduce({}, function(accum, item, callback){ ...; callback(error, accum); })
		.then(function(accum){ ... }, function(errors){ ... });

Item handler functions

The per-item methods above each expect an item handler function, which will be called once per item in the result set. The method will not finish successfully (i.e., resolve the returned promise object, or call the finished callback without an error) until all item handler calls have completed.

Item handler functions may return different types of values, which influence when that result handler call is considered "completed":

  • a promise object - the handler call is completed when the returned promise is resolved.
  • undefined - the handler call is completed when the passed callback is called.

Filters

Filters are expressions that accept a value and a boolean. Selectors are like filters, but they return a value, rather than a boolean. A filter expression is a chain of filters and selectors. Filter expressions are initially passed the item

We are assuming the user calls the base object for filters and selectors f.

f
    // Selectors
    .field('...')               // Select a field from the current item
    
    // Filters
    .eq([field,] value)
    .le([field,] value)
    .contains([field,] value | list())
    .in([field,] query | value | list())
    .isNull([field])
    .and(filter, [filter, ...])
    .or(filter, [filter, ...])

Examples

Find the top 5 highest rated reviews in a product database.

var results = od('postgres', "tcp://user:p@ssw0rd@example-svr")
    .db('example_db')
    .schema('product_schema')
    .table('reviews')
    .between('score', 80, 100)
    .orderBy('-score', 'created')
    .limit(5)
    .execute();

// Print the first review    
results.first(function(error, firstReview)
{
    console.log("FIRST!!!", firstReview);
});

// Or handle all the reviews (Don't try and do both)
results.all(function(error, reviews)
{
    reviews.forEach(function(review)
    {
        console.log('Review:', review);
    });
});

High-Level Fluent API Proposal

Class Methods

BaseChar.get(id | filter)
        .filter(filter)
        .all()

Instance Methods

  • How to handle instance function collisions with model fields?
    • Option 1: Make fields override instance methods, and have the Model object have versions of those instance methods that take an instance as the first argument.
    • Option 2: Make the instance callable:
      modelInst.json = "moo";
      modelInst().save();
      
      // Not "moo"
      console.log(modelInst().json());
baseCharInst.delete()
            .save(depth)  // depth === null is the same as saveAll()
            .saveAll()
            .json(depth) // depth === null is the same as jsonAll()
            .jsonAll()
            //.populate() // Dave has ideas... use prefecth and properties, maybe?
            .prefetch(depth) // depth === null means get all
            [relation]

Filter

Always returns true/false.

f.field(name).eq(...)
             .le(...)
             .contains(...)
             .in(...)
             .isNull(...)

// Examples
BaseChar.filter(BaseChar.user.id.eq(3));
BaseChar.filter(f.field('user').id.eq(3));

Selectors

Have methods that return filters. (f.field is actually a selector).

Selector:
  sel(row) => any
  
Filter:
  sel(row) => boolean

This means any selector can be used as a filter, but you rely on javascript semantics for boolean coersion. Filter is just a more specific type of selector. Also using selectors as filters gets dangerous when you're sending this to the DB, as there may be mismatches between what the database things is truthy, and what javascript thinks is truthy. Because of this, we want to limit things to only take Selectors or Filters where appropriate.

@Morgul
Copy link
Author

Morgul commented May 14, 2014

I've added a modification API. In doing so, I've hit on something:

We need to be able to pass in javascript callbacks.

Not for everything, but I can see it being really useful to be able to call .update() and pass in a callback that is called for every result, and the new value is calculated in that function. Under the hood this means we'd have to do a select query, execute it, call the callback for every value, take the value the callback spit out, and then do a replace query on all the values... but it would work. And, sometimes, you want to pay the performance cost for the convenience. (Also, it opens up the possibility of workarounds for any missing functionality we might have in this API. That is either good, or bad, depending on your point of view.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment