| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 | /*GET /noteGET /note/:idPOST /notePUT /note/:idDELETE /note/:id*/// Could be cool if we could serve some api doc or even an api script// user could do <script href="/note/api?token=1&user=2"></script> and be served// a client-side javascript api?// Eg.// HTTP.api.note.create();// HTTP.api.login(username, password);// HTTP.api.logout_publishHTTP = {};// Cache the names of all http methods we've published_publishHTTP.currentlyPublished = [];var defaultAPIPrefix = '/api/';/** * @method _publishHTTP.getPublishScope * @private * @param {Object} scope * @returns {httpPublishGetPublishScope.publishScope} *  * Creates a nice scope for the publish method */_publishHTTP.getPublishScope = function httpPublishGetPublishScope(scope) {  var publishScope = {};  publishScope.userId = scope.userId;  publishScope.params = scope.params;  publishScope.query = scope.query;  // TODO: Additional scoping  // publishScope.added  // publishScope.ready  return publishScope;};_publishHTTP.formatHandlers = {};/** * @method _publishHTTP.formatHandlers.json * @private * @param {Object} result - The result object * @returns {String} JSON *  * Formats the output into JSON and sets the appropriate content type on `this` */_publishHTTP.formatHandlers.json = function httpPublishJSONFormatHandler(result) {  // Set the method scope content type to json  this.setContentType('application/json');  // Return EJSON string  return EJSON.stringify(result);};/** * @method _publishHTTP.formatResult * @private * @param {Object} result - The result object * @param {Object} scope * @param {String} [defaultFormat='json'] - Default format to use if format is not in query string. * @returns {Any} The formatted result *  * Formats the result into the format selected by querystring eg. "&format=json" */_publishHTTP.formatResult = function httpPublishFormatResult(result, scope, defaultFormat) {  // Get the format in lower case and default to json  var format = scope && scope.query && scope.query.format || defaultFormat || 'json';  // Set the format handler found  var formatHandlerFound = !!(typeof _publishHTTP.formatHandlers[format] === 'function');  // Set the format handler and fallback to default json if handler not found  var formatHandler = _publishHTTP.formatHandlers[(formatHandlerFound) ? format : 'json'];  // Check if format handler is a function  if (typeof formatHandler !== 'function') {    // We break things the user could have overwritten the default json handler    throw new Error('The default json format handler not found');  }  if (!formatHandlerFound) {    scope.setStatusCode(500);    return '{"error":"Format handler for: `' + format + '` not found"}';  }  // Execute the format handler  try {    return formatHandler.apply(scope, [result]);  } catch(err) {    scope.setStatusCode(500);    return '{"error":"Format handler for: `' + format + '` Error: ' + err.message + '"}';  }};/** * @method _publishHTTP.error * @private * @param {String} statusCode - The status code * @param {String} message - The message * @param {Object} scope * @returns {Any} The formatted result *  * Responds with error message in the expected format */_publishHTTP.error = function httpPublishError(statusCode, message, scope) {  var result = _publishHTTP.formatResult(message, scope);  scope.setStatusCode(statusCode);  return result;};/** * @method _publishHTTP.getMethodHandler * @private * @param {Meteor.Collection} collection - The Meteor.Collection instance * @param {String} methodName - The method name * @returns {Function} The server method *  * Returns the DDP connection handler, already setup and secured */_publishHTTP.getMethodHandler = function httpPublishGetMethodHandler(collection, methodName) {  if (collection instanceof Meteor.Collection) {    if (collection._connection && collection._connection.method_handlers) {      return collection._connection.method_handlers[collection._prefix + methodName];    } else {      throw new Error('HTTP publish does not work with current version of Meteor');    }  } else {    throw new Error('_publishHTTP.getMethodHandler expected a collection');  }};/** * @method _publishHTTP.unpublishList * @private * @param {Array} names - List of method names to unpublish * @returns {undefined} *  * Unpublishes all HTTP methods that have names matching the given list. */_publishHTTP.unpublishList = function httpPublishUnpublishList(names) {  if (!names.length) {    return;  }    // Carry object for methods  var methods = {};  // Unpublish the rest points by setting them to false  for (var i = 0, ln = names.length; i < ln; i++) {    methods[names[i]] = false;  }  HTTP.methods(methods);    // Remove the names from our list of currently published methods  _publishHTTP.currentlyPublished = _.difference(_publishHTTP.currentlyPublished, names);};/** * @method _publishHTTP.unpublish * @private * @param {String|Meteor.Collection} [name] - The method name or collection * @returns {undefined} *  * Unpublishes all HTTP methods that were published with the given name or  * for the given collection. Call with no arguments to unpublish all. */_publishHTTP.unpublish = function httpPublishUnpublish(/* name or collection, options */) {    // Determine what method name we're unpublishing  var name = (arguments[0] instanceof Meteor.Collection) ?          defaultAPIPrefix + arguments[0]._name : arguments[0];            // Unpublish name and name/id  if (name && name.length) {    _publishHTTP.unpublishList([name, name + '/:id']);  }     // If no args, unpublish all  else {    _publishHTTP.unpublishList(_publishHTTP.currentlyPublished);  }  };/** * @method HTTP.publishFormats * @public * @param {Object} newHandlers * @returns {undefined} *  * Add publish formats. Example: ```js HTTP.publishFormats({    json: function(inputObject) {      // Set the method scope content type to json      this.setContentType('application/json');      // Return EJSON string      return EJSON.stringify(inputObject);    }  }); ``` */HTTP.publishFormats = function httpPublishFormats(newHandlers) {  _.extend(_publishHTTP.formatHandlers, newHandlers);};/** * @method HTTP.publish * @public * @param {Object} options * @param {String} [name] - Restpoint name (url prefix). Optional if `collection` is passed. Will mount on `/api/collectionName` by default. * @param {Meteor.Collection} [collection] - Meteor.Collection instance. Required for all restpoints except collectionGet * @param {String} [options.defaultFormat='json'] - Format to use for responses when `format` is not found in the query string. * @param {String} [options.collectionGet=true] - Add GET restpoint for collection? Requires a publish function. * @param {String} [options.collectionPost=true] - Add POST restpoint for adding documents to the collection? * @param {String} [options.documentGet=true] - Add GET restpoint for documents in collection? Requires a publish function. * @param {String} [options.documentPut=true] - Add PUT restpoint for updating a document in the collection? * @param {String} [options.documentDelete=true] - Add DELETE restpoint for deleting a document in the collection? * @param {Function} [publishFunc] - A publish function. Required to mount GET restpoints. * @returns {undefined} * @todo this should use options argument instead of optional args *  * Publishes one or more restpoints, mounted on "name" ("/api/collectionName/" * by default). The GET restpoints are subscribed to the document set (cursor) * returned by the publish function you supply. The other restpoints forward * requests to Meteor's built-in DDP methods (insert, update, remove), meaning * that full allow/deny security is automatic. *  * __Usage:__ *  * Publish only: *  * HTTP.publish({name: 'mypublish'}, publishFunc); *  * Publish and mount crud rest point for collection /api/myCollection: *  * HTTP.publish({collection: myCollection}, publishFunc); *  * Mount CUD rest point for collection and documents without GET: *  * HTTP.publish({collection: myCollection}); *  */HTTP.publish = function httpPublish(options, publishFunc) {  options = _.extend({    name: null,    auth: null,    collection: null,    defaultFormat: null,    collectionGet: true,    collectionPost: true,    documentGet: true,    documentPut: true,    documentDelete: true  }, options || {});    var collection = options.collection;    // Use provided name or build one  var name = (typeof options.name === "string") ? options.name : defaultAPIPrefix + collection._name;  // Make sure we have a name  if (typeof name !== "string") {    throw new Error('HTTP.publish expected a collection or name option');  }    var defaultFormat = options.defaultFormat;  // Rig the methods for the CRUD interface  var methods = {};  // console.log('HTTP restpoint: ' + name);  // list and create  methods[name] = {};  if (options.collectionGet && publishFunc) {    // Return the published documents    methods[name].get = function(data) {      // Format the scope for the publish method      var publishScope = _publishHTTP.getPublishScope(this);      // Get the publish cursor      var cursor = publishFunc.apply(publishScope, [data]);      // Check if its a cursor      if (cursor && cursor.fetch) {        // Fetch the data fron cursor        var result = cursor.fetch();        // Return the data        return _publishHTTP.formatResult(result, this, defaultFormat);      } else {        // We didnt get any        return _publishHTTP.error(200, [], this);      }    };  }  if (collection) {    // If we have a collection then add insert method    if (options.collectionPost) {      methods[name].post = function(data) {        var insertMethodHandler = _publishHTTP.getMethodHandler(collection, 'insert');        // Make sure that _id isset else create a Meteor id        data._id = data._id || Random.id();        // Create the document        try {          // We should be passed a document in data          insertMethodHandler.apply(this, [data]);          // Return the data          return _publishHTTP.formatResult({ _id: data._id }, this, defaultFormat);        } catch(err) {          // This would be a Meteor.error?          return _publishHTTP.error(err.error, { error: err.message }, this);        }      };    }    // We also add the findOne, update and remove methods    methods[name + '/:id'] = {};        if (options.documentGet && publishFunc) {      // We have to have a publish method inorder to publish id? The user could      // just write a publish all if needed - better to make this explicit      methods[name + '/:id'].get = function(data) {        // Get the mongoId        var mongoId = this.params.id;        // We would allways expect a string but it could be empty        if (mongoId !== '') {          // Format the scope for the publish method          var publishScope = _publishHTTP.getPublishScope(this);          // Get the publish cursor          var cursor = publishFunc.apply(publishScope, [data]);          // Result will contain the document if found          var result;          // Check to see if document is in published cursor          if (cursor) {            cursor.forEach(function(doc) {              if (!result) {                if (doc._id === mongoId) {                  result = doc;                }              }            });          }          // If the document is found the return          if (result) {            return _publishHTTP.formatResult(result, this, defaultFormat);          } else {            // We do a check to see if the doc id exists            var exists = collection.findOne({ _id: mongoId });            // If it exists its not published to the user            if (exists) {              // Unauthorized              return _publishHTTP.error(401, { error: 'Unauthorized' }, this);            } else {              // Not found              return _publishHTTP.error(404, { error: 'Document with id ' + mongoId + ' not found' }, this);            }          }        } else {          return _publishHTTP.error(400, { error: 'Method expected a document id' }, this);        }      };    }    if (options.documentPut) {      methods[name + '/:id'].put = function(data) {        // Get the mongoId        var mongoId = this.params.id;        // We would allways expect a string but it could be empty        if (mongoId !== '') {          var updateMethodHandler = _publishHTTP.getMethodHandler(collection, 'update');          // Create the document          try {            // We should be passed a document in data            updateMethodHandler.apply(this, [{ _id: mongoId }, data]);            // Return the data            return _publishHTTP.formatResult({ _id: mongoId }, this, defaultFormat);          } catch(err) {            // This would be a Meteor.error?            return _publishHTTP.error(err.error, { error: err.message }, this);          }        } else {          return _publishHTTP.error(400, { error: 'Method expected a document id' }, this);        }            };    }    if (options.documentDelete) {      methods[name + '/:id'].delete = function(data) {         // Get the mongoId        var mongoId = this.params.id;        // We would allways expect a string but it could be empty        if (mongoId !== '') {          var removeMethodHandler = _publishHTTP.getMethodHandler(collection, 'remove');          // Create the document          try {            // We should be passed a document in data            removeMethodHandler.apply(this, [{ _id: mongoId }]);            // Return the data            return _publishHTTP.formatResult({ _id: mongoId }, this, defaultFormat);          } catch(err) {            // This would be a Meteor.error?            return _publishHTTP.error(err.error, { error: err.message }, this);          }        } else {          return _publishHTTP.error(400, { error: 'Method expected a document id' }, this);        }           };    }  }  // Authenticate with our own auth method: https://github.com/zcfs/Meteor-http-methods#authentication  if (options.auth) {    if (methods[name]) {      methods[name].auth = options.auth;    }    if (methods[name + '/:id']) {      methods[name + '/:id'].auth = options.auth;    }  }  // Publish the methods  HTTP.methods(methods);    // Mark these method names as currently published  _publishHTTP.currentlyPublished = _.union(_publishHTTP.currentlyPublished, _.keys(methods));  }; // EO Publish/** * @method HTTP.unpublish * @public * @param {String|Meteor.Collection} [name] - The method name or collection * @returns {undefined} *  * Unpublishes all HTTP methods that were published with the given name or  * for the given collection. Call with no arguments to unpublish all. */HTTP.unpublish = _publishHTTP.unpublish;
 |