http.publish.server.api.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. /*
  2. GET /note
  3. GET /note/:id
  4. POST /note
  5. PUT /note/:id
  6. DELETE /note/:id
  7. */
  8. // Could be cool if we could serve some api doc or even an api script
  9. // user could do <script href="/note/api?token=1&user=2"></script> and be served
  10. // a client-side javascript api?
  11. // Eg.
  12. // HTTP.api.note.create();
  13. // HTTP.api.login(username, password);
  14. // HTTP.api.logout
  15. _publishHTTP = {};
  16. // Cache the names of all http methods we've published
  17. _publishHTTP.currentlyPublished = [];
  18. var defaultAPIPrefix = '/api/';
  19. /**
  20. * @method _publishHTTP.getPublishScope
  21. * @private
  22. * @param {Object} scope
  23. * @returns {httpPublishGetPublishScope.publishScope}
  24. *
  25. * Creates a nice scope for the publish method
  26. */
  27. _publishHTTP.getPublishScope = function httpPublishGetPublishScope(scope) {
  28. var publishScope = {};
  29. publishScope.userId = scope.userId;
  30. publishScope.params = scope.params;
  31. publishScope.query = scope.query;
  32. // TODO: Additional scoping
  33. // publishScope.added
  34. // publishScope.ready
  35. return publishScope;
  36. };
  37. _publishHTTP.formatHandlers = {};
  38. /**
  39. * @method _publishHTTP.formatHandlers.json
  40. * @private
  41. * @param {Object} result - The result object
  42. * @returns {String} JSON
  43. *
  44. * Formats the output into JSON and sets the appropriate content type on `this`
  45. */
  46. _publishHTTP.formatHandlers.json = function httpPublishJSONFormatHandler(result) {
  47. // Set the method scope content type to json
  48. this.setContentType('application/json');
  49. // Return EJSON string
  50. return EJSON.stringify(result);
  51. };
  52. /**
  53. * @method _publishHTTP.formatResult
  54. * @private
  55. * @param {Object} result - The result object
  56. * @param {Object} scope
  57. * @param {String} [defaultFormat='json'] - Default format to use if format is not in query string.
  58. * @returns {Any} The formatted result
  59. *
  60. * Formats the result into the format selected by querystring eg. "&format=json"
  61. */
  62. _publishHTTP.formatResult = function httpPublishFormatResult(result, scope, defaultFormat) {
  63. // Get the format in lower case and default to json
  64. var format = scope && scope.query && scope.query.format || defaultFormat || 'json';
  65. // Set the format handler found
  66. var formatHandlerFound = !!(typeof _publishHTTP.formatHandlers[format] === 'function');
  67. // Set the format handler and fallback to default json if handler not found
  68. var formatHandler = _publishHTTP.formatHandlers[(formatHandlerFound) ? format : 'json'];
  69. // Check if format handler is a function
  70. if (typeof formatHandler !== 'function') {
  71. // We break things the user could have overwritten the default json handler
  72. throw new Error('The default json format handler not found');
  73. }
  74. if (!formatHandlerFound) {
  75. scope.setStatusCode(500);
  76. return '{"error":"Format handler for: `' + format + '` not found"}';
  77. }
  78. // Execute the format handler
  79. try {
  80. return formatHandler.apply(scope, [result]);
  81. } catch(err) {
  82. scope.setStatusCode(500);
  83. return '{"error":"Format handler for: `' + format + '` Error: ' + err.message + '"}';
  84. }
  85. };
  86. /**
  87. * @method _publishHTTP.error
  88. * @private
  89. * @param {String} statusCode - The status code
  90. * @param {String} message - The message
  91. * @param {Object} scope
  92. * @returns {Any} The formatted result
  93. *
  94. * Responds with error message in the expected format
  95. */
  96. _publishHTTP.error = function httpPublishError(statusCode, message, scope) {
  97. var result = _publishHTTP.formatResult(message, scope);
  98. scope.setStatusCode(statusCode);
  99. return result;
  100. };
  101. /**
  102. * @method _publishHTTP.getMethodHandler
  103. * @private
  104. * @param {Meteor.Collection} collection - The Meteor.Collection instance
  105. * @param {String} methodName - The method name
  106. * @returns {Function} The server method
  107. *
  108. * Returns the DDP connection handler, already setup and secured
  109. */
  110. _publishHTTP.getMethodHandler = function httpPublishGetMethodHandler(collection, methodName) {
  111. if (collection instanceof Meteor.Collection) {
  112. if (collection._connection && collection._connection.method_handlers) {
  113. return collection._connection.method_handlers[collection._prefix + methodName];
  114. } else {
  115. throw new Error('HTTP publish does not work with current version of Meteor');
  116. }
  117. } else {
  118. throw new Error('_publishHTTP.getMethodHandler expected a collection');
  119. }
  120. };
  121. /**
  122. * @method _publishHTTP.unpublishList
  123. * @private
  124. * @param {Array} names - List of method names to unpublish
  125. * @returns {undefined}
  126. *
  127. * Unpublishes all HTTP methods that have names matching the given list.
  128. */
  129. _publishHTTP.unpublishList = function httpPublishUnpublishList(names) {
  130. if (!names.length) {
  131. return;
  132. }
  133. // Carry object for methods
  134. var methods = {};
  135. // Unpublish the rest points by setting them to false
  136. for (var i = 0, ln = names.length; i < ln; i++) {
  137. methods[names[i]] = false;
  138. }
  139. HTTP.methods(methods);
  140. // Remove the names from our list of currently published methods
  141. _publishHTTP.currentlyPublished = _.difference(_publishHTTP.currentlyPublished, names);
  142. };
  143. /**
  144. * @method _publishHTTP.unpublish
  145. * @private
  146. * @param {String|Meteor.Collection} [name] - The method name or collection
  147. * @returns {undefined}
  148. *
  149. * Unpublishes all HTTP methods that were published with the given name or
  150. * for the given collection. Call with no arguments to unpublish all.
  151. */
  152. _publishHTTP.unpublish = function httpPublishUnpublish(/* name or collection, options */) {
  153. // Determine what method name we're unpublishing
  154. var name = (arguments[0] instanceof Meteor.Collection) ?
  155. defaultAPIPrefix + arguments[0]._name : arguments[0];
  156. // Unpublish name and name/id
  157. if (name && name.length) {
  158. _publishHTTP.unpublishList([name, name + '/:id']);
  159. }
  160. // If no args, unpublish all
  161. else {
  162. _publishHTTP.unpublishList(_publishHTTP.currentlyPublished);
  163. }
  164. };
  165. /**
  166. * @method HTTP.publishFormats
  167. * @public
  168. * @param {Object} newHandlers
  169. * @returns {undefined}
  170. *
  171. * Add publish formats. Example:
  172. ```js
  173. HTTP.publishFormats({
  174. json: function(inputObject) {
  175. // Set the method scope content type to json
  176. this.setContentType('application/json');
  177. // Return EJSON string
  178. return EJSON.stringify(inputObject);
  179. }
  180. });
  181. ```
  182. */
  183. HTTP.publishFormats = function httpPublishFormats(newHandlers) {
  184. _.extend(_publishHTTP.formatHandlers, newHandlers);
  185. };
  186. /**
  187. * @method HTTP.publish
  188. * @public
  189. * @param {Object} options
  190. * @param {String} [name] - Restpoint name (url prefix). Optional if `collection` is passed. Will mount on `/api/collectionName` by default.
  191. * @param {Meteor.Collection} [collection] - Meteor.Collection instance. Required for all restpoints except collectionGet
  192. * @param {String} [options.defaultFormat='json'] - Format to use for responses when `format` is not found in the query string.
  193. * @param {String} [options.collectionGet=true] - Add GET restpoint for collection? Requires a publish function.
  194. * @param {String} [options.collectionPost=true] - Add POST restpoint for adding documents to the collection?
  195. * @param {String} [options.documentGet=true] - Add GET restpoint for documents in collection? Requires a publish function.
  196. * @param {String} [options.documentPut=true] - Add PUT restpoint for updating a document in the collection?
  197. * @param {String} [options.documentDelete=true] - Add DELETE restpoint for deleting a document in the collection?
  198. * @param {Function} [publishFunc] - A publish function. Required to mount GET restpoints.
  199. * @returns {undefined}
  200. * @todo this should use options argument instead of optional args
  201. *
  202. * Publishes one or more restpoints, mounted on "name" ("/api/collectionName/"
  203. * by default). The GET restpoints are subscribed to the document set (cursor)
  204. * returned by the publish function you supply. The other restpoints forward
  205. * requests to Meteor's built-in DDP methods (insert, update, remove), meaning
  206. * that full allow/deny security is automatic.
  207. *
  208. * __Usage:__
  209. *
  210. * Publish only:
  211. *
  212. * HTTP.publish({name: 'mypublish'}, publishFunc);
  213. *
  214. * Publish and mount crud rest point for collection /api/myCollection:
  215. *
  216. * HTTP.publish({collection: myCollection}, publishFunc);
  217. *
  218. * Mount CUD rest point for collection and documents without GET:
  219. *
  220. * HTTP.publish({collection: myCollection});
  221. *
  222. */
  223. HTTP.publish = function httpPublish(options, publishFunc) {
  224. options = _.extend({
  225. name: null,
  226. auth: null,
  227. collection: null,
  228. defaultFormat: null,
  229. collectionGet: true,
  230. collectionPost: true,
  231. documentGet: true,
  232. documentPut: true,
  233. documentDelete: true
  234. }, options || {});
  235. var collection = options.collection;
  236. // Use provided name or build one
  237. var name = (typeof options.name === "string") ? options.name : defaultAPIPrefix + collection._name;
  238. // Make sure we have a name
  239. if (typeof name !== "string") {
  240. throw new Error('HTTP.publish expected a collection or name option');
  241. }
  242. var defaultFormat = options.defaultFormat;
  243. // Rig the methods for the CRUD interface
  244. var methods = {};
  245. // console.log('HTTP restpoint: ' + name);
  246. // list and create
  247. methods[name] = {};
  248. if (options.collectionGet && publishFunc) {
  249. // Return the published documents
  250. methods[name].get = function(data) {
  251. // Format the scope for the publish method
  252. var publishScope = _publishHTTP.getPublishScope(this);
  253. // Get the publish cursor
  254. var cursor = publishFunc.apply(publishScope, [data]);
  255. // Check if its a cursor
  256. if (cursor && cursor.fetch) {
  257. // Fetch the data fron cursor
  258. var result = cursor.fetch();
  259. // Return the data
  260. return _publishHTTP.formatResult(result, this, defaultFormat);
  261. } else {
  262. // We didnt get any
  263. return _publishHTTP.error(200, [], this);
  264. }
  265. };
  266. }
  267. if (collection) {
  268. // If we have a collection then add insert method
  269. if (options.collectionPost) {
  270. methods[name].post = function(data) {
  271. var insertMethodHandler = _publishHTTP.getMethodHandler(collection, 'insert');
  272. // Make sure that _id isset else create a Meteor id
  273. data._id = data._id || Random.id();
  274. // Create the document
  275. try {
  276. // We should be passed a document in data
  277. insertMethodHandler.apply(this, [data]);
  278. // Return the data
  279. return _publishHTTP.formatResult({ _id: data._id }, this, defaultFormat);
  280. } catch(err) {
  281. // This would be a Meteor.error?
  282. return _publishHTTP.error(err.error, { error: err.message }, this);
  283. }
  284. };
  285. }
  286. // We also add the findOne, update and remove methods
  287. methods[name + '/:id'] = {};
  288. if (options.documentGet && publishFunc) {
  289. // We have to have a publish method inorder to publish id? The user could
  290. // just write a publish all if needed - better to make this explicit
  291. methods[name + '/:id'].get = function(data) {
  292. // Get the mongoId
  293. var mongoId = this.params.id;
  294. // We would allways expect a string but it could be empty
  295. if (mongoId !== '') {
  296. // Format the scope for the publish method
  297. var publishScope = _publishHTTP.getPublishScope(this);
  298. // Get the publish cursor
  299. var cursor = publishFunc.apply(publishScope, [data]);
  300. // Result will contain the document if found
  301. var result;
  302. // Check to see if document is in published cursor
  303. if (cursor) {
  304. cursor.forEach(function(doc) {
  305. if (!result) {
  306. if (doc._id === mongoId) {
  307. result = doc;
  308. }
  309. }
  310. });
  311. }
  312. // If the document is found the return
  313. if (result) {
  314. return _publishHTTP.formatResult(result, this, defaultFormat);
  315. } else {
  316. // We do a check to see if the doc id exists
  317. var exists = collection.findOne({ _id: mongoId });
  318. // If it exists its not published to the user
  319. if (exists) {
  320. // Unauthorized
  321. return _publishHTTP.error(401, { error: 'Unauthorized' }, this);
  322. } else {
  323. // Not found
  324. return _publishHTTP.error(404, { error: 'Document with id ' + mongoId + ' not found' }, this);
  325. }
  326. }
  327. } else {
  328. return _publishHTTP.error(400, { error: 'Method expected a document id' }, this);
  329. }
  330. };
  331. }
  332. if (options.documentPut) {
  333. methods[name + '/:id'].put = function(data) {
  334. // Get the mongoId
  335. var mongoId = this.params.id;
  336. // We would allways expect a string but it could be empty
  337. if (mongoId !== '') {
  338. var updateMethodHandler = _publishHTTP.getMethodHandler(collection, 'update');
  339. // Create the document
  340. try {
  341. // We should be passed a document in data
  342. updateMethodHandler.apply(this, [{ _id: mongoId }, data]);
  343. // Return the data
  344. return _publishHTTP.formatResult({ _id: mongoId }, this, defaultFormat);
  345. } catch(err) {
  346. // This would be a Meteor.error?
  347. return _publishHTTP.error(err.error, { error: err.message }, this);
  348. }
  349. } else {
  350. return _publishHTTP.error(400, { error: 'Method expected a document id' }, this);
  351. }
  352. };
  353. }
  354. if (options.documentDelete) {
  355. methods[name + '/:id'].delete = function(data) {
  356. // Get the mongoId
  357. var mongoId = this.params.id;
  358. // We would allways expect a string but it could be empty
  359. if (mongoId !== '') {
  360. var removeMethodHandler = _publishHTTP.getMethodHandler(collection, 'remove');
  361. // Create the document
  362. try {
  363. // We should be passed a document in data
  364. removeMethodHandler.apply(this, [{ _id: mongoId }]);
  365. // Return the data
  366. return _publishHTTP.formatResult({ _id: mongoId }, this, defaultFormat);
  367. } catch(err) {
  368. // This would be a Meteor.error?
  369. return _publishHTTP.error(err.error, { error: err.message }, this);
  370. }
  371. } else {
  372. return _publishHTTP.error(400, { error: 'Method expected a document id' }, this);
  373. }
  374. };
  375. }
  376. }
  377. // Authenticate with our own auth method: https://github.com/zcfs/Meteor-http-methods#authentication
  378. if (options.auth) {
  379. if (methods[name]) {
  380. methods[name].auth = options.auth;
  381. }
  382. if (methods[name + '/:id']) {
  383. methods[name + '/:id'].auth = options.auth;
  384. }
  385. }
  386. // Publish the methods
  387. HTTP.methods(methods);
  388. // Mark these method names as currently published
  389. _publishHTTP.currentlyPublished = _.union(_publishHTTP.currentlyPublished, _.keys(methods));
  390. }; // EO Publish
  391. /**
  392. * @method HTTP.unpublish
  393. * @public
  394. * @param {String|Meteor.Collection} [name] - The method name or collection
  395. * @returns {undefined}
  396. *
  397. * Unpublishes all HTTP methods that were published with the given name or
  398. * for the given collection. Call with no arguments to unpublish all.
  399. */
  400. HTTP.unpublish = _publishHTTP.unpublish;