<commit_before>// activity.js
//
// data object representing an activity
//
// Copyright 2011,2012 StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var databank = require('databank'),
    Step = require('step'),
    dateFormat = require('dateformat'),
    URLMaker = require('../urlmaker').URLMaker,
    IDMaker = require('../idmaker').IDMaker,
    ActivityObject = require('./activityobject').ActivityObject,
    DatabankObject = databank.DatabankObject;

var Activity = DatabankObject.subClass('activity');

Activity.schema = { pkey: 'id',
                    fields: ['actor',
                             'content',
                             'generator',
                             'icon',
                             'id',
                             'object',
                             'published',
                             'provider',
                             'target',
                             'title',
                             'url',
                             'uuid',
                             'updated',
                             'verb'],
                    indices: ['actor.id', 'object.id', 'uuid'] };

Activity.verbs = ['add',
                  'cancel',
                  'checkin',
                  'delete',
                  'favorite',
                  'follow',
                  'give',
                  'ignore',
                  'invite',
                  'join',
                  'leave',
                  'like',
                  'make-friend',
                  'play',
                  'post',
                  'receive',
                  'remove',
                  'remove-friend',
                  'request-friend',
                  'rsvp-maybe',
                  'rsvp-no',
                  'rsvp-yes',
                  'save',
                  'share',
                  'stop-following',
                  'tag',
                  'unfavorite',
                  'unlike',
                  'unsave',
                  'update'];

var i = 0, verb;

// Constants-like members for activity verbs

for (i = 0; i < Activity.verbs.length; i++) {
    verb = Activity.verbs[i];
    Activity[verb.toUpperCase().replace('-', '_')] = verb;
}

Activity.init = function(inst, properties) {

    DatabankObject.init(inst, properties);

    if (!this.verb) {
        this.verb = "post";
    }

    if (inst.actor) {
        inst.actor = ActivityObject.toObject(inst.actor, ActivityObject.PERSON);
    }

    if (inst.object) {
        inst.object = ActivityObject.toObject(inst.object);
    }
};

Activity.prototype.apply = function(defaultActor, callback) {

    var Edge = require('./edge').Edge;

    // Ensure an actor

    this.actor = this.actor || defaultActor;

    // XXX: Polymorphism is probably the right thing here
    // but I kinda CBA. How's this: rewrite when we get over 5 case's...?

    switch (this.verb) {
    case Activity.POST:
        // Force stub author data
        this.object.author = {objectType: this.actor.objectType,
                              id: this.actor.id};
        // Is this it...?
        ActivityObject.createObject(this.object, function(err, result) {
            callback(err, result);
        });
        break;
    case Activity.FOLLOW:
        if (!this.actor.id || !this.object.id) {
            callback(new Error("No ID."));
        }
        // XXX: OStatus if necessary
        Edge.create({id: this.actor.id + '->' + this.object.id,
                     from: { id: this.actor.id, objectType: this.actor.objectType },
                     to: {id: this.object.id, objectType: this.object.objectType }},
                    callback);
        break;
    case Activity.STOP_FOLLOWING:
        var edges = [];
        // XXX: OStatus if necessary
        Edge.search({'from.id': this.actor.id, 'to.id': this.object.id}, function(err, edges) {
            if (err) {
                callback(err);
            } else if (edges.length === 0) { // that's bad
                callback(new Error("No such edge."));
            } else if (edges.length > 1) { // that's worse
                // XXX: Kill 'em all
                callback(new Error("Too many edges."));
            } else {
                edges[0].del(callback);
            }
        });
        break;
    default:
        // XXX: fave/unfave, join/leave, ...?
        callback(null);
        break;
    }
};

// XXX: identical to save

Activity.beforeCreate = function(props, callback) {

    var now = dateFormat(new Date(), "isoDateTime", true);

    props.updated = now;

    if (!props.published) {
        props.published = now;
    }

    if (!props.id) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI('activity', props.uuid);
        props.links = {
            self: URLMaker.makeURL('api/activity/' + props.uuid)
        };
        // FIXME: assumes person data was set and that it's a local actor
        props.url  = URLMaker.makeURL(props.actor.preferredUsername + '/activity/' + props.uuid);

        // default verb

        if (!props.verb) {
            props.verb = 'post';
        }
    }

    if (!props.actor) {
        callback(new Error("Activity has no actor"), null);
    }

    if (!props.object) {
        callback(new Error("Activity has no object"), null);
    }

    var actor = null,
        object = null;

    Step (
        function() {
            ActivityObject.ensureObject(props.actor, this);
        },
        function(err, result) {
            if (err) throw err;
            actor = result;
            ActivityObject.ensureObject(props.object, this);
        },
        function(err, result) {
            if (err) {
                callback(err, null);
                return;
            }
            object = result;
            // slim them down to references
            // FIXME: probably shouldn't overwrite attributes
            props.actor = {objectType: actor.objectType,
                         id: actor.id};
            props.object = {objectType: object.objectType,
                          id: object.id};
            callback(null, props);
        }
    );
};

Activity.prototype.afterCreate = function(callback) {
    this.expand(callback);
};

Activity.prototype.beforeUpdate = function(props, callback) {

    var now = dateFormat(new Date(), "isoDateTime", true);

    props.updated = now;

    callback(null, props);
};

// When save()'ing an activity, ensure the actor and object
// are persisted, then save them by reference.

Activity.prototype.beforeSave = function(callback) {

    var now = dateFormat(new Date(), "isoDateTime", true),
        act = this;

    act.updated = now;

    if (!act.published) {
        act.published = now;
    }

    if (!act.id) {
        act.uuid = IDMaker.makeID();
        act.id   = ActivityObject.makeURI('activity', act.uuid);
        act.links = {};
        act.links.self = URLMaker.makeURL('api/activity/' + act.uuid);
        // FIXME: assumes person data was set and that it's a local actor
        act.url  = URLMaker.makeURL(act.actor.preferredUsername + '/activity/' + act.uuid);
    }

    if (!act.actor) {
        callback(new Error("Activity has no actor"));
    }

    if (!act.object) {
        callback(new Error("Activity has no object"));
    }

    var actor = null,
        object = null;

    Step (
        function() {
            ActivityObject.ensureObject(act.actor, this);
        },
        function(err, result) {
            if (err) throw err;
            actor = result;
            ActivityObject.ensureObject(act.object, this);
        },
        function(err, result) {
            if (err) {
                callback(err);
                return;
            }
            object = result;
            // slim them down to references
            // FIXME: probably shouldn't overwrite attributes
            act.actor = {objectType: actor.objectType,
                         id: actor.id};
            act.object = {objectType: object.objectType,
                          id: object.id};
            callback(null);
        }
    );
};

// When get()'ing an activity, also get the actor and the object,
// which are saved by reference

Activity.prototype.afterGet = function(callback) {
    this.expand(callback);
};

Activity.prototype.expand = function(callback) {
    var act = this;

    Step(
        function() {
            act.expandActor(this.parallel());
            act.expandObject(this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, act);
            }
        }
    );
};

Activity.prototype.expandObject = function(callback) {
    var act = this;

    ActivityObject.getObject(this.object.objectType, this.object.id, function(err, object) {
        if (err) {
            callback(err, null);
        } else {
            // XXX: Check for the old object!
            act.object = object;
            if (act.verb == "post") {
                delete act.object.author;
            }
            callback(null, act);
        }
    });
};

Activity.prototype.expandActor = function(callback) {
    var act = this;

    ActivityObject.getObject(this.actor.objectType, this.actor.id, function(err, actor) {
        if (err) {
            callback(err, null);
        } else {
            // XXX: Check for the old actor!
            act.actor = actor;
            callback(null, this);
        }
    });
};

exports.Activity = Activity;
<commit_msg>Add an afterSave() to expand actor, object<commit_after>// activity.js
//
// data object representing an activity
//
// Copyright 2011,2012 StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var databank = require('databank'),
    Step = require('step'),
    dateFormat = require('dateformat'),
    URLMaker = require('../urlmaker').URLMaker,
    IDMaker = require('../idmaker').IDMaker,
    ActivityObject = require('./activityobject').ActivityObject,
    DatabankObject = databank.DatabankObject;

var Activity = DatabankObject.subClass('activity');

Activity.schema = { pkey: 'id',
                    fields: ['actor',
                             'content',
                             'generator',
                             'icon',
                             'id',
                             'object',
                             'published',
                             'provider',
                             'target',
                             'title',
                             'url',
                             'uuid',
                             'updated',
                             'verb'],
                    indices: ['actor.id', 'object.id', 'uuid'] };

Activity.verbs = ['add',
                  'cancel',
                  'checkin',
                  'delete',
                  'favorite',
                  'follow',
                  'give',
                  'ignore',
                  'invite',
                  'join',
                  'leave',
                  'like',
                  'make-friend',
                  'play',
                  'post',
                  'receive',
                  'remove',
                  'remove-friend',
                  'request-friend',
                  'rsvp-maybe',
                  'rsvp-no',
                  'rsvp-yes',
                  'save',
                  'share',
                  'stop-following',
                  'tag',
                  'unfavorite',
                  'unlike',
                  'unsave',
                  'update'];

var i = 0, verb;

// Constants-like members for activity verbs

for (i = 0; i < Activity.verbs.length; i++) {
    verb = Activity.verbs[i];
    Activity[verb.toUpperCase().replace('-', '_')] = verb;
}

Activity.init = function(inst, properties) {

    DatabankObject.init(inst, properties);

    if (!this.verb) {
        this.verb = "post";
    }

    if (inst.actor) {
        inst.actor = ActivityObject.toObject(inst.actor, ActivityObject.PERSON);
    }

    if (inst.object) {
        inst.object = ActivityObject.toObject(inst.object);
    }
};

Activity.prototype.apply = function(defaultActor, callback) {

    var Edge = require('./edge').Edge;

    // Ensure an actor

    this.actor = this.actor || defaultActor;

    // XXX: Polymorphism is probably the right thing here
    // but I kinda CBA. How's this: rewrite when we get over 5 case's...?

    switch (this.verb) {
    case Activity.POST:
        // Force stub author data
        this.object.author = {objectType: this.actor.objectType,
                              id: this.actor.id};
        // Is this it...?
        ActivityObject.createObject(this.object, function(err, result) {
            callback(err, result);
        });
        break;
    case Activity.FOLLOW:
        if (!this.actor.id || !this.object.id) {
            callback(new Error("No ID."));
        }
        // XXX: OStatus if necessary
        Edge.create({id: this.actor.id + '->' + this.object.id,
                     from: { id: this.actor.id, objectType: this.actor.objectType },
                     to: {id: this.object.id, objectType: this.object.objectType }},
                    callback);
        break;
    case Activity.STOP_FOLLOWING:
        var edges = [];
        // XXX: OStatus if necessary
        Edge.search({'from.id': this.actor.id, 'to.id': this.object.id}, function(err, edges) {
            if (err) {
                callback(err);
            } else if (edges.length === 0) { // that's bad
                callback(new Error("No such edge."));
            } else if (edges.length > 1) { // that's worse
                // XXX: Kill 'em all
                callback(new Error("Too many edges."));
            } else {
                edges[0].del(callback);
            }
        });
        break;
    default:
        // XXX: fave/unfave, join/leave, ...?
        callback(null);
        break;
    }
};

// XXX: identical to save

Activity.beforeCreate = function(props, callback) {

    var now = dateFormat(new Date(), "isoDateTime", true);

    props.updated = now;

    if (!props.published) {
        props.published = now;
    }

    if (!props.id) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI('activity', props.uuid);
        props.links = {
            self: URLMaker.makeURL('api/activity/' + props.uuid)
        };
        // FIXME: assumes person data was set and that it's a local actor
        props.url  = URLMaker.makeURL(props.actor.preferredUsername + '/activity/' + props.uuid);

        // default verb

        if (!props.verb) {
            props.verb = 'post';
        }
    }

    if (!props.actor) {
        callback(new Error("Activity has no actor"), null);
    }

    if (!props.object) {
        callback(new Error("Activity has no object"), null);
    }

    var actor = null,
        object = null;

    Step (
        function() {
            ActivityObject.ensureObject(props.actor, this);
        },
        function(err, result) {
            if (err) throw err;
            actor = result;
            ActivityObject.ensureObject(props.object, this);
        },
        function(err, result) {
            if (err) {
                callback(err, null);
                return;
            }
            object = result;
            // slim them down to references
            // FIXME: probably shouldn't overwrite attributes
            props.actor = {objectType: actor.objectType,
                         id: actor.id};
            props.object = {objectType: object.objectType,
                          id: object.id};
            callback(null, props);
        }
    );
};

Activity.prototype.afterCreate = function(callback) {
    this.expand(callback);
};

Activity.prototype.afterSave = function(callback) {
    this.expand(callback);
};

Activity.prototype.beforeUpdate = function(props, callback) {

    var now = dateFormat(new Date(), "isoDateTime", true);

    props.updated = now;

    callback(null, props);
};

// When save()'ing an activity, ensure the actor and object
// are persisted, then save them by reference.

Activity.prototype.beforeSave = function(callback) {

    var now = dateFormat(new Date(), "isoDateTime", true),
        act = this;

    act.updated = now;

    if (!act.published) {
        act.published = now;
    }

    if (!act.id) {
        act.uuid = IDMaker.makeID();
        act.id   = ActivityObject.makeURI('activity', act.uuid);
        act.links = {};
        act.links.self = URLMaker.makeURL('api/activity/' + act.uuid);
        // FIXME: assumes person data was set and that it's a local actor
        act.url  = URLMaker.makeURL(act.actor.preferredUsername + '/activity/' + act.uuid);
    }

    if (!act.actor) {
        callback(new Error("Activity has no actor"));
    }

    if (!act.object) {
        callback(new Error("Activity has no object"));
    }

    var actor = null,
        object = null;

    Step (
        function() {
            ActivityObject.ensureObject(act.actor, this);
        },
        function(err, result) {
            if (err) throw err;
            actor = result;
            ActivityObject.ensureObject(act.object, this);
        },
        function(err, result) {
            if (err) {
                callback(err);
                return;
            }
            object = result;
            // slim them down to references
            // FIXME: probably shouldn't overwrite attributes
            act.actor = {objectType: actor.objectType,
                         id: actor.id};
            act.object = {objectType: object.objectType,
                          id: object.id};
            callback(null);
        }
    );
};

// When get()'ing an activity, also get the actor and the object,
// which are saved by reference

Activity.prototype.afterGet = function(callback) {
    this.expand(callback);
};

Activity.prototype.expand = function(callback) {
    var act = this;

    Step(
        function() {
            act.expandActor(this.parallel());
            act.expandObject(this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, act);
            }
        }
    );
};

Activity.prototype.expandObject = function(callback) {
    var act = this;

    ActivityObject.getObject(this.object.objectType, this.object.id, function(err, object) {
        if (err) {
            callback(err, null);
        } else {
            // XXX: Check for the old object!
            act.object = object;
            if (act.verb == "post") {
                delete act.object.author;
            }
            callback(null, act);
        }
    });
};

Activity.prototype.expandActor = function(callback) {
    var act = this;

    ActivityObject.getObject(this.actor.objectType, this.actor.id, function(err, actor) {
        if (err) {
            callback(err, null);
        } else {
            // XXX: Check for the old actor!
            act.actor = actor;
            callback(null, this);
        }
    });
};

exports.Activity = Activity;
<|endoftext|>


<commit_before>// activity.js
//
// data object representing an activity
//
// Copyright 2011,2012 StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var databank = require("databank"),
    Step = require("step"),
    _ = require("underscore"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    ActivityObject = require("./activityobject").ActivityObject,
    DatabankObject = databank.DatabankObject;

var Activity = DatabankObject.subClass("activity");

Activity.schema = { pkey: "id",
                    fields: ["actor",
                             "content",
                             "generator",
                             "icon",
                             "id",
                             "object",
                             "published",
                             "provider",
                             "target",
                             "title",
                             "url",
                             "uuid",
                             "updated",
                             "verb"],
                    indices: ["actor.id", "object.id", "uuid"] };

Activity.init = function(inst, properties) {

    DatabankObject.init(inst, properties);

    if (!this.verb) {
        this.verb = "post";
    }

    if (inst.actor) {
        inst.actor = ActivityObject.toObject(inst.actor, ActivityObject.PERSON);
    }

    if (inst.object) {
        inst.object = ActivityObject.toObject(inst.object);
    }
};

Activity.prototype.apply = function(defaultActor, callback) {

    var act = this,
        User = require("./user").User,
        user;

    // Ensure an actor

    this.actor = this.actor || defaultActor;

    // XXX: Polymorphism is probably the right thing here
    // but I kinda CBA. How's this: rewrite when we get over 5 case's...?

    switch (this.verb) {
    case Activity.POST:
        // Force author data
        this.object.author = this.actor;
        // Is this it...?
        ActivityObject.createObject(this.object, function(err, result) {
            callback(err, result);
        });
        break;
    case Activity.FOLLOW:
        if (!this.actor.id || !this.object.id) {
            callback(new Error("No ID."));
        }

        ActivityObject.ensureObject(act.object, function(err, followed) {
            User.fromPerson(act.actor.id, function(err, user) {
                if (err) {
                    callback(err, null);
                } else {
                    user.follow(act.object.id, callback);
                }
            });
        });
        break;
    case Activity.STOP_FOLLOWING:
        // XXX: OStatus if necessary
        ActivityObject.ensureObject(act.object, function(err, followed) {
            User.fromPerson(act.actor.id, function(err, user) {
                if (err) {
                    callback(err, null);
                } else {
                    user.stopFollowing(act.object.id, callback);
                }
            });
        });
        break;
    case Activity.FAVORITE: // synonyms
    case Activity.LIKE:
        // XXX: Should we record favorite data for
        // remote users?
        Step(
            function() {
                User.fromPerson(act.actor.id, this);
            },
            function(err, results) {
                if (err) throw err;
                user = results;
                ActivityObject.ensureObject(act.object, this);
            },
            function(err, tofavor) {
                if (err) {
                    callback(err, null);
                } else {
                    user.favorite(tofavor.id, tofavor.objectType, callback);
                }
            }
        );
        break;
    case Activity.UNFAVORITE:
    case Activity.UNLIKE:
        // XXX: Should we record favorite data for
        // remote users?
        Step(
            function() {
                User.fromPerson(act.actor.id, this);
            },
            function(err, results) {
                if (err) throw err;
                user = results;
                ActivityObject.ensureObject(act.object, this);
            },
            function(err, tounfavor) {
                if (err) {
                    callback(err, null);
                } else {
                    user.unfavorite(tounfavor.id, tounfavor.objectType, callback);
                }
            }
        );
        break;
    case Activity.DELETE:
        Step(
            function() {
                ActivityObject.getObject(act.object.objectType, act.object.id, this);
            },
            function(err, toDelete) {
                if (err) throw err;
                if (toDelete.author.id !== act.actor.id) {
                    throw new Error("Can't delete " + toDelete.id + ": not author.");
                }
                toDelete.efface(this);
            },
            function(err, ts) {
                if (err) {
                    callback(err);
                } else {
                    callback(null);
                }
            }
        );
        break;
    case Activity.UPDATE:
        Step(
            function() {
                ActivityObject.getObject(act.object.objectType, act.object.id, this);
            },
            function(err, toUpdate) {
                if (err) throw err;
                if (toUpdate.author.id !== act.actor.id) {
                    throw new Error("Can't update " + toUpdate.id + ": not author.");
                }
                toUpdate.update(act.object, this);
            },
            function(err, result) {
                if (err) {
                    callback(err);
                } else {
                    act.object = result;
                    callback(null);
                }
            }
        );
        break;
    case Activity.ADD:
        Step(
            function() {
                ActivityObject.ensureObject(act.object, this.parallel());
                ActivityObject.getObject(act.target.objectType, act.target.id, this.parallel());
            },
            function(err, toAdd, target) {
                if (err) throw err;
                if (target.author.id !== act.actor.id) {
                    throw new Error("Can't add to " + target.id + ": not author.");
                }
                if (target.objectType !== "collection") {
                    throw new Error("Can't add to " + target.id + ": not a collection.");
                }
                if (!_(target).has('objectTypes') || !_(target.objectTypes).isArray() ||
                    target.objectTypes.indexOf(toAdd.objectType) === -1) {
                    throw new Error("Can't add to " + target.id + ": incorrect type.");
                }
                target.getStream(this);
            },
            function(err, stream) {
                if (err) throw err;
                stream.deliverObject({id: act.object.id, objectType: act.object.objectType}, this);
            },
            function(err) {
                if (err) {
                    callback(err);
                } else {
                    callback(null);
                }
            }
        );
        break;
    case Activity.REMOVE:
        Step(
            function() {
                ActivityObject.ensureObject(act.object, this.parallel());
                ActivityObject.getObject(act.target.objectType, act.target.id, this.parallel());
            },
            function(err, toAdd, target) {
                if (err) throw err;
                if (target.author.id !== act.actor.id) {
                    throw new Error("Can't remove from " + target.id + ": not author.");
                }
                if (target.objectType !== "collection") {
                    throw new Error("Can't remove from " + target.id + ": not a collection.");
                }
                if (!_(target).has('objectTypes') || !_(target.objectTypes).isArray() ||
                    target.objectTypes.indexOf(toAdd.objectType) === -1) {
                    throw new Error("Can't remove from " + target.id + ": incorrect type.");
                }
                target.getStream(this);
            },
            function(err, stream) {
                if (err) throw err;
                stream.removeObject({id: act.object.id, objectType: act.object.objectType}, this);
            },
            function(err) {
                if (err) {
                    callback(err);
                } else {
                    callback(null);
                }
            }
        );
        break;
    default:
        // XXX: fave/unfave, join/leave, ...?
        callback(null);
        break;
    }
};

// XXX: identical to save

Activity.beforeCreate = function(props, callback) {

    var now = Stamper.stamp();

    props.updated = now;

    if (!props.published) {
        props.published = now;
    }

    if (!props.id) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI("activity", props.uuid);
        props.links = {
            self: URLMaker.makeURL("api/activity/" + props.uuid)
        };
        // FIXME: assumes person data was set and that it's a local actor
        props.url  = URLMaker.makeURL(props.actor.preferredUsername + "/activity/" + props.uuid);

        // default verb

        if (!props.verb) {
            props.verb = "post";
        }
    }

    if (!props.actor) {
        callback(new Error("Activity has no actor"), null);
    }

    if (!props.object) {
        callback(new Error("Activity has no object"), null);
    }

    Step(
        function() {
            ActivityObject.compressProperty(props, "actor", this.parallel());
            ActivityObject.compressProperty(props, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

Activity.prototype.beforeUpdate = function(props, callback) {

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            ActivityObject.compressProperty(props, "actor", this.parallel());
            ActivityObject.compressProperty(props, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// When save()'ing an activity, ensure the actor and object
// are persisted, then save them by reference.

Activity.prototype.beforeSave = function(callback) {

    var now = Stamper.stamp(),
        act = this;

    act.updated = now;

    if (!act.published) {
        act.published = now;
    }

    if (!act.id) {
        act.uuid = IDMaker.makeID();
        act.id   = ActivityObject.makeURI("activity", act.uuid);
        act.links = {};
        act.links.self = URLMaker.makeURL("api/activity/" + act.uuid);
        // FIXME: assumes person data was set and that it's a local actor
        act.url  = URLMaker.makeURL(act.actor.preferredUsername + "/activity/" + act.uuid);
    }

    if (!act.actor) {
        callback(new Error("Activity has no actor"));
        return;
    }

    if (!act.object) {
        callback(new Error("Activity has no object"));
        return;
    }

    Step(
        function() {
            ActivityObject.compressProperty(act, "actor", this.parallel());
            ActivityObject.compressProperty(act, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

// When get()'ing an activity, also get the actor and the object,
// which are saved by reference

Activity.prototype.afterCreate =
Activity.prototype.afterSave =
Activity.prototype.afterUpdate =
Activity.prototype.afterGet = function(callback) {
    this.expand(callback);
};

Activity.prototype.expand = function(callback) {
    var act = this;

    Step(
        function() {
            ActivityObject.expandProperty(act, "actor", this.parallel());
            ActivityObject.expandProperty(act, "object", this.parallel());
        },
        function(err) {
            if (err) throw err;
            act.object.expandFeeds(this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                // Implied
                if (act.verb === "post" && _(act.object).has("author")) {
                    delete act.object.author;
                }
                callback(null);
            }
        }
    );
};

Activity.prototype.compress = function(callback) {
    var act = this;
    Step(
        function() {
            ActivityObject.compressProperty(act, "actor", this.parallel());
            ActivityObject.compressProperty(act, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

Activity.prototype.efface = function(callback) {

    var keepers = ["actor", "object", "uuid", "id", "published", "deleted", "updated"],
        prop,
        obj = this;

    for (prop in obj) {
        if (obj.hasOwnProperty(prop) && keepers.indexOf(prop) === -1) {
            delete obj[prop];
        }
    }

    var now = Stamper.stamp();

    obj.deleted = obj.updated = now;

    obj.save(callback);
};

Activity.verbs = ["accept",
                  "access",
                  "acknowledge",
                  "add",
                  "agree",
                  "append",
                  "approve",
                  "archive",
                  "assign",
                  "at",
                  "attach",
                  "attend",
                  "author",
                  "authorize",
                  "borrow",
                  "build",
                  "cancel",
                  "close",
                  "complete",
                  "confirm",
                  "consume",
                  "checkin",
                  "close",
                  "create",
                  "delete",
                  "deliver",
                  "deny",
                  "disagree",
                  "dislike",
                  "experience",
                  "favorite",
                  "find",
                  "follow",
                  "give",
                  "host",
                  "ignore",
                  "insert",
                  "install",
                  "interact",
                  "invite",
                  "join",
                  "leave",
                  "like",
                  "listen",
                  "lose",
                  "make-friend",
                  "open",
                  "play",
                  "post",
                  "present",
                  "purchase",
                  "qualify",
                  "read",
                  "receive",
                  "reject",
                  "remove",
                  "remove-friend",
                  "replace",
                  "request",
                  "request-friend",
                  "resolve",
                  "return",
                  "retract",
                  "rsvp-maybe",
                  "rsvp-no",
                  "rsvp-yes",
                  "satisfy",
                  "save",
                  "schedule",
                  "search",
                  "sell",
                  "send",
                  "share",
                  "sponsor",
                  "start",
                  "stop-following",
                  "submit",
                  "tag",
                  "terminate",
                  "tie",
                  "unfavorite",
                  "unlike",
                  "unsatisfy",
                  "unsave",
                  "unshare",
                  "update",
                  "use",
                  "watch",
                  "win"];

var i = 0, verb;

// Constants-like members for activity verbs

for (i = 0; i < Activity.verbs.length; i++) {
    verb = Activity.verbs[i];
    Activity[verb.toUpperCase().replace("-", "_")] = verb;
}

exports.Activity = Activity;

<commit_msg>new AppError for model errors<commit_after>// activity.js
//
// data object representing an activity
//
// Copyright 2011,2012 StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var databank = require("databank"),
    Step = require("step"),
    _ = require("underscore"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    ActivityObject = require("./activityobject").ActivityObject,
    DatabankObject = databank.DatabankObject;

var AppError = function(msg) {
    Error.captureStackTrace(this, AppError);
    this.name = 'AppError';
    this.message = msg;
};

AppError.prototype = new Error();
AppError.prototype.constructor = AppError;

var Activity = DatabankObject.subClass("activity");

Activity.schema = { pkey: "id",
                    fields: ["actor",
                             "content",
                             "generator",
                             "icon",
                             "id",
                             "object",
                             "published",
                             "provider",
                             "target",
                             "title",
                             "url",
                             "uuid",
                             "updated",
                             "verb"],
                    indices: ["actor.id", "object.id", "uuid"] };

Activity.init = function(inst, properties) {

    DatabankObject.init(inst, properties);

    if (!this.verb) {
        this.verb = "post";
    }

    if (inst.actor) {
        inst.actor = ActivityObject.toObject(inst.actor, ActivityObject.PERSON);
    }

    if (inst.object) {
        inst.object = ActivityObject.toObject(inst.object);
    }
};

Activity.prototype.apply = function(defaultActor, callback) {

    var act = this,
        User = require("./user").User,
        user;

    // Ensure an actor

    this.actor = this.actor || defaultActor;

    // XXX: Polymorphism is probably the right thing here
    // but I kinda CBA. How's this: rewrite when we get over 5 case's...?

    switch (this.verb) {
    case Activity.POST:
        // Force author data
        this.object.author = this.actor;
        // Is this it...?
        ActivityObject.createObject(this.object, function(err, result) {
            callback(err, result);
        });
        break;
    case Activity.FOLLOW:
        if (!this.actor.id || !this.object.id) {
            callback(new AppError("No ID."));
        }

        ActivityObject.ensureObject(act.object, function(err, followed) {
            User.fromPerson(act.actor.id, function(err, user) {
                if (err) {
                    callback(err, null);
                } else {
                    user.follow(act.object.id, callback);
                }
            });
        });
        break;
    case Activity.STOP_FOLLOWING:
        // XXX: OStatus if necessary
        ActivityObject.ensureObject(act.object, function(err, followed) {
            User.fromPerson(act.actor.id, function(err, user) {
                if (err) {
                    callback(err, null);
                } else {
                    user.stopFollowing(act.object.id, callback);
                }
            });
        });
        break;
    case Activity.FAVORITE: // synonyms
    case Activity.LIKE:
        // XXX: Should we record favorite data for
        // remote users?
        Step(
            function() {
                User.fromPerson(act.actor.id, this);
            },
            function(err, results) {
                if (err) throw err;
                user = results;
                ActivityObject.ensureObject(act.object, this);
            },
            function(err, tofavor) {
                if (err) {
                    callback(err, null);
                } else {
                    user.favorite(tofavor.id, tofavor.objectType, callback);
                }
            }
        );
        break;
    case Activity.UNFAVORITE:
    case Activity.UNLIKE:
        // XXX: Should we record favorite data for
        // remote users?
        Step(
            function() {
                User.fromPerson(act.actor.id, this);
            },
            function(err, results) {
                if (err) throw err;
                user = results;
                ActivityObject.ensureObject(act.object, this);
            },
            function(err, tounfavor) {
                if (err) {
                    callback(err, null);
                } else {
                    user.unfavorite(tounfavor.id, tounfavor.objectType, callback);
                }
            }
        );
        break;
    case Activity.DELETE:
        Step(
            function() {
                ActivityObject.getObject(act.object.objectType, act.object.id, this);
            },
            function(err, toDelete) {
                if (err) throw err;
                if (toDelete.author.id !== act.actor.id) {
                    throw new AppError("Can't delete " + toDelete.id + ": not author.");
                }
                toDelete.efface(this);
            },
            function(err, ts) {
                if (err) {
                    callback(err);
                } else {
                    callback(null);
                }
            }
        );
        break;
    case Activity.UPDATE:
        Step(
            function() {
                ActivityObject.getObject(act.object.objectType, act.object.id, this);
            },
            function(err, toUpdate) {
                if (err) throw err;
                if (toUpdate.author.id !== act.actor.id) {
                    throw new AppError("Can't update " + toUpdate.id + ": not author.");
                }
                toUpdate.update(act.object, this);
            },
            function(err, result) {
                if (err) {
                    callback(err);
                } else {
                    act.object = result;
                    callback(null);
                }
            }
        );
        break;
    case Activity.ADD:
        Step(
            function() {
                ActivityObject.ensureObject(act.object, this.parallel());
                ActivityObject.getObject(act.target.objectType, act.target.id, this.parallel());
            },
            function(err, toAdd, target) {
                if (err) throw err;
                if (target.author.id !== act.actor.id) {
                    throw new AppError("Can't add to " + target.id + ": not author.");
                }
                if (target.objectType !== "collection") {
                    throw new AppError("Can't add to " + target.id + ": not a collection.");
                }
                if (!_(target).has('objectTypes') || !_(target.objectTypes).isArray() ||
                    target.objectTypes.indexOf(toAdd.objectType) === -1) {
                    throw new AppError("Can't add to " + target.id + ": incorrect type.");
                }
                target.getStream(this);
            },
            function(err, stream) {
                if (err) throw err;
                stream.deliverObject({id: act.object.id, objectType: act.object.objectType}, this);
            },
            function(err) {
                if (err) {
                    callback(err);
                } else {
                    callback(null);
                }
            }
        );
        break;
    case Activity.REMOVE:
        Step(
            function() {
                ActivityObject.ensureObject(act.object, this.parallel());
                ActivityObject.getObject(act.target.objectType, act.target.id, this.parallel());
            },
            function(err, toAdd, target) {
                if (err) throw err;
                if (target.author.id !== act.actor.id) {
                    throw new AppError("Can't remove from " + target.id + ": not author.");
                }
                if (target.objectType !== "collection") {
                    throw new AppError("Can't remove from " + target.id + ": not a collection.");
                }
                if (!_(target).has('objectTypes') || !_(target.objectTypes).isArray() ||
                    target.objectTypes.indexOf(toAdd.objectType) === -1) {
                    throw new AppError("Can't remove from " + target.id + ": incorrect type.");
                }
                target.getStream(this);
            },
            function(err, stream) {
                if (err) throw err;
                stream.removeObject({id: act.object.id, objectType: act.object.objectType}, this);
            },
            function(err) {
                if (err) {
                    callback(err);
                } else {
                    callback(null);
                }
            }
        );
        break;
    default:
        // XXX: fave/unfave, join/leave, ...?
        callback(null);
        break;
    }
};

// XXX: identical to save

Activity.beforeCreate = function(props, callback) {

    var now = Stamper.stamp();

    props.updated = now;

    if (!props.published) {
        props.published = now;
    }

    if (!props.id) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI("activity", props.uuid);
        props.links = {
            self: URLMaker.makeURL("api/activity/" + props.uuid)
        };
        // FIXME: assumes person data was set and that it's a local actor
        props.url  = URLMaker.makeURL(props.actor.preferredUsername + "/activity/" + props.uuid);

        // default verb

        if (!props.verb) {
            props.verb = "post";
        }
    }

    if (!props.actor) {
        callback(new Error("Activity has no actor"), null);
    }

    if (!props.object) {
        callback(new Error("Activity has no object"), null);
    }

    Step(
        function() {
            ActivityObject.compressProperty(props, "actor", this.parallel());
            ActivityObject.compressProperty(props, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

Activity.prototype.beforeUpdate = function(props, callback) {

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            ActivityObject.compressProperty(props, "actor", this.parallel());
            ActivityObject.compressProperty(props, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// When save()'ing an activity, ensure the actor and object
// are persisted, then save them by reference.

Activity.prototype.beforeSave = function(callback) {

    var now = Stamper.stamp(),
        act = this;

    act.updated = now;

    if (!act.published) {
        act.published = now;
    }

    if (!act.id) {
        act.uuid = IDMaker.makeID();
        act.id   = ActivityObject.makeURI("activity", act.uuid);
        act.links = {};
        act.links.self = URLMaker.makeURL("api/activity/" + act.uuid);
        // FIXME: assumes person data was set and that it's a local actor
        act.url  = URLMaker.makeURL(act.actor.preferredUsername + "/activity/" + act.uuid);
    }

    if (!act.actor) {
        callback(new Error("Activity has no actor"));
        return;
    }

    if (!act.object) {
        callback(new Error("Activity has no object"));
        return;
    }

    Step(
        function() {
            ActivityObject.compressProperty(act, "actor", this.parallel());
            ActivityObject.compressProperty(act, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

// When get()'ing an activity, also get the actor and the object,
// which are saved by reference

Activity.prototype.afterCreate =
Activity.prototype.afterSave =
Activity.prototype.afterUpdate =
Activity.prototype.afterGet = function(callback) {
    this.expand(callback);
};

Activity.prototype.expand = function(callback) {
    var act = this;

    Step(
        function() {
            ActivityObject.expandProperty(act, "actor", this.parallel());
            ActivityObject.expandProperty(act, "object", this.parallel());
        },
        function(err) {
            if (err) throw err;
            act.object.expandFeeds(this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                // Implied
                if (act.verb === "post" && _(act.object).has("author")) {
                    delete act.object.author;
                }
                callback(null);
            }
        }
    );
};

Activity.prototype.compress = function(callback) {
    var act = this;
    Step(
        function() {
            ActivityObject.compressProperty(act, "actor", this.parallel());
            ActivityObject.compressProperty(act, "object", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

Activity.prototype.efface = function(callback) {

    var keepers = ["actor", "object", "uuid", "id", "published", "deleted", "updated"],
        prop,
        obj = this;

    for (prop in obj) {
        if (obj.hasOwnProperty(prop) && keepers.indexOf(prop) === -1) {
            delete obj[prop];
        }
    }

    var now = Stamper.stamp();

    obj.deleted = obj.updated = now;

    obj.save(callback);
};

Activity.verbs = ["accept",
                  "access",
                  "acknowledge",
                  "add",
                  "agree",
                  "append",
                  "approve",
                  "archive",
                  "assign",
                  "at",
                  "attach",
                  "attend",
                  "author",
                  "authorize",
                  "borrow",
                  "build",
                  "cancel",
                  "close",
                  "complete",
                  "confirm",
                  "consume",
                  "checkin",
                  "close",
                  "create",
                  "delete",
                  "deliver",
                  "deny",
                  "disagree",
                  "dislike",
                  "experience",
                  "favorite",
                  "find",
                  "follow",
                  "give",
                  "host",
                  "ignore",
                  "insert",
                  "install",
                  "interact",
                  "invite",
                  "join",
                  "leave",
                  "like",
                  "listen",
                  "lose",
                  "make-friend",
                  "open",
                  "play",
                  "post",
                  "present",
                  "purchase",
                  "qualify",
                  "read",
                  "receive",
                  "reject",
                  "remove",
                  "remove-friend",
                  "replace",
                  "request",
                  "request-friend",
                  "resolve",
                  "return",
                  "retract",
                  "rsvp-maybe",
                  "rsvp-no",
                  "rsvp-yes",
                  "satisfy",
                  "save",
                  "schedule",
                  "search",
                  "sell",
                  "send",
                  "share",
                  "sponsor",
                  "start",
                  "stop-following",
                  "submit",
                  "tag",
                  "terminate",
                  "tie",
                  "unfavorite",
                  "unlike",
                  "unsatisfy",
                  "unsave",
                  "unshare",
                  "update",
                  "use",
                  "watch",
                  "win"];

var i = 0, verb;

// Constants-like members for activity verbs

for (i = 0; i < Activity.verbs.length; i++) {
    verb = Activity.verbs[i];
    Activity[verb.toUpperCase().replace("-", "_")] = verb;
}

exports.Activity = Activity;
exports.AppError = AppError;

<|endoftext|>


<commit_before>'use strict';

var fs          = require('fs');
var path        = require('path');
var Promise     = require('../ext/promise');
var rimraf      = Promise.denodeify(require('rimraf'));
var ncp         = Promise.denodeify(require('ncp'));
var Task        = require('./task');
var SilentError = require('../errors/silent');

var signalsTrapped = false;

module.exports = Task.extend({
  setupBroccoliBuilder: function() {
    this.environment = this.environment || 'development';
    process.env.EMBER_ENV = process.env.EMBER_ENV || this.environment;

    var broccoli = require('broccoli');
    this.tree    = broccoli.loadBrocfile();
    this.builder = new broccoli.Builder(this.tree);
  },

  trapSignals: function() {
    if (!signalsTrapped) {
      process.on('SIGINT',  this.onSIGINT.bind(this));
      process.on('SIGTERM', this.onSIGTERM.bind(this));
      signalsTrapped = true;
    }
  },

  cleanupOnExit: function() {
    process.addListener('exit', this.onExit.bind(this));
  },

  init: function() {
    this.setupBroccoliBuilder();
    this.cleanupOnExit();
    this.trapSignals();

    this.mkdirp = require('mkdirp');
  },

  canDeleteOutputPath: function(outputPath) {
    return !this.project.root.contains(outputPath);
  },

  /**
    This is used to ensure that the output path is emptied, but not deleted
    itself. If we simply used `rimraf(this.outputPath)`, any symlinks would
    now be broken. This iterates the direct children of the output path,
    and calls `rimraf` on each (this preserving any symlinks).
  */
  clearOutputPath: function() {
    var outputPath = this.outputPath;
    if (!fs.existsSync(outputPath)) { return Promise.resolve();}

    if(!this.canDeleteOutputPath(outputPath)) {
      return Promise.reject(new SilentError('Using a build destination path of `' + outputPath + '` is not supported.'));
    }

    var promises = [];
    var entries = fs.readdirSync(outputPath);

    for (var i = 0, l = entries.length; i < l; i++) {
      promises.push(rimraf(path.join(outputPath, entries[i])));
    }

    return Promise.all(promises);
  },

  copyToOutputPath: function(inputPath) {
    if (!fs.existsSync(this.outputPath)) {
      this.mkdirp.sync(this.outputPath);
    }

    return ncp(inputPath, this.outputPath, {
      dereference: true,
      clobber: true,
      stopOnErr: true
    });
  },

  processBuildResult: function(results) {
    var self = this;

    return this.clearOutputPath()
      .then(function() {
        return self.copyToOutputPath(results.directory);
      })
      .then(function() {
        return results;
      });
  },

  addonsPostBuild: function(results){
    var addonPromises = [];

    if(this.project && this.project.addons.length) {
      addonPromises = this.project.addons.map(function(addon){
        if(addon.postBuild) {
          return addon.postBuild(results);
        }
      }).filter(Boolean);
    }

    return Promise.all(addonPromises).then(function() {
      return results;
    });
  },

  build: function() {
    return this.builder.build.apply(this.builder, arguments)
      .then(this.processBuildResult.bind(this))
      .then(this.addonsPostBuild.bind(this));
  },

  onExit: function() {
    this.builder.cleanup();
  },

  onSIGINT: function() {
    process.exit(1);
  },
  onSIGTERM: function() {
    process.exit(1);
  }
});
<commit_msg>Limit concurrent number of copy operations.<commit_after>'use strict';

var fs          = require('fs');
var path        = require('path');
var Promise     = require('../ext/promise');
var rimraf      = Promise.denodeify(require('rimraf'));
var ncp         = Promise.denodeify(require('ncp'));
var Task        = require('./task');
var SilentError = require('../errors/silent');

var signalsTrapped = false;

module.exports = Task.extend({
  setupBroccoliBuilder: function() {
    this.environment = this.environment || 'development';
    process.env.EMBER_ENV = process.env.EMBER_ENV || this.environment;

    var broccoli = require('broccoli');
    this.tree    = broccoli.loadBrocfile();
    this.builder = new broccoli.Builder(this.tree);
  },

  trapSignals: function() {
    if (!signalsTrapped) {
      process.on('SIGINT',  this.onSIGINT.bind(this));
      process.on('SIGTERM', this.onSIGTERM.bind(this));
      signalsTrapped = true;
    }
  },

  cleanupOnExit: function() {
    process.addListener('exit', this.onExit.bind(this));
  },

  init: function() {
    this.setupBroccoliBuilder();
    this.cleanupOnExit();
    this.trapSignals();

    this.mkdirp = require('mkdirp');
  },

  canDeleteOutputPath: function(outputPath) {
    return !this.project.root.contains(outputPath);
  },

  /**
    This is used to ensure that the output path is emptied, but not deleted
    itself. If we simply used `rimraf(this.outputPath)`, any symlinks would
    now be broken. This iterates the direct children of the output path,
    and calls `rimraf` on each (this preserving any symlinks).
  */
  clearOutputPath: function() {
    var outputPath = this.outputPath;
    if (!fs.existsSync(outputPath)) { return Promise.resolve();}

    if(!this.canDeleteOutputPath(outputPath)) {
      return Promise.reject(new SilentError('Using a build destination path of `' + outputPath + '` is not supported.'));
    }

    var promises = [];
    var entries = fs.readdirSync(outputPath);

    for (var i = 0, l = entries.length; i < l; i++) {
      promises.push(rimraf(path.join(outputPath, entries[i])));
    }

    return Promise.all(promises);
  },

  copyToOutputPath: function(inputPath) {
    if (!fs.existsSync(this.outputPath)) {
      this.mkdirp.sync(this.outputPath);
    }

    return ncp(inputPath, this.outputPath, {
      dereference: true,
      clobber: true,
      stopOnErr: true,
      limit: 2
    });
  },

  processBuildResult: function(results) {
    var self = this;

    return this.clearOutputPath()
      .then(function() {
        return self.copyToOutputPath(results.directory);
      })
      .then(function() {
        return results;
      });
  },

  addonsPostBuild: function(results){
    var addonPromises = [];

    if(this.project && this.project.addons.length) {
      addonPromises = this.project.addons.map(function(addon){
        if(addon.postBuild) {
          return addon.postBuild(results);
        }
      }).filter(Boolean);
    }

    return Promise.all(addonPromises).then(function() {
      return results;
    });
  },

  build: function() {
    return this.builder.build.apply(this.builder, arguments)
      .then(this.processBuildResult.bind(this))
      .then(this.addonsPostBuild.bind(this));
  },

  onExit: function() {
    this.builder.cleanup();
  },

  onSIGINT: function() {
    process.exit(1);
  },
  onSIGTERM: function() {
    process.exit(1);
  }
});
<|endoftext|>


<commit_before>const uuid = require('uuid/v4');
const epochTime = require('../helpers/epoch_time');
const assert = require('assert');
const instance = require('../helpers/weak_cache');

module.exports = function getSession(provider) {
  let adapter;

  function getAdapter() {
    if (!adapter) adapter = new (instance(provider).Adapter)('Session');
    return adapter;
  }

  function authorizationFor(clientId) {
    this.authorizations = this.authorizations || {};
    const key = String(clientId);
    if (!this.authorizations[key]) {
      this.authorizations[key] = {};
    }

    return this.authorizations[key];
  }

  class Session {
    constructor(id, data) {
      if (data) Object.assign(this, data);
      this.id = id;
    }

    accountId() {
      return this.account;
    }

    authTime() {
      return this.loginTs;
    }

    past(age) {
      const maxAge = +age;

      if (this.loginTs && Number.isFinite(maxAge) && maxAge > 0) {
        return epochTime() - this.loginTs > maxAge;
      }

      return false;
    }

    save(ttl = instance(provider).configuration('cookies.long.maxAge') / 1000) {
      const payload = { ...this, ...{ exp: epochTime() + ttl } };
      delete payload.id;

      return getAdapter().upsert(this.id, payload, ttl);
    }

    destroy() {
      return getAdapter().destroy(this.id);
    }

    sidFor(clientId, value) {
      const authorization = authorizationFor.call(this, clientId);

      if (value) {
        authorization.sid = value;
        return undefined;
      }

      return authorization.sid;
    }

    metaFor(clientId, value) {
      const authorization = authorizationFor.call(this, clientId);

      if (value) {
        authorization.meta = value;
        return undefined;
      }

      return authorization.meta;
    }

    static async find(id) {
      assert(id, 'id must be provided to Session#find');
      const data = await getAdapter().find(id);
      if (data && data.exp && data.exp > epochTime()) {
        return new Session(id, data);
      }
      return new Session(id);
    }

    static async get(ctx) {
      // is there supposed to be a session bound? generate if not
      const sessionId = ctx.cookies.get(provider.cookieName('session'), {
        signed: instance(provider).configuration('cookies.long.signed'),
      }) || uuid();

      const session = await this.find(sessionId);

      // refresh the session duration
      ctx.cookies.set(provider.cookieName('session'), sessionId, instance(provider).configuration('cookies.long'));
      return session;
    }
  }

  return Session;
};
<commit_msg>force drop expired sessions<commit_after>const uuid = require('uuid/v4');
const epochTime = require('../helpers/epoch_time');
const assert = require('assert');
const instance = require('../helpers/weak_cache');

module.exports = function getSession(provider) {
  let adapter;

  function getAdapter() {
    if (!adapter) adapter = new (instance(provider).Adapter)('Session');
    return adapter;
  }

  function authorizationFor(clientId) {
    this.authorizations = this.authorizations || {};
    const key = String(clientId);
    if (!this.authorizations[key]) {
      this.authorizations[key] = {};
    }

    return this.authorizations[key];
  }

  class Session {
    constructor(id, data) {
      if (data) Object.assign(this, data);
      this.id = id;
    }

    accountId() {
      return this.account;
    }

    authTime() {
      return this.loginTs;
    }

    past(age) {
      const maxAge = +age;

      if (this.loginTs && Number.isFinite(maxAge) && maxAge > 0) {
        return epochTime() - this.loginTs > maxAge;
      }

      return false;
    }

    save(ttl = instance(provider).configuration('cookies.long.maxAge') / 1000) {
      const payload = { ...this, ...{ exp: epochTime() + ttl } };
      delete payload.id;

      return getAdapter().upsert(this.id, payload, ttl);
    }

    destroy() {
      return getAdapter().destroy(this.id);
    }

    sidFor(clientId, value) {
      const authorization = authorizationFor.call(this, clientId);

      if (value) {
        authorization.sid = value;
        return undefined;
      }

      return authorization.sid;
    }

    metaFor(clientId, value) {
      const authorization = authorizationFor.call(this, clientId);

      if (value) {
        authorization.meta = value;
        return undefined;
      }

      return authorization.meta;
    }

    static async find(id) {
      assert(id, 'id must be provided to Session#find');
      const data = await getAdapter().find(id);
      if (data) {
        if (data.exp && data.exp < epochTime()) {
          await getAdapter().destroy(id);
        } else {
          return new Session(id, data);
        }
      }
      return new Session(id);
    }

    static async get(ctx) {
      // is there supposed to be a session bound? generate if not
      const sessionId = ctx.cookies.get(provider.cookieName('session'), {
        signed: instance(provider).configuration('cookies.long.signed'),
      }) || uuid();

      const session = await this.find(sessionId);

      // refresh the session duration
      ctx.cookies.set(provider.cookieName('session'), sessionId, instance(provider).configuration('cookies.long'));
      return session;
    }
  }

  return Session;
};
<|endoftext|>


<commit_before>'use strict';
var path = require('path');
var micromatch = require('micromatch');

// Skip external modules. Based on http://git.io/pzPO.
var internalModuleRegexp = process.platform === 'win32' ?
  /^(\.|\w:)/ :
  /^[\/.]/;

module.exports = {
  internalOnly: internalModuleRegexp.test.bind(internalModuleRegexp),

  /**
   * Create a filter function for use with module-deps, allowing the specified
   * external modules through.
   *
   * @param {Array<String>} indexes - the list of entry points that will be
   * used by module-deps
   * @param options - An options object with `external` being a
   * micromatch-compaitible glob. *NOTE:* the glob will be matched relative to
   * the top-level node_modules directory for each entry point.
   * @return {function} - A function for use as the module-deps `postFilter`
   * options.
   */
  externals: function externalModuleFilter(indexes, options) {
    var externalFilters = false;
    if (options.external) {
      var test = micromatch.matcher(options.external);
      externalFilters = indexes.map(function (index) {
        // grab the path of the top-level node_modules directory.
        var topNodeModules = path.join(path.dirname(index), 'node_modules');
        return function matchGlob(file, pkg) {
          // if a module is not found, don't include it.
          if (!file || !pkg) return false;
          // if package.json specifies a 'main' script, strip that path off
          // the file to get the module's directory.
          // otherwise, just use the dirname of the file.
          if (pkg.main) {
            file = file.slice(0, -path.normalize(pkg.main).length);
          } else {
            file = path.dirname(file);
          }
          // test the path relative to the top node_modules dir.
          var p = path.relative(topNodeModules, file);
          return test(p);
        };
      });
    }

    return function (id, file, pkg) {
      var internal = internalModuleRegexp.test(id);
      return internal || (externalFilters &&
        externalFilters
        .map(function (f) {
          return f(file, pkg);
        })
        .reduce(function (a, b) {
          return a || b;
        }, false));
    };
  }
};

<commit_msg>Let coverage ignore windows-specific code. Refs #102<commit_after>'use strict';
var path = require('path');
var micromatch = require('micromatch');

// Skip external modules. Based on http://git.io/pzPO.
var internalModuleRegexp = process.platform === 'win32' ?
  /* istanbul ignore next */
  /^(\.|\w:)/ :
  /^[\/.]/;

module.exports = {
  internalOnly: internalModuleRegexp.test.bind(internalModuleRegexp),

  /**
   * Create a filter function for use with module-deps, allowing the specified
   * external modules through.
   *
   * @param {Array<String>} indexes - the list of entry points that will be
   * used by module-deps
   * @param options - An options object with `external` being a
   * micromatch-compaitible glob. *NOTE:* the glob will be matched relative to
   * the top-level node_modules directory for each entry point.
   * @return {function} - A function for use as the module-deps `postFilter`
   * options.
   */
  externals: function externalModuleFilter(indexes, options) {
    var externalFilters = false;
    if (options.external) {
      var test = micromatch.matcher(options.external);
      externalFilters = indexes.map(function (index) {
        // grab the path of the top-level node_modules directory.
        var topNodeModules = path.join(path.dirname(index), 'node_modules');
        return function matchGlob(file, pkg) {
          // if a module is not found, don't include it.
          if (!file || !pkg) return false;
          // if package.json specifies a 'main' script, strip that path off
          // the file to get the module's directory.
          // otherwise, just use the dirname of the file.
          if (pkg.main) {
            file = file.slice(0, -path.normalize(pkg.main).length);
          } else {
            file = path.dirname(file);
          }
          // test the path relative to the top node_modules dir.
          var p = path.relative(topNodeModules, file);
          return test(p);
        };
      });
    }

    return function (id, file, pkg) {
      var internal = internalModuleRegexp.test(id);
      return internal || (externalFilters &&
        externalFilters
        .map(function (f) {
          return f(file, pkg);
        })
        .reduce(function (a, b) {
          return a || b;
        }, false));
    };
  }
};

<|endoftext|>


<commit_before>/*
 * provider.js: Abstraction providing an interface into pluggable configuration storage.
 *
 * (C) 2011, Charlie Robbins
 *
 */

var async = require('async'),
    common = require('./common'),
    stores = require('./stores');

//
// ### function Provider (options)
// #### @options {Object} Options for this instance.
// Constructor function for the Provider object responsible
// for exposing the pluggable storage features of `nconf`.
//
var Provider = exports.Provider = function (options) {
  var self = this;

  //
  // Setup default options for working with `stores`,
  // `overrides`, `process.env` and `process.argv`.
  //
  options         = options           || {};
  this._overrides = options.overrides || null;
  this._argv      = options.argv      || false;
  this._env       = options.env       || false;
  this._reserved  = Object.keys(Provider.prototype);
  this._stores    = [];
  this.sources    = [];

  //
  // Add the default `system` store for working with
  // `overrides`, `process.env`, `process.argv` and
  // a simple in-memory objects.
  //
  this.add('system', options);

  //
  // Add any stores passed in through the options
  // to this instance.
  //
  if (options.type) {
    this.add(options.type, options);
  }
  else if (options.store) {
    this.add(options.store.name || options.store.type, options.store);
  }
  else if (options.stores) {
    Object.keys(options.stores).forEach(function (name) {
      var store = options.stores[name];
      self.add(store.name || name || store.type, store);
    });
  }

  //
  // Add any read-only sources to this instance
  //
  if (options.source) {
    this.sources.push(this.create(options.source.type || options.source.name, options.source));
  }
  else if (options.sources) {
    Object.keys(options.sources).forEach(function (name) {
      var source = options.sources[name];
      self.sources.push(self.create(source.type || source.name || name, source));
    });
  }
};

//
// ### function use (name, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Adds (or replaces) a new store with the specified `name`
// and `options`. If `options.type` is not set, then `name`
// will be used instead:
//
//    provider.use('file');
//    provider.use('file', { type: 'file', filename: '/path/to/userconf' })
//
Provider.prototype.use = function (name, options) {
  if (name === 'system') {
    return;
  }
  else if (this._reserved.indexOf(name) !== -1) {
    throw new Error('Cannot use reserved name: ' + name);
  }

  options  = options      || {};
  var type = options.type || name;

  function sameOptions (store) {
    return Object.keys(options).every(function (key) {
      return options[key] === store[key];
    });
  }

  var store = this[name],
      update = store && !sameOptions(store);

  if (!store || update) {
    if (update) {
      this.remove(name);
    }

    this.add(name, options);
  }

  return this;
};

//
// ### function add (name, options)
// #### @name {string} Name of the store to add to this instance
// #### @options {Object} Options for the store to create
// Adds a new store with the specified `name` and `options`. If `options.type`
// is not set, then `name` will be used instead:
//
//    provider.add('memory');
//    provider.add('userconf', { type: 'file', filename: '/path/to/userconf' })
//
Provider.prototype.add = function (name, options) {
  if (this._reserved.indexOf(name) !== -1) {
    throw new Error('Cannot use reserved name: ' + name);
  }

  options  = options      || {};
  var type = options.type || name;

  if (Object.keys(stores).indexOf(common.capitalize(type)) === -1) {
    throw new Error('Cannot add store with unknown type: ' + type);
  }

  this[name] = this.create(type, options);
  this._stores.push(name);

  if (this[name].loadSync) {
    this[name].loadSync();
  }

  return this;
};

//
// ### function remove (name)
// #### @name {string} Name of the store to remove from this instance
// Removes a store with the specified `name` from this instance. Users
// are allowed to pass in a type argument (e.g. `memory`) as name if
// this was used in the call to `.add()`.
//
Provider.prototype.remove = function (name) {
  if (this._reserved.indexOf(name) !== -1) {
    throw new Error('Cannot use reserved name: ' + name);
  }
  else if (!this[name]) {
    throw new Error('Cannot remove store that does not exist: ' + name);
  }

  delete this[name];
  this._stores.splice(this._stores.indexOf(name), 1);
  return this;
};

//
// ### function create (type, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Creates a store of the specified `type` using the
// specified `options`.
//
Provider.prototype.create = function (type, options) {
  return new stores[common.capitalize(type.toLowerCase())](options);
};

//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Provider.prototype.get = function (key, callback) {
  //
  // If there is no callback we can short-circuit into the default
  // logic for traversing stores.
  //
  if (!callback) {
    return this._execute('get', 1, key, callback);
  }

  //
  // Otherwise the asynchronous, hierarchical `get` is
  // slightly more complicated because we do not need to traverse
  // the entire set of stores, but up until there is a defined value.
  //
  var current = 0,
      self = this,
      response;

  async.whilst(function () {
    return typeof response === 'undefined' && current < self._stores.length;
  }, function (next) {
    var store = self[self._stores[current]];
    current++;

    if (store.get.length >= 2) {
      return store.get(key, function (err, value) {
        if (err) {
          return next(err);
        }

        response = value;
        next();
      });
    }

    response = store.get(key);
    next();
  }, function (err) {
    return err ? callback(err) : callback(null, response);
  });
};

//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Provider.prototype.set = function (key, value, callback) {
  return this._execute('set', 2, key, value, callback);
};

//
// ### function reset (callback)
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Provider.prototype.reset = function (callback) {
  return this._execute('reset', 0, callback);
};

//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Provider.prototype.clear = function (key, callback) {
  return this._execute('clear', 1, key, callback);
};

//
// ### function merge ([key,] value [, callback])
// #### @key {string} Key to merge the value into
// #### @value {literal|Object} Value to merge into the key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Merges the properties in `value` into the existing object value at `key`.
//
// 1. If the existing value `key` is not an Object, it will be completely overwritten.
// 2. If `key` is not supplied, then the `value` will be merged into the root.
//
Provider.prototype.merge = function () {
  var self = this,
      args = Array.prototype.slice.call(arguments),
      callback = typeof args[args.length - 1] === 'function' && args.pop(),
      value = args.pop(),
      key = args.pop();

  function mergeProperty (prop, next) {
    return self._execute('merge', 2, prop, value[prop], next);
  }

  if (!key) {
    if (Array.isArray(value) || typeof value !== 'object') {
      return onError(new Error('Cannot merge non-Object into top-level.'), callback);
    }

    return async.forEach(Object.keys(value), mergeProperty, callback || function () { })
  }

  return this._execute('merge', 2, key, value, callback);
};

//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Provider.prototype.load = function (callback) {
  var self = this,
      stores = this._stores.map(function (name) { return self[name] })
                           .concat(this.sources);

  function loadStoreSync(store) {
    if (!store.loadSync) {
      throw new Error('nconf store ' + store.type + ' has no loadSync() method');
    }

    return store.loadSync();
  }

  function loadStore(store, next) {
    if (!store.load && !store.loadSync) {
      return next(new Error('nconf store ' + store.type + ' has no load() method'));
    }

    return store.loadSync
      ? next(null, store.loadSync())
      : store.load(next);
  }

  //
  // If we don't have a callback and the current
  // store is capable of loading synchronously
  // then do so.
  //
  if (!callback) {
    return common.merge(stores.map(loadStoreSync));
  }

  async.map(stores, loadStore, function (err, objs) {
    return err ? callback(err) : callback(null, common.merge(objs));
  });
};

//
// ### function save (value, callback)
// #### @value {Object} **Optional** Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
Provider.prototype.save = function (value, callback) {
  if (!callback && typeof value === 'function') {
    callback = value;
    value = null;
  }

  var self = this;

  function saveStoreSync(name) {
    var store = self[name];

    if (!store.saveSync) {
      throw new Error('nconf store ' + store.type + ' has no saveSync() method');
    }

    return store.saveSync();
  }

  function saveStore(name, next) {
    var store = self[name];

    if (!store.save && !store.saveSync) {
      return next(new Error('nconf store ' + store.type + ' has no save() method'));
    }

    return store.saveSync
      ? next(null, store.saveSync())
      : store.save(next);
  }

  //
  // If we don't have a callback and the current
  // store is capable of saving synchronously
  // then do so.
  //
  if (!callback) {
    return common.merge(this._stores.map(saveStoreSync));
  }

  async.map(this._stores, saveStore, function (err, objs) {
    return err ? callback(err) : callback();
  });
};

//
// ### @private function _execute (action, syncLength, [arguments])
// #### @action {string} Action to execute on `this.store`.
// #### @syncLength {number} Function length of the sync version.
// #### @arguments {Array} Arguments array to apply to the action
// Executes the specified `action` on all stores for this instance, ensuring a callback supplied
// to a synchronous store function is still invoked.
//
Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
  var args = Array.prototype.slice.call(arguments, 2),
      callback = typeof args[args.length - 1] === 'function' && args.pop(),
      self = this,
      response;

  function runAction (name, next) {
    var store = self[name]

    return store[action].length > syncLength
      ? store[action].apply(store, args.concat(next))
      : next(null, store[action].apply(store, args));
  }

  if (callback) {
    return async.forEach(self._stores, runAction, function (err) {
      return err ? callback(err) : callback();
    });
  }

  this._stores.forEach(function (name) {
    var store = self[name];
    response = store[action].apply(store, args);
  });

  return response;
}

//
// ### @argv {boolean}
// Gets or sets a property representing overrides which supercede all
// other values for this instance.
//
Provider.prototype.__defineSetter__('overrides', function (val) { updateSystem.call(this, 'overrides', val) });
Provider.prototype.__defineGetter__('overrides', function () { return this._argv });

//
// ### @argv {boolean}
// Gets or sets a property indicating if we should wrap calls to `.get`
// by checking `optimist.argv`. Can be a boolean or the pass-thru
// options for `optimist`.
//
Provider.prototype.__defineSetter__('argv', function (val) { updateSystem.call(this, 'argv', val) });
Provider.prototype.__defineGetter__('argv', function () { return this._argv });

//
// ### @env {boolean}
// Gets or sets a property indicating if we should wrap calls to `.get`
// by checking `process.env`. Can be a boolean or an Array of
// environment variables to extract.
//
Provider.prototype.__defineSetter__('env', function (val) { updateSystem.call(this, 'env', val) });
Provider.prototype.__defineGetter__('env', function () { return this._env });

//
// Throw the `err` if a callback is not supplied
//
function onError(err, callback) {
  if (callback) {
    return callback(err);
  }

  throw err;
}

//
// Helper function for working with the
// default `system` store for providers.
//
function updateSystem(prop, value) {
  var system = this['system'];

  if (system[prop] === value) {
    return;
  }

  value = value || false;
  this['_' + prop] = value;
  system[prop] = value;
  system.loadSync();
}<commit_msg>[api] Load sources into the default system store so they are permenantly cached<commit_after>/*
 * provider.js: Abstraction providing an interface into pluggable configuration storage.
 *
 * (C) 2011, Charlie Robbins
 *
 */

var async = require('async'),
    common = require('./common'),
    stores = require('./stores');

//
// ### function Provider (options)
// #### @options {Object} Options for this instance.
// Constructor function for the Provider object responsible
// for exposing the pluggable storage features of `nconf`.
//
var Provider = exports.Provider = function (options) {
  var self = this;

  //
  // Setup default options for working with `stores`,
  // `overrides`, `process.env` and `process.argv`.
  //
  options         = options           || {};
  this._overrides = options.overrides || null;
  this._argv      = options.argv      || false;
  this._env       = options.env       || false;
  this._reserved  = Object.keys(Provider.prototype);
  this._stores    = [];
  this.sources    = [];

  //
  // Add the default `system` store for working with
  // `overrides`, `process.env`, `process.argv` and
  // a simple in-memory objects.
  //
  this.add('system', options);

  //
  // Add any stores passed in through the options
  // to this instance.
  //
  if (options.type) {
    this.add(options.type, options);
  }
  else if (options.store) {
    this.add(options.store.name || options.store.type, options.store);
  }
  else if (options.stores) {
    Object.keys(options.stores).forEach(function (name) {
      var store = options.stores[name];
      self.add(store.name || name || store.type, store);
    });
  }

  //
  // Add any read-only sources to this instance
  //
  if (options.source) {
    this.sources.push(this.create(options.source.type || options.source.name, options.source));
  }
  else if (options.sources) {
    Object.keys(options.sources).forEach(function (name) {
      var source = options.sources[name];
      self.sources.push(self.create(source.type || source.name || name, source));
    });
  }
};

//
// ### function use (name, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Adds (or replaces) a new store with the specified `name`
// and `options`. If `options.type` is not set, then `name`
// will be used instead:
//
//    provider.use('file');
//    provider.use('file', { type: 'file', filename: '/path/to/userconf' })
//
Provider.prototype.use = function (name, options) {
  if (name === 'system') {
    return;
  }
  else if (this._reserved.indexOf(name) !== -1) {
    throw new Error('Cannot use reserved name: ' + name);
  }

  options  = options      || {};
  var type = options.type || name;

  function sameOptions (store) {
    return Object.keys(options).every(function (key) {
      return options[key] === store[key];
    });
  }

  var store = this[name],
      update = store && !sameOptions(store);

  if (!store || update) {
    if (update) {
      this.remove(name);
    }

    this.add(name, options);
  }

  return this;
};

//
// ### function add (name, options)
// #### @name {string} Name of the store to add to this instance
// #### @options {Object} Options for the store to create
// Adds a new store with the specified `name` and `options`. If `options.type`
// is not set, then `name` will be used instead:
//
//    provider.add('memory');
//    provider.add('userconf', { type: 'file', filename: '/path/to/userconf' })
//
Provider.prototype.add = function (name, options) {
  if (this._reserved.indexOf(name) !== -1) {
    throw new Error('Cannot use reserved name: ' + name);
  }

  options  = options      || {};
  var type = options.type || name;

  if (Object.keys(stores).indexOf(common.capitalize(type)) === -1) {
    throw new Error('Cannot add store with unknown type: ' + type);
  }

  this[name] = this.create(type, options);
  this._stores.push(name);

  if (this[name].loadSync) {
    this[name].loadSync();
  }

  return this;
};

//
// ### function remove (name)
// #### @name {string} Name of the store to remove from this instance
// Removes a store with the specified `name` from this instance. Users
// are allowed to pass in a type argument (e.g. `memory`) as name if
// this was used in the call to `.add()`.
//
Provider.prototype.remove = function (name) {
  if (this._reserved.indexOf(name) !== -1) {
    throw new Error('Cannot use reserved name: ' + name);
  }
  else if (!this[name]) {
    throw new Error('Cannot remove store that does not exist: ' + name);
  }

  delete this[name];
  this._stores.splice(this._stores.indexOf(name), 1);
  return this;
};

//
// ### function create (type, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Creates a store of the specified `type` using the
// specified `options`.
//
Provider.prototype.create = function (type, options) {
  return new stores[common.capitalize(type.toLowerCase())](options);
};

//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Provider.prototype.get = function (key, callback) {
  //
  // If there is no callback we can short-circuit into the default
  // logic for traversing stores.
  //
  if (!callback) {
    return this._execute('get', 1, key, callback);
  }

  //
  // Otherwise the asynchronous, hierarchical `get` is
  // slightly more complicated because we do not need to traverse
  // the entire set of stores, but up until there is a defined value.
  //
  var current = 0,
      self = this,
      response;

  async.whilst(function () {
    return typeof response === 'undefined' && current < self._stores.length;
  }, function (next) {
    var store = self[self._stores[current]];
    current++;

    if (store.get.length >= 2) {
      return store.get(key, function (err, value) {
        if (err) {
          return next(err);
        }

        response = value;
        next();
      });
    }

    response = store.get(key);
    next();
  }, function (err) {
    return err ? callback(err) : callback(null, response);
  });
};

//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Provider.prototype.set = function (key, value, callback) {
  return this._execute('set', 2, key, value, callback);
};

//
// ### function reset (callback)
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Provider.prototype.reset = function (callback) {
  return this._execute('reset', 0, callback);
};

//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Provider.prototype.clear = function (key, callback) {
  return this._execute('clear', 1, key, callback);
};

//
// ### function merge ([key,] value [, callback])
// #### @key {string} Key to merge the value into
// #### @value {literal|Object} Value to merge into the key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Merges the properties in `value` into the existing object value at `key`.
//
// 1. If the existing value `key` is not an Object, it will be completely overwritten.
// 2. If `key` is not supplied, then the `value` will be merged into the root.
//
Provider.prototype.merge = function () {
  var self = this,
      args = Array.prototype.slice.call(arguments),
      callback = typeof args[args.length - 1] === 'function' && args.pop(),
      value = args.pop(),
      key = args.pop();

  function mergeProperty (prop, next) {
    return self._execute('merge', 2, prop, value[prop], next);
  }

  if (!key) {
    if (Array.isArray(value) || typeof value !== 'object') {
      return onError(new Error('Cannot merge non-Object into top-level.'), callback);
    }

    return async.forEach(Object.keys(value), mergeProperty, callback || function () { })
  }

  return this._execute('merge', 2, key, value, callback);
};

//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Provider.prototype.load = function (callback) {
  var self = this,
      stores = this._stores.map(function (name) { return self[name] });

  function loadStoreSync(store) {
    if (!store.loadSync) {
      throw new Error('nconf store ' + store.type + ' has no loadSync() method');
    }

    return store.loadSync();
  }

  function loadStore(store, next) {
    if (!store.load && !store.loadSync) {
      return next(new Error('nconf store ' + store.type + ' has no load() method'));
    }

    return store.loadSync
      ? next(null, store.loadSync())
      : store.load(next);
  }

  function loadBatch (targets, done) {
    if (!done) {
      return common.merge(targets.map(loadStoreSync));
    }

    async.map(targets, loadStore, function (err, objs) {
      return err ? done(err) : done(null, common.merge(objs));
    });
  }

  function mergeSources (data) {
    //
    // If `data` was returned then merge it into
    // the system store.
    //
    if (data && typeof data === 'object') {
      Object.keys(data).forEach(function (key) {
        self.system.merge(key, data[key]);
      });
    }
  }

  function loadSources () {
    //
    // If we don't have a callback and the current
    // store is capable of loading synchronously
    // then do so.
    //
    if (!callback) {
      mergeSources(loadBatch(self.sources));
      return loadBatch(stores);
    }

    loadBatch(self.sources, function (err, data) {
      if (err) {
        return callback(err);
      }

      mergeSources(data);
      return loadBatch(stores, callback);
    });
  }

  return self.sources.length
    ? loadSources()
    : loadBatch(stores, callback);
};

//
// ### function save (value, callback)
// #### @value {Object} **Optional** Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`.
//
Provider.prototype.save = function (value, callback) {
  if (!callback && typeof value === 'function') {
    callback = value;
    value = null;
  }

  var self = this;

  function saveStoreSync(name) {
    var store = self[name];

    if (!store.saveSync) {
      throw new Error('nconf store ' + store.type + ' has no saveSync() method');
    }

    return store.saveSync();
  }

  function saveStore(name, next) {
    var store = self[name];

    if (!store.save && !store.saveSync) {
      return next(new Error('nconf store ' + store.type + ' has no save() method'));
    }

    return store.saveSync
      ? next(null, store.saveSync())
      : store.save(next);
  }

  //
  // If we don't have a callback and the current
  // store is capable of saving synchronously
  // then do so.
  //
  if (!callback) {
    return common.merge(this._stores.map(saveStoreSync));
  }

  async.map(this._stores, saveStore, function (err, objs) {
    return err ? callback(err) : callback();
  });
};

//
// ### @private function _execute (action, syncLength, [arguments])
// #### @action {string} Action to execute on `this.store`.
// #### @syncLength {number} Function length of the sync version.
// #### @arguments {Array} Arguments array to apply to the action
// Executes the specified `action` on all stores for this instance, ensuring a callback supplied
// to a synchronous store function is still invoked.
//
Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
  var args = Array.prototype.slice.call(arguments, 2),
      callback = typeof args[args.length - 1] === 'function' && args.pop(),
      self = this,
      response;

  function runAction (name, next) {
    var store = self[name]

    return store[action].length > syncLength
      ? store[action].apply(store, args.concat(next))
      : next(null, store[action].apply(store, args));
  }

  if (callback) {
    return async.forEach(self._stores, runAction, function (err) {
      return err ? callback(err) : callback();
    });
  }

  this._stores.forEach(function (name) {
    var store = self[name];
    response = store[action].apply(store, args);
  });

  return response;
}

//
// ### @argv {boolean}
// Gets or sets a property representing overrides which supercede all
// other values for this instance.
//
Provider.prototype.__defineSetter__('overrides', function (val) { updateSystem.call(this, 'overrides', val) });
Provider.prototype.__defineGetter__('overrides', function () { return this._argv });

//
// ### @argv {boolean}
// Gets or sets a property indicating if we should wrap calls to `.get`
// by checking `optimist.argv`. Can be a boolean or the pass-thru
// options for `optimist`.
//
Provider.prototype.__defineSetter__('argv', function (val) { updateSystem.call(this, 'argv', val) });
Provider.prototype.__defineGetter__('argv', function () { return this._argv });

//
// ### @env {boolean}
// Gets or sets a property indicating if we should wrap calls to `.get`
// by checking `process.env`. Can be a boolean or an Array of
// environment variables to extract.
//
Provider.prototype.__defineSetter__('env', function (val) { updateSystem.call(this, 'env', val) });
Provider.prototype.__defineGetter__('env', function () { return this._env });

//
// Throw the `err` if a callback is not supplied
//
function onError(err, callback) {
  if (callback) {
    return callback(err);
  }

  throw err;
}

//
// Helper function for working with the
// default `system` store for providers.
//
function updateSystem(prop, value) {
  var system = this['system'];

  if (system[prop] === value) {
    return;
  }

  value = value || false;
  this['_' + prop] = value;
  system[prop] = value;
  system.loadSync();
}<|endoftext|>