Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom types #180

Merged
merged 8 commits into from
Jan 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,42 @@ Fortune implements everything you need to get started with JSON API, with a few

It does not come with any authentication or authorization, you should implement your own application-specific logic (see [keystore.js](//github.com/daliwali/fortune/blob/master/examples/keystore.js) for an example).

#### Custom Types

Custom type defines its own schema and hooks and might be injected to any resource's schema side by side to standard data types and incorporates its data hooks into the resources' ones.
Usage example:

```
app.customType("date-timezone", {
date: String,
timeZone: String
}).beforeWrite([{
name: 'datetz2db',
priority: -1,
init: function() {
return function(req, res) {
return DateTz.todb(this)
}
}
}]).afterRead([{
name: 'datetz4db',
priority: 1000,
init: function() {
return function(req, res) {
return DateTz.fromdb(this);
}
}
}])

app.resource("schedule", {
...
arrival: 'date-timezone'
...
})
```

The datetz2db and datetz4db will be automatically attached to the parent resource hook chain.

## Guide & Documentation

The full guide and API documentation are located at [fortunejs.com](http://fortunejs.com/).
Expand Down
93 changes: 77 additions & 16 deletions lib/fortune.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Fortune.prototype.resources = function(req){
var _this = this;

function filterHooks(hooks, time, type, resource, name){
var ary = hooks[time][type];
var ary = (hooks[time] || [])[type] || [];
return _.reduce(_this._hookFilters, function(memo, filter){
return filter(memo, name, time.replace('_', ''), type, resource)
}, ary);
Expand All @@ -198,18 +198,19 @@ Fortune.prototype.resources = function(req){
var resources = _.map(_this._resources, function(md, name){
var schema = _.clone(md.schema);

_.each(schema, function(v,k){
var vIsFunction = _.isFunction(v),
typeFn = vIsFunction ? v : v.type;

if(typeFn){
typeFn = typeFn.toString();
typeFn = typeFn.substr('function '.length);
typeFn = typeFn.substr(0, typeFn.indexOf('('));
var jsonFriendify = function(schema) {
_.each(schema, function(v,k){
if(_.isFunction(v)){
schema[k] = v.name;
}
else if(_.isObject(v)) {
schema[k] = jsonFriendify(v);
}
});
return schema;
}

schema[k] = vIsFunction ? typeFn : _.extend({}, v, {type: typeFn});
}
});
schema = jsonFriendify(schema);

var hooks = {
beforeRead: _.map(filterHooks(md.hooks, '_before', 'read', {}, name), function(h){ return h.name}),
Expand Down Expand Up @@ -252,6 +253,14 @@ Fortune.prototype._exposeResourceDefinitions = function() {
});
};

Fortune.prototype.customType = function(name, schema, options, schemaCallback) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema here is the thing passed into mongoose, right? It should also accept userland schema to expose in /resources output.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Pls take a look. Result looks like:

"deptDate": {
  "date": "String",
  "time": "String",
  "timeZone": "String"
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like i broke something, hold on.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected. Right now, it only accepts user-level schemas and make validations against them. On data level any complex objects are converted to mongoose.Schema.Mixed data type on adapter level (see here: https://github.com/flyvictor/fortune/blob/master/lib/adapters/mongodb.js#L68).
Also, simpler one-scheme syntax follows the same look and feel of an ordinal fortune resource.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if Mixed would really work here. If mongoose doesn't cast types for us (and it doesn't for Mixed type) we could get inconsistent data types.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue for this: #182

this._customTypes = this._customTypes || {}
this._customTypes[name] = _.extend({}, options, { name: name, schema: schema })
this._resource = name;

return this;
}

/**
* Define a resource and setup routes simultaneously. A schema field may be either a native type, a plain object, or a string that refers to a related resource.
*
Expand Down Expand Up @@ -303,6 +312,20 @@ Fortune.prototype.resource = function(name, schema, options, schemaCallback) {
return this;
}

var customTypes = {};
schema = _.mapValues(schema, function(value, key) {
if((typeof value).toLowerCase() === "string") {

// Verify there is a custom type with the name
var customType = _this._customTypes[value];
customTypes[key] = customType;

// Custom type has Mixed schema instead
return customType.schema;
}
return value;
});

this._resources = this._resources || {};
this._resources[name] = {
actions: actionsObj,
Expand All @@ -316,8 +339,47 @@ Fortune.prototype.resource = function(name, schema, options, schemaCallback) {
authMethods: authMethods,
validation: validation
};

plugins.init(this, this._resources[name]);

hooks.initGlobalHooks(_this._resources[name], _this.options);

var customHooks = _.flatten(_.map(_.keys(customTypes), function(key) {
// Modify a names of the hooks to include field name they applied to
_.each(customTypes[key].hooks, function(whenHooks, when) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it expect a structure like this? It kind of doesn't follow semantics of hooks?

{before: {
   write:  {
    fn: function(){}
   }
 }
}`

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does. But not sure what you meant by "It kind of doesn't follow semantics of hooks?". Can you pls extend the statement

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, think i got it. In design doc these hooks were passed through as properties of options, but chaining is probably even better.

_.each(whenHooks, function(actionHooks, action) {
_.each(actionHooks, function(actionHook) {
if(actionHook.name) {
actionHook.name = [key, actionHook.name].join('-');
}
// Set highest priority
actionHook.priority = 1000;

// Bind the handler to particular data type
var handler = actionHook.fn
if(handler) {
actionHook.fn = function(req, res) {
if(this[key]) {
this[key] = handler.call(this[key], req, res);
}
return this;
}
}
});
});
})
return customTypes[key].hooks;
}));

function mergeArray(to, from) {
if(_.isArray(to) && _.isArray(from)) {
return (to || []).concat(from);
}
}

var mergedHooks = _.merge(_.merge.apply(this, customHooks, mergeArray) || {}, _this._resources[name].hooks, mergeArray);
_this._resources[name].hooks = mergedHooks;

//Register updates in queryParser. Should be called here to make sure that all resources are registered
_this._exposeResourceDefinitions();
_this._querytree = querytree.init(this);
Expand All @@ -338,15 +400,15 @@ Fortune.prototype.resource = function(name, schema, options, schemaCallback) {
function () {
schema = _this._preprocessSchema(schema);

// Store a copy of the input.
_this._schema[name] = _.clone(schema);
try {
// Store a copy of the input.
_this._schema[name] = _.clone(schema);
schema = _this.adapter.schema(name, schema, options, schemaCallback);

// Pass any upsertKeys to the schema
schema.upsertKeys = upsertKeys || [];

_this._route(name, _this.adapter.model(name, schema,modelOptions), _this._resources, inflect, _this._querytree, _this._metadataProviders);
_this._route(name, _this.adapter.model(name, schema, modelOptions), _this._resources, inflect, _this._querytree, _this._metadataProviders);
_this._resourceInitialized();
} catch(error) {
console.trace('There was an error loading the "' + name + '" resource. ' + error.stack || err);
Expand Down Expand Up @@ -436,7 +498,6 @@ function GlobalHook(time, type){
}

function Hook(time, type){

return function(name, hooksArray, inlineConfig){
var that = this;
if (this.options.throwOnLegacyTransforms && (_.isFunction(name) || _.isFunction(hooksArray))){
Expand Down
15 changes: 10 additions & 5 deletions lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,23 @@ exports.addHook = function(name, hooks, stage, type, inlineConfig){

name.split(' ').forEach(function(resourceName) {
hooks = normalize(hooks);
var resource = _this._resources[resourceName];
if (!resource){
return console.warn('You are trying to attach a hook to %s, ' +
'that is not defined in this instance of fortune', resourceName);

var resource;
if (!_this._resources || !_this._resources[resourceName]) {
if(!_this._customTypes || !_this._customTypes[resourceName]) {
return console.warn('You are trying to attach a hook to %s, ' +
'that is not defined in this instance of fortune', resourceName);
}
else resource = _this._customTypes[resourceName];
}
else resource = _this._resources[resourceName];

resource.hooks = resource.hooks || {};
resource.hooks[stage] = resource.hooks[stage] || {};
resource.hooks[stage][type] = resource.hooks[stage][type] || [];
_.each(hooks, function(hook){
var hookOptions = getHookConfig(hook, resource, inlineConfig);
var fn = hook.init(hookOptions, _this);
//fn._priority = hook.priority || 0;
resource.hooks[stage][type].push(_.extend({
_priority: hook.priority || 0,
name: hook.name,
Expand Down
2 changes: 1 addition & 1 deletion lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ function route(name, model, resources, inflect, querytree, metadataProviders) {
instrumentor.createTracer( tracePrefix, function(resolve, reject){
//If no transforms found for this resource return without change
var hooks = resources[model].hooks;
if (!hooks[time][type]) return resolve(resource);
if (!hooks[time] || !hooks[time][type]) return resolve(resource);
resource = _.extend(resource, isNew);
var transform = resource;
_.each(filterHooks(hooks, time, type, resource, name), function(h){
Expand Down
27 changes: 23 additions & 4 deletions test/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var hooks = {};
if (req.query['fail' + type]) {
console.log('Failing hook',type);
_.defer(function() {
res.send(321);
res.sendStatus(321);
});
if (req.query['fail' + type] === 'boolean')
return false;
Expand Down Expand Up @@ -66,14 +66,32 @@ module.exports = function(options, port, ioPort) {
}
}]);


app.beforeAll(hooks.beforeAll)
.beforeAllRead(hooks.beforeAllRead)
.beforeAllWrite(hooks.beforeAllWrite)
.afterAll(hooks.afterAll)
.afterAllRead(hooks.afterAllRead)
.afterAllWrite(hooks.afterAllWrite)

.customType("location", {
lat: Number,
lon: Number
})
.beforeWrite([{
name: 'todb',
init: function() {
return function(req, res){
return this;
}
}
}])
.afterRead([{
name: 'fromdb',
init: function(){
return function(req, res){
return this;
}
}
}])
.resource('person', {
name: String,
official: String,
Expand All @@ -97,6 +115,7 @@ module.exports = function(options, port, ioPort) {
nestedField1: String,
nestedField2: Number
}],
location: 'location',
upsertTest : String,
_tenantId: String
}, {
Expand Down Expand Up @@ -195,7 +214,7 @@ module.exports = function(options, port, ioPort) {

.before('person pet', function(req, res){
if (this.email === '[email protected]'){
res.send(321);
res.sendStatus(321);
return false;
}
return this;
Expand Down
Loading