123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- /*
- GET /note
- GET /note/:id
- POST /note
- PUT /note/:id
- DELETE /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;
|