mirror of
https://github.com/wekan/wekan.git
synced 2025-12-18 16:30:13 +01:00
466 lines
No EOL
15 KiB
JavaScript
466 lines
No EOL
15 KiB
JavaScript
/*
|
|
|
|
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; |