Comparing version 0.3.2 to 0.4.0
@@ -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 @@ |
@@ -7,1 +7,2 @@ "use strict"; | ||
exports.Document = require('./lib/document'); | ||
exports.EmbeddedDocument = require('./lib/embedded-document'); |
"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 @@ }); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
168033
22
3999
315
5