Postgres recently gave a very thoughtful gift to the JavaScript community by adding support for JSON types. PostHole allows you to assign models to your JSON fields for easy management, must like a keystore, but with the full power of Postgres behind it. Write your queries with standard SQL, and get your JSON fields back as instantiated models.
PostHole uses VeryModel does to make working with models easy and powerful.
PostHole uses node-postgres aka module pg
for the connection and queries.
var PostHole = require('posthole');
var PersonFactory = new PostHole.Model({
firstName: {},
lastName: {},
fullName: {derive: function () {
return this.firstName + ' ' + this.lastName;
}},
}, {
table: 'person',
field: 'personjson'
});
var author = PersonFactory.create({
firstName: 'Nathan',
lastName: 'Fritz'
});
function startDemo() {
author.save(function (err, id) {
if (err) {
console.log(err);
return;
}
//query can be any query, with the fields from many models, renamed with AS or not.
//query function is the same as postgres module 'pg's query
PostHole.query("SELECT * FROM person WHERE id = $1", [id], function(err, result) {
if (err || result.rows.length !== 1) {
console.log(err);
return;
}
//every returned field mapped to a model will be a VeryModel instance
//results are otherwise the same as the node postgres module 'pg'
console.log(result.rows[0].personjson.toJSON());
/* {
* firstName: "Nathan",
* lastName: "Fritz",
* fullName: "Nathan Fritz"
* }
*/
console.log("Saved to id: %s", result.rows[0].personjson.id);
});
});
}
PostHole.connect('postgres://fritzy@localhost/fritzy', function (err) {
if (err) {
throw err;
}
//grab the meta data for the fields of the table
//figures out which field is the primary key
//sets up table if id doesn't exist
PersonFactory.setup(startDemo);
});
Sets the default connection and returns a standard node-postgres pg
instance.
Arguments:
- Postgres connection URI
Disconnects the default connection.
Identical to node-postgres's query, except that resulting fields that match a registered Model Factory will be cast as VeryModel instances even if the names are aliased.
Arguments:
- SQL Query String
- optional array of arguments
- optional callback
Constructor for making and registering VeryModel Model Factories that can be associated with JSON/JSONB Postgres Table Fields.
Arguments:
- Definition Object
- Model Options
Example:
var posthole = require('posthole');
var Person = new posthole.Model({
//some fields here
name: {type: 'string'},
phone: {type: 'string'}
},
{
//options here
table: 'some_table',
field: 'some_field'
});
<a name='defining-a-model-factory'></a>
## Defining a Model
A model defines the types of fields that you might use in an object.
#### Field Definition Properties
When making a model, you must define the fields in the model.
A field definition may be a simple empty `{}` if anything goes.
Most field definition properties that can be functions are called with the model instance as the `this` context.
* [type](#def-type)
* [validate](#def-validate)
* [processIn](#def-processIn)
* [processOut](#def-processOut)
* [onSet](#def-onSet)
* [derive](#def-derive)
* [required](#def-required)
* [default](#def-default)
* [private](#def-private)
* [model](#def-model)
* [collection](#def-collection)
<a name='def-type'></a>
__type__
A string which references a built in type.
Built in types include `string`, `array`, `integer`, `numeric`, `enum`, `boolean`.
Strings and arrays may have `min` and `max` values, both for validation, and max will truncate the results when saving or on `toJSON`.
Enums may include `values`, an array (and eventually a ECMAScript 6 set).
You can override any of the definition fields of a specified type. Validate, processIn, processOut, and onSet will use both the built-in and your override. The others will replace the definition field.
`type` does not need to be set at all. In fact, `{}` is a perfectly valid definition.
Example:
{field: {type: 'string', max: 140}}
----
<a name='def-validate'></a>
__validate__
The `validate` field takes a value and should determine whether that value is acceptable or not. It's run during `doValidate()` or during `save` if you set the option `validateOnSave: true`.
The function should return a boolean, an array of errors, an empty array, or an error string.
Example:
```js
new dulcimer.Model({field: {
validate: function (value) {
//validate on even
return (value % 2 === 0);
}
});
processIn
is a function that is passed a value on loading from the database, create
, or loadData
. It should return a value.
This function is often paired with processOut
in order to make an interactive object when in model form, and a serialized form when saved.
processIn
does not handle the case of direct assignment like modelinst.field = 'cheese';
. Use onSet
for this case.
Example:
new dulcimer.Model({someDateField: {
processIn: function (value) {
return moment(value);
},
})
processOut
is a function that takes a value and returns a value, just like processIn
, but is typically used to serialize the value for storage. It runs on save()
and toJSON()
.
Example:
new dulcimer.Model({someDateField: {
processOut: function (value) {
return value.format(); //turn moment into string
},
})
onSet
is just like processIn
, except that it only runs on direct assignment. It's a function that takes a value and returns a value.
Example:
new dulcimer.Model({someDateField: {
processIn: function (value) {
return moment(value);
},
onSet: function (value) {
if (moment.isMoment(value)) {
return value;
} else {
return moment(value);
}
},
processOut: function (value) {
return value.format();
},
})
derive
is a function that returns a value whenever the field is accessed (which can be quite frequent). The this
context, is the current model instance, so you can access other fields.
Example:
new dulcimer.Model({
firstName: {type: 'string'},
lastName: {type: 'string'},
fullName: {
type: 'string',
derive: function () {
return [this.firstName, this.lastName].join(" ");
},
}
});
❗ Warning! DO NOT REFERENCE THE DERIVE FIELD WITHIN ITS DERIVE FUNCTION! You will cause an infinite recursion. This is bad and will crash your program.
required
is a boolean, false by default.
A required field will attempt to bring in the default
value if a value is not present.
Example:
new dulcimer.Model({
comment: {
type: 'string',
required: true,
default: "User has nothing to say."
},
});
default
may be a value or a function. Default is only brought into play when a field is required
but not assigned.
In function form, default
behaves similarly to derive
, except that it only executes once.
new dulcimer.Model({
comment: {
type: 'string',
required: true,
default: function () {
return this.author.fullName + ' has nothing to say.';
},
},
});
❗ Warning! Assigning mutable objects as a default can result in the default getting changed over time. When assigning objects, arrays, or essentially any advanced type, set default to a function that returns a new instance of the object.
Models within models. What will we think of next?
model
can either be a definition object or a Model Factory instance.
Just like sub models, collection
is a definition object or Model Factory instance, however, the data stored in this field will be an array of structures defined by the definition.
private
is a boolean, false by default, which determines whether a field is saved into the object upon save and included in the object resulting from toJSON().
You can force private methods to be included in saved objects with the model option savePrivate, while preserving toJSON omittion.
- table: required for associating the model with a table
- field: required for associating the model with a field
- primaryKey: optional as the primary key should be detected automatically
Saves the current model instance to a serialized form in the db.
Any processOut functions will be ran to process the fields into their serialized form.
Arguments:
- callback: function (err)
Callback Arguments:
- err: Only set if an error occurred.
- id: if this model is new
Example:
var person = Person.create({
firstName: 'Nathan',
lastName: 'Fritz',
});
person.save(function (err) {
console.log("Person:", person.fullName, "saved as", person.key);
//fullName is a derived field
//didn't pass options because they're optional, remember?
});
Deletes the instance from the database. :warning: This removes the entire row!
Arguments:
- callback:
function (err)
Callback Arguments:
- err: Only set when an error has occurred while deleting.
Example:
person.delete(function (err) {
});
Outputs a JSON style object from the model.
Boolean Flags:
- withPrivate: false by default. If true, includes fields with private set to true.
Example:
You want an example? Look at all of the other examples... most of them use toJSON.
☝️ toJSON does not produce a string, but an object. See: toString.
Just like toJSON, but produces a JSON string rather than an object.
Arguments:
- other: model instance to compare this one to.
Result: object of each field with left, and right values.
{
firstName: {left: 'Nathan', right: 'Sam'},
lastName: {left: 'Fritz', right: 'Fritz'},
}
Get the changes since get or create.
Result: object of each field with then, now, and changed boolean.
{
body: {then: "I dont liek cheese.", now: "I don't like cheese.", changed: true},
updated: {then: '2014-02-10 11:11:11', now: '2014-02-10 12:12:12', changed: true},
created: {then: '2014-02-10 11:11:11', now: '2014-02-10 11:11:11', changed: false},
}
Get a new model instance of this instance with all of the changes since get or create reversed.
Result: Model instance.
Loads data just like when a model instance is retrieved or created.
processIn is called on any fields specified, but onSet is not.
Essentially the same things happen as when running create but can be done after the model instance is initialized.
Example:
var person = Person.create({
firstName: 'Nathan',
lastName: 'Fritz',
});
person.favoriteColor = 'blue';
person.loadData({
favoriteColor: 'green',
favoriteFood: 'burrito',
});
console.log(person.toJSON());
// {firstName: 'Nathan', lastName: 'Fritz', favoriteFood: 'burrito', favoriteColor: 'green'}
``