Socket
Socket
Sign inDemoInstall

camo

Package Overview
Dependencies
124
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.3.2 to 0.4.0

lib/base-document.js

11

CHANGELOG.md

@@ -0,1 +1,12 @@

## 0.4.0 (2015-06-22)
Features:
- Changed `.isModel()` to `.isDocument()`.
- Added `EmbeddedDocument` class and tests.
+ The following features work with `EmbeddedDocument`s:
= Schema options: default, min, max, type, choices
= All types supported in `Document` also work in `EmbeddedDocument`
= Array of `EmbeddedDocument`s
= Pre/post validate, save, and delete hooks
## 0.3.2 (2015-06-19)

@@ -2,0 +13,0 @@

1

index.js

@@ -7,1 +7,2 @@ "use strict";

exports.Document = require('./lib/document');
exports.EmbeddedDocument = require('./lib/embedded-document');

470

lib/document.js
"use strict";
var _ = require('lodash');
var EventEmitter = require('events').EventEmitter;
var DB = require('./clients').getClient;
var BaseDocument = require('./base-document');
var isSupportedType = require('./validate').isSupportedType;
var isValidType = require('./validate').isValidType;
var isInChoices = require('./validate').isInChoices;
var isArray = require('./validate').isArray;
var isModel = require('./validate').isModel;
var isDocument = require('./validate').isDocument;
var isEmbeddedDocument = require('./validate').isEmbeddedDocument;
var isString = require('./validate').isString;
var normalizeType = function(property) {
// TODO: Only copy over stuff we support
class Document extends BaseDocument {
constructor(name) {
super();
var typeDeclaration = {};
if (property.type) {
typeDeclaration = property;
} else if (isSupportedType(property)) {
typeDeclaration.type = property;
} else {
throw new Error('Unsupported type or bad variable. ' +
'Remember, non-persisted objects must start with an underscore (_). Got:', property);
}
return typeDeclaration;
};
// For more handler methods:
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy
let schemaProxyHandler = {
get: function(target, propKey) {
// Return current value, if set
if (propKey in target._values) {
return target._values[propKey];
}
// Alias 'id' and '_id'
if (propKey === 'id') {
return target._values._id;
}
return Reflect.get(target, propKey);
},
set: function(target, propKey, value) {
if (propKey in target._schema) {
target._values[propKey] = value;
return true;
}
// Alias 'id' and '_id'
if (propKey === 'id') {
target._values._id = value;
return true;
}
return Reflect.set(target, propKey, value);
},
deleteProperty: function(target, propKey) {
delete target._schema[propKey];
delete target._values[propKey];
return true;
},
has: function(target, propKey) {
return propKey in target._schema || Reflect.has(target, propKey);
}
};
class Document {
constructor(name) {
this._meta = {
collection: name
};
this._schema = { // Defines document structure/properties
_id: { type: String }, // Native ID to backend database
};
this._values = {}; // Contains values for properties defined in schema
}

@@ -88,18 +24,10 @@

// how, we'll be lazy use this.
static extendsDocument() {
return true;
static documentClass() {
return 'document';
}
extendsDocument() {
return true;
documentClass() {
return 'document';
}
get id() {
return this._values._id;
}
set id(id) {
this._values._id = id;
}
get meta() {

@@ -124,34 +52,25 @@ return this._meta;

schema(extension) {
if (!extension) return;
_.extend(this._schema, extension);
}
save() {
var that = this;
/*
* Pre/post Hooks
*
* To add a hook, the extending class just needs
* to override the appropriate hook method below.
*/
// TODO: Should we generate a list of embeddeds on creation?
var embeddeds = [];
_.keys(this._values).forEach(function(v) {
if (isEmbeddedDocument(that._schema[v].type) ||
(isArray(that._schema[v].type) && isEmbeddedDocument(that._schema[v].type[0]))) {
embeddeds = embeddeds.concat(that._values[v]);
}
});
preValidate() { }
// Also need to call pre/post functions for embeddeds
var preValidatePromises = [];
preValidatePromises = preValidatePromises.concat(_.invoke(embeddeds, 'preValidate'));
preValidatePromises.push(that.preValidate());
postValidate() { }
return Promise.all(preValidatePromises).then(function() {
preSave() { }
// Ensure we at least have defaults set
postSave() { }
preDelete() { }
postDelete() { }
save() {
var that = this;
return new Promise(function(resolve, reject) {
return resolve(that.preValidate());
}).then(function() {
// Ensure we at least have defaults set
// TODO: We already do this on .create(), so
// should it really be done again?
_.keys(that._schema).forEach(function(key) {

@@ -164,45 +83,4 @@ if (!(key in that._values)) {

// Validate the assigned type, choices, and min/max
_.keys(that._values).forEach(function(key) {
var value = that._values[key];
that.validate();
if (!isValidType(value, that._schema[key].type)) {
// TODO: Formatting should probably be done somewhere else
var typeName = null;
var valueName = null;
if (Array.isArray(that._schema[key].type)) {
typeName = '[' + that._schema[key].type[0].name + ']';
} else {
typeName = that._schema[key].type.name;
}
if (Array.isArray(value)) {
// TODO: Not descriptive enough! Strings can look like numbers
valueName = '[' + value.toString() + ']';
} else {
valueName = typeof(value);
}
let err = new Error('Value assigned to ' + that._meta.collection + '.' + key +
' should be ' + typeName + ', got ' + valueName);
return Promise.reject(err);
}
if (!isInChoices(that._schema[key].choices, value)) {
let err = new Error('Value assigned to ' + that._meta.collection + '.' + key +
' should be in [' + that._schema[key].choices.join(', ') + '], got ' + value);
return Promise.reject(err);
}
if (that._schema[key].min && value < that._schema[key].min) {
let err = new Error('Value assigned to ' + that._meta.collection + '.' + key +
' is less than min, ' + that._schema[key].min + ', got ' + value);
return Promise.reject(err);
}
if (that._schema[key].max && value > that._schema[key].max) {
let err = new Error('Value assigned to ' + that._meta.collection + '.' + key +
' is less than max, ' + that._schema[key].max + ', got ' + value);
return Promise.reject(err);
}
});
// TODO: We should instead track what has changed and

@@ -220,6 +98,6 @@ // only update those values. Maybe make that._changed

_.keys(that._values).forEach(function(key) {
if (isModel(that._values[key]) || // isModel OR
(isArray(that._values[key]) && // isArray AND contains value AND value isModel
if (isDocument(that._values[key]) || // isDocument OR
(isArray(that._values[key]) && // isArray AND contains value AND value isDocument
that._values[key].length > 0 &&
isModel(that._values[key][0]))) {
isDocument(that._values[key][0]))) {

@@ -239,12 +117,38 @@ // Handle array of references (ex: { type: [MyObject] })

// Replace EmbeddedDocument references with just their data
_.keys(that._values).forEach(function(key) {
if (isEmbeddedDocument(that._values[key]) || // isEmbeddedDocument OR
(isArray(that._values[key]) && // isArray AND contains value AND value isEmbeddedDocument
that._values[key].length > 0 &&
isEmbeddedDocument(that._values[key][0]))) {
// Handle array of references (ex: { type: [MyObject] })
if (isArray(that._values[key])) {
toUpdate[key] = [];
that._values[key].forEach(function(v) {
toUpdate[key].push(v.toData());
});
} else {
toUpdate[key] = that._values[key].toData();
}
}
});
return toUpdate;
}).then(function(data) {
// TODO: Need to return this so if its a promise it'll work
that.postValidate();
return data;
}).then(function(data) {
// TODO: Need to return this so if its a promise it'll work
that.preSave();
return data;
}).then(function(data) {
var postValidatePromises = [];
postValidatePromises.push(data); // TODO: hack?
postValidatePromises = postValidatePromises.concat(_.invoke(embeddeds, 'postValidate'));
postValidatePromises.push(that.postValidate());
return Promise.all(postValidatePromises);
}).then(function(prevData) {
var data = prevData[0];
var preSavePromises = [];
preSavePromises.push(data); // TODO: hack?
preSavePromises = preSavePromises.concat(_.invoke(embeddeds, 'preSave'));
preSavePromises.push(that.preSave());
return Promise.all(preSavePromises);
}).then(function(prevData) {
var data = prevData[0];
return DB().save(that._meta.collection, that.id, data);

@@ -256,3 +160,6 @@ }).then(function(id) {

}).then(function() {
return that.postSave();
var postSavePromises = [];
postSavePromises = postSavePromises.concat(_.invoke(embeddeds, 'postSave'));
postSavePromises.push(that.postSave());
return Promise.all(postSavePromises);
}).then(function() {

@@ -268,8 +175,25 @@ return that;

return new Promise(function(resolve, reject) {
return resolve(that.preDelete());
}).then(function() {
var embeddeds = [];
_.keys(this._values).forEach(function(v) {
if (isEmbeddedDocument(that._schema[v].type) ||
(isArray(that._schema[v].type) && isEmbeddedDocument(that._schema[v].type[0]))) {
embeddeds = embeddeds.concat(that._values[v]);
}
});
var preDeletePromises = [];
preDeletePromises = preDeletePromises.concat(_.invoke(embeddeds, 'preDelete'));
preDeletePromises.push(that.preDelete());
return Promise.all(preDeletePromises).then(function() {
return DB().delete(that._meta.collection, that.id);
}).then(function(deleteReturn) {
that.postDelete();
var postDeletePromises = [];
postDeletePromises.push(deleteReturn); // TODO: hack?
postDeletePromises = postDeletePromises.concat(_.invoke(embeddeds, 'postDelete'));
postDeletePromises.push(that.postDelete());
return Promise.all(postDeletePromises);
}).then(function(prevData) {
var deleteReturn = prevData[0];
return deleteReturn;

@@ -302,8 +226,8 @@ });

var doc = that.fromData(data, that);
if (populate) {
return that.dereferenceDocuments(doc);
return that.fromData(data, populate);
}).then(function(docs) {
if (docs && docs.length > 0) {
return docs[0];
}
return doc;
return null;
});

@@ -323,13 +247,5 @@ }

.then(function(datas) {
var documents = [];
datas.forEach(function(d) {
documents.push(that.fromData(d, that));
});
if (documents.length < 1) return documents;
if (populate) {
return that.dereferenceDocuments(documents);
}
return documents;
return that.fromData(datas, populate);
}).then(function(docs) {
return docs;
});

@@ -343,186 +259,18 @@ }

static clearCollection() {
return DB().clearCollection(this.collectionName());
}
generateSchema() {
var that = this;
_.keys(this).forEach(function(k) {
// Ignore private variables
if (_.startsWith(k, '_')) {
return;
}
// Normalize the type format
that._schema[k] = normalizeType(that[k]);
// Assign a default if needed
if (isArray(that._schema[k].type)) {
that._values[k] = that.getDefault(k) || [];
} else {
that._values[k] = that.getDefault(k);
}
// Should we delete these member variables so they
// don't get in the way? Probably a waste of time
// since the Proxy intercepts all gets/sets to them.
//delete that[k];
});
}
static create() {
var instance = new this();
instance.generateSchema();
return new Proxy(instance, schemaProxyHandler);
}
static fromData(data, clazz) {
var instance = clazz.create();
_.keys(data).forEach(function(key) {
var value = null;
if (data[key] === null) {
value = instance.getDefault(key);
} else {
value = data[key];
}
// If its not in the schema, we don't care about it... right?
if (key in instance._schema) {
instance._values[key] = value;
}
});
instance.id = data._id;
return instance;
}
static dereferenceDocuments(docs) {
if (!docs) return docs;
var documents = null;
if (!isArray(docs)) {
documents = [docs];
} else if (docs.length < 1) {
return docs;
} else {
documents = docs;
static fromData(datas, populate) {
if (!isArray(datas)) {
datas = [datas];
}
// Load all 1-level-deep references
// First, find all unique keys needed to be loaded...
var keys = [];
// TODO: Bad assumption: Not all documents in the database will have the same schema...
// Hmm, if this is true, thats an error on the user.
var anInstance = documents[0];
_.keys(anInstance._schema).forEach(function(key) {
// Handle array of references (ex: { type: [MyObject] })
if (isArray(anInstance._schema[key].type) &&
anInstance._schema[key].type.length > 0 &&
isModel(anInstance._schema[key].type[0])) {
keys.push(key);
return super.fromData(datas, populate).then(function(instances) {
for (var i = 0; i < instances.length; i++) {
instances[i].id = datas[i]._id;
}
// Handle anInstance[key] being a string id, a native id, or a Document instance
else if ((isString(anInstance[key]) || DB().isNativeId(anInstance[key])) &&
isModel(anInstance._schema[key].type)) {
keys.push(key);
}
return instances;
});
// ...then get all ids for each type of reference to be loaded...
// ids = {
// houses: {
// 'abc123': ['ak23lj', '2kajlc', 'ckajl32'],
// 'l2jo99': ['28dsa0']
// },
// friends: {
// '1039da': ['lj0adf', 'k2jha']
// }
//}
var ids = {};
keys.forEach(function(k) {
ids[k] = {};
documents.forEach(function(d) {
ids[k][DB().toCanonicalId(d.id)] = [].concat(d[k]); // Handles values and arrays
// Also, initialize document member arrays
// to assign to later if needed
if (isArray(d[k])) {
d[k] = [];
}
});
});
// ...then for each array of ids, load them all...
var loadPromises = [];
_.keys(ids).forEach(function(key) {
var keyIds = [];
_.keys(ids[key]).forEach(function(k) {
// Before adding to list, we convert id to the
// backend database's native ID format.
keyIds = keyIds.concat(ids[key][k]);
});
// Handle array of references (like [MyObject])
var type = null;
if (isArray(anInstance._schema[key].type)) {
type = anInstance._schema[key].type[0];
} else {
type = anInstance._schema[key].type;
}
// Bulk load dereferences
var p = type.loadMany({ '_id': { $in: keyIds } }, { populate: false })
.then(function(dereferences) {
// Assign each dereferenced object to parent
dereferences.forEach(function(deref) {
// For each model member...
_.keys(ids[key]).forEach(function(k) {
// ...if this dereference is in the array...
var cIds = [];
ids[key][k].forEach(function(i) {
cIds.push(DB().toCanonicalId(i));
});
if (cIds.indexOf(DB().toCanonicalId(deref.id)) > -1) {
// ...find the document it belongs to...
documents.forEach(function(doc) {
// ...and make the assignment (value or array-based).
if (DB().toCanonicalId(doc.id) === k) {
if (isArray(anInstance._schema[key].type)) {
doc[key].push(deref);
} else {
doc[key] = deref;
}
}
});
}
});
});
});
loadPromises.push(p);
});
// ...and finally execute all promises and return our
// fully loaded documents.
return Promise.all(loadPromises).then(function() {
return docs;
});
}
getDefault(schemaProp) {
if (schemaProp in this._schema && 'default' in this._schema[schemaProp]) {
var def = this._schema[schemaProp].default;
var defVal = typeof(def) === 'function' ? def() : def;
this._values[schemaProp] = defVal; // TODO: Wait... should we be assigning it here?
return defVal;
}
return null;
static clearCollection() {
return DB().clearCollection(this.collectionName());
}

@@ -529,0 +277,0 @@ }

@@ -31,10 +31,14 @@ var _ = require('lodash');

var isModel = function(m) {
return m && m.extendsDocument && m.extendsDocument();
var isDocument = function(m) {
return m && m.documentClass && m.documentClass() === 'document';
};
var isEmbeddedDocument = function(e) {
return e && e.documentClass && e.documentClass() === 'embedded';
};
var isSupportedType = function(t) {
return (t === String || t === Number || t === Boolean ||
t === Buffer || t === Date || t === Array ||
isArray(t) || t === Object || (t.extendsDocument && t.extendsDocument()));
isArray(t) || t === Object || typeof(t.documentClass) === 'function');
};

@@ -57,4 +61,6 @@

return isObject(value);
} else if (type.extendsDocument && type.extendsDocument()) {
return isModel(value);
} else if (type.documentClass && type.documentClass() === 'document') {
return isDocument(value);
} else if (type.documentClass && type.documentClass() === 'embedded') {
return isEmbeddedDocument(value);
} else {

@@ -111,3 +117,4 @@ throw new Error('Unsupported type: ' + type.name);

exports.isArray = isArray;
exports.isModel = isModel;
exports.isDocument = isDocument;
exports.isEmbeddedDocument = isEmbeddedDocument;
exports.isSupportedType = isSupportedType;

@@ -114,0 +121,0 @@ exports.isType = isType;

{
"name": "camo",
"version": "0.3.2",
"version": "0.4.0",
"description": "A lightweight ES6 ODM for Mongo-like databases.",

@@ -5,0 +5,0 @@ "author": {

@@ -10,2 +10,3 @@ # Camo

* <a href="#declaring-your-document">Declaring Your Document</a>
* <a href="#embedded-documents">Embedded Documents</a>
* <a href="#creating-and-saving">Creating and Saving</a>

@@ -80,3 +81,3 @@ * <a href="#loading">Loading</a>

### Declaring Your Document
All models must inherit from `Document`, which handles much of the interface to your backend NoSQL database.
All models must inherit from the `Document` class, which handles much of the interface to your backend NoSQL database.

@@ -116,5 +117,6 @@ ```javascript

- `Array`
- `EmbeddedDocument`
- Document Reference
Arrays can either be declared as either un-typed (`[]`), or typed (`[String]`). Typed arrays are enforced by Camo and an `Error` will be thrown if a value of the wrong type is saved in the array. Arrays of references are also supported.
Arrays can either be declared as either un-typed (using `Array` or `[]`), or typed (using the `[TYPE]` syntax, like `[String]`). Typed arrays are enforced by Camo on `.save()` and an `Error` will be thrown if a value of the wrong type is saved in the array. Arrays of references are also supported.

@@ -164,2 +166,43 @@ To declare a member variable in the schema, either directly assign it one of the types above, or assign it an object with options. Like this:

#### Embedded Documents
Embedded documents can also be used within `Document`s. You must declare them separately from the main `Document` that it is being used in. `EmbeddedDocument`s are good for when you need an `Object`, but also need enforced schemas, validation, defaults, hooks, and member functions. All of the options (type, default, min, etc) mentioned above work on `EmbeddedDocument`s as well.
```javascript
var Document = require('camo').Document;
var EmbeddedDocument = require('camo').EmbeddedDocument;
class Money extends EmbeddedDocument {
constructor() {
super();
this.value = {
type: Number,
choices: [1, 5, 10, 20, 50, 100]
};
this.currency = {
type: String,
default: 'usd'
}
}
}
class Wallet extends Document {
constructor() {
super('wallet');
this.contents = [Money];
}
}
var wallet = Wallet.create();
wallet.contents.push(Money.create());
wallet.contents[0].value = 5;
wallet.contents.push(Money.create());
wallet.contents[1].value = 100;
wallet.save().then(function() {
console.log('Both Wallet and Money objects were saved!');
});
````
### Creating and Saving

@@ -222,4 +265,6 @@ To create a new instance of our document, we need to use the `.create()` method, which handles all of the construction for us.

### Hooks
Camo provides hooks for you to execute code before and after critical parts of your database interactions. For each hook you use, you may return a value (which, as of now, will be discarded) or a Promise for executing asynchronous code. Using Promises, we don't need to provide separate async and sync hooks, thus making your code simpler and easier to understand.
Camo provides hooks for you to execute code before and after critical parts of your database interactions. For each hook you use, you may return a value (which, as of now, will be discarded) or a Promise for executing asynchronous code. Using Promises throughout Camo allows us to not have to provide separate async and sync hooks, thus making your code simpler and easier to understand.
Hooks can be used not only on `Document` objects, but `EmbeddedDocument` objects as well. The embedded object's hooks will be called when it's parent `Document` is saved/validated/deleted (depending on the hook you provide).
In order to create a hook, you must override a class method. The hooks currently provided, and their corresponding methods, are:

@@ -226,0 +271,0 @@

@@ -8,3 +8,3 @@ "use strict";

var Document = require('../index').Document;
var isModel = require('../lib/validate').isModel;
var isDocument = require('../lib/validate').isDocument;
var Data = require('./data');

@@ -182,3 +182,3 @@ var getData1 = require('./util').data1;

expect(b.employees[0].boss).to.not.be.null;
expect(!isModel(b.employees[0].boss)).to.be.true;
expect(!isDocument(b.employees[0].boss)).to.be.true;
}).then(done, done);

@@ -185,0 +185,0 @@ });

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc