index.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import express from 'express';
  2. import { MemoryBackend, wrapBackend } from './backends/index.js';
  3. import { MemoryFileBackend } from '../fileStore/backends/index.js';
  4. import fileStore from '../fileStore/index.js';
  5. import { throwError, errorGuard, errorMiddleware } from '../error.js';
  6. // Utility functions
  7. // ROADMAP
  8. // - Add bulk operations with atomicity
  9. // - Add Queries
  10. // - Add relationship
  11. // - Add http2 relationship ?
  12. // - Add multiple strategies
  13. // - Read / Write
  14. // - Read only
  15. // - No access (only from execute)
  16. const SAFE_METHOD = ['GET', 'OPTIONS', 'HEAD'];
  17. // Store Middleware
  18. export const store = ({
  19. prefix = 'store',
  20. backend = MemoryBackend(),
  21. fileBackend = MemoryFileBackend(),
  22. hooks = {},
  23. } = {}) => {
  24. const router = express.Router();
  25. const applyHooks = async (
  26. type,
  27. req,
  28. roContextAddition,
  29. writableContextAddition = {}
  30. ) => {
  31. let hooksMap = hooks;
  32. if (typeof hooks === 'function') {
  33. hooksMap = hooks(req);
  34. }
  35. const {
  36. body,
  37. params: { boxId, id },
  38. query,
  39. method,
  40. authenticatedUser = null,
  41. } = req;
  42. const roContext = {
  43. method,
  44. boxId: boxId,
  45. resourceId: id,
  46. userId: authenticatedUser,
  47. ...roContextAddition,
  48. };
  49. let context = {
  50. query,
  51. body,
  52. ...writableContextAddition,
  53. ...roContext,
  54. };
  55. const hookList = hooksMap[type] || [];
  56. for (const hook of hookList) {
  57. const newContext = await hook(context);
  58. context = { ...newContext, ...roContext };
  59. }
  60. return context;
  61. };
  62. // Resource list
  63. router.get(
  64. `/${prefix}/:boxId/`,
  65. errorGuard(async (req, res) => {
  66. const { boxId } = req.params;
  67. const { siteId, authenticatedUser } = req;
  68. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  69. const { query, allow = false } = await applyHooks('before', req, {
  70. store: wrappedBackend,
  71. });
  72. if (!allow && !(await wrappedBackend.checkSecurity(boxId, null))) {
  73. throwError('You need read access for this box', 403);
  74. }
  75. const {
  76. limit = '50',
  77. sort = '_createdOn',
  78. skip = '0',
  79. q,
  80. fields,
  81. } = query;
  82. const onlyFields = fields ? fields.split(',') : [];
  83. const parsedLimit = parseInt(limit, 10);
  84. const parsedSkip = parseInt(skip, 10);
  85. let sortProperty = sort;
  86. let asc = true;
  87. // If prefixed with '-' inverse order
  88. if (sort[0] === '-') {
  89. sortProperty = sort.substring(1);
  90. asc = false;
  91. }
  92. const response = await wrappedBackend.list(boxId, {
  93. sort: sortProperty,
  94. asc,
  95. limit: parsedLimit,
  96. skip: parsedSkip,
  97. onlyFields: onlyFields,
  98. q,
  99. });
  100. const { response: hookedResponse } = await applyHooks(
  101. 'after',
  102. req,
  103. {
  104. query,
  105. store: wrappedBackend,
  106. },
  107. { response }
  108. );
  109. res.json(hookedResponse);
  110. })
  111. );
  112. // One object
  113. router.get(
  114. `/${prefix}/:boxId/:id`,
  115. errorGuard(async (req, res) => {
  116. const { boxId, id } = req.params;
  117. const { siteId, authenticatedUser } = req;
  118. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  119. if (boxId[0] === '_') {
  120. throwError(
  121. "'_' char is forbidden as first letter of a box id parameter",
  122. 400
  123. );
  124. }
  125. const { allow = false } = await applyHooks('before', req, {
  126. store: wrappedBackend,
  127. });
  128. if (!allow && !(await wrappedBackend.checkSecurity(boxId, id))) {
  129. throwError('You need read access for this box', 403);
  130. }
  131. const response = await wrappedBackend.get(boxId, id);
  132. const { response: hookedResponse } = await applyHooks(
  133. 'after',
  134. req,
  135. {
  136. store: wrappedBackend,
  137. },
  138. { response }
  139. );
  140. res.json(hookedResponse);
  141. })
  142. );
  143. // Create / replace object
  144. router.post(
  145. `/${prefix}/:boxId/:id?`,
  146. errorGuard(async (req, res) => {
  147. const {
  148. params: { boxId, id },
  149. siteId,
  150. authenticatedUser,
  151. } = req;
  152. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  153. if (boxId[0] === '_') {
  154. throwError(
  155. "'_' char is forbidden for first letter of a box id parameter",
  156. 400
  157. );
  158. }
  159. const { body, allow = false } = await applyHooks('before', req, {
  160. store: wrappedBackend,
  161. });
  162. if (!allow && !(await wrappedBackend.checkSecurity(boxId, id, true))) {
  163. throwError('You need write access for this box', 403);
  164. }
  165. const response = await wrappedBackend.save(boxId, id, body);
  166. const { response: hookedResponse } = await applyHooks('after', req, {
  167. response,
  168. store: wrappedBackend,
  169. });
  170. return res.json(hookedResponse);
  171. })
  172. );
  173. // Update existing object
  174. router.put(
  175. `/${prefix}/:boxId/:id`,
  176. errorGuard(async (req, res) => {
  177. const { boxId, id } = req.params;
  178. const { siteId, authenticatedUser } = req;
  179. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  180. if (boxId[0] === '_') {
  181. throwError(
  182. "'_' char is forbidden for first letter of a letter of a box id parameter",
  183. 400
  184. );
  185. }
  186. const { body, allow = false } = await applyHooks('before', req, {
  187. store: wrappedBackend,
  188. });
  189. if (!allow && !(await wrappedBackend.checkSecurity(boxId, id, true))) {
  190. throwError('You need write access for this resource', 403);
  191. }
  192. const response = await wrappedBackend.update(boxId, id, body);
  193. const { response: hookedResponse } = await applyHooks('after', req, {
  194. response,
  195. store: wrappedBackend,
  196. });
  197. return res.json(hookedResponse);
  198. })
  199. );
  200. // Delete object
  201. router.delete(
  202. `/${prefix}/:boxId/:id`,
  203. errorGuard(async (req, res) => {
  204. const { boxId, id } = req.params;
  205. const { siteId, authenticatedUser } = req;
  206. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  207. if (boxId[0] === '_') {
  208. throwError(
  209. "'_' char is forbidden for first letter of a box id parameter",
  210. 400
  211. );
  212. }
  213. const { allow = false } = await applyHooks('before', req, {
  214. store: wrappedBackend,
  215. });
  216. if (!allow && !(await wrappedBackend.checkSecurity(boxId, id, true))) {
  217. throwError('You need write access for this resource', 403);
  218. }
  219. const result = await wrappedBackend.delete(boxId, id);
  220. await applyHooks('after', req, {
  221. store: wrappedBackend,
  222. });
  223. if (result === 1) {
  224. res.json({ message: 'Deleted' });
  225. return;
  226. }
  227. throwError('Box or resource not found', 404);
  228. })
  229. );
  230. router.use(
  231. `/${prefix}/:boxId/:id/file`,
  232. errorGuard(async (req, _, next) => {
  233. const { boxId, id } = req.params;
  234. const { siteId, authenticatedUser } = req;
  235. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  236. const { allow = false } = await applyHooks('beforeFile', req, {
  237. store: wrappedBackend,
  238. });
  239. if (
  240. !allow &&
  241. !(await wrappedBackend.checkSecurity(
  242. boxId,
  243. id,
  244. !SAFE_METHOD.includes(req.method)
  245. ))
  246. ) {
  247. throwError('You need write access for this resource', 403);
  248. }
  249. req.boxId = boxId;
  250. req.resourceId = id;
  251. next();
  252. }),
  253. fileStore(fileBackend, { prefix }),
  254. errorGuard(async (req, _, next) => {
  255. const { siteId, authenticatedUser } = req;
  256. const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
  257. console.log('execute after file hooks');
  258. await applyHooks('afterFile', req, {
  259. store: wrappedBackend,
  260. });
  261. next();
  262. })
  263. );
  264. router.use(errorMiddleware);
  265. return router;
  266. };
  267. export default store;