123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- import express from 'express';
- import { MemoryBackend, wrapBackend } from './backends/index.js';
- import { MemoryFileBackend } from '../fileStore/backends/index.js';
- import fileStore from '../fileStore/index.js';
- import { throwError, errorGuard, errorMiddleware } from '../error.js';
- // Utility functions
- // ROADMAP
- // - Add bulk operations with atomicity
- // - Add Queries
- // - Add relationship
- // - Add http2 relationship ?
- // - Add multiple strategies
- // - Read / Write
- // - Read only
- // - No access (only from execute)
- const SAFE_METHOD = ['GET', 'OPTIONS', 'HEAD'];
- // Store Middleware
- export const store = ({
- prefix = 'store',
- backend = MemoryBackend(),
- fileBackend = MemoryFileBackend(),
- hooks = {},
- } = {}) => {
- const router = express.Router();
- const applyHooks = async (
- type,
- req,
- roContextAddition,
- writableContextAddition = {}
- ) => {
- let hooksMap = hooks;
- if (typeof hooks === 'function') {
- hooksMap = hooks(req);
- }
- const {
- body,
- params: { boxId, id },
- query,
- method,
- authenticatedUser = null,
- } = req;
- const roContext = {
- method,
- boxId: boxId,
- resourceId: id,
- userId: authenticatedUser,
- ...roContextAddition,
- };
- let context = {
- query,
- body,
- ...writableContextAddition,
- ...roContext,
- };
- const hookList = hooksMap[type] || [];
- for (const hook of hookList) {
- const newContext = await hook(context);
- context = { ...newContext, ...roContext };
- }
- return context;
- };
- // Resource list
- router.get(
- `/${prefix}/:boxId/`,
- errorGuard(async (req, res) => {
- const { boxId } = req.params;
- const { siteId, authenticatedUser } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- const { query, allow = false } = await applyHooks('before', req, {
- store: wrappedBackend,
- });
- if (!allow && !(await wrappedBackend.checkSecurity(boxId, null))) {
- throwError('You need read access for this box', 403);
- }
- const {
- limit = '50',
- sort = '_createdOn',
- skip = '0',
- q,
- fields,
- } = query;
- const onlyFields = fields ? fields.split(',') : [];
- const parsedLimit = parseInt(limit, 10);
- const parsedSkip = parseInt(skip, 10);
- let sortProperty = sort;
- let asc = true;
- // If prefixed with '-' inverse order
- if (sort[0] === '-') {
- sortProperty = sort.substring(1);
- asc = false;
- }
- const response = await wrappedBackend.list(boxId, {
- sort: sortProperty,
- asc,
- limit: parsedLimit,
- skip: parsedSkip,
- onlyFields: onlyFields,
- q,
- });
- const { response: hookedResponse } = await applyHooks(
- 'after',
- req,
- {
- query,
- store: wrappedBackend,
- },
- { response }
- );
- res.json(hookedResponse);
- })
- );
- // One object
- router.get(
- `/${prefix}/:boxId/:id`,
- errorGuard(async (req, res) => {
- const { boxId, id } = req.params;
- const { siteId, authenticatedUser } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- if (boxId[0] === '_') {
- throwError(
- "'_' char is forbidden as first letter of a box id parameter",
- 400
- );
- }
- const { allow = false } = await applyHooks('before', req, {
- store: wrappedBackend,
- });
- if (!allow && !(await wrappedBackend.checkSecurity(boxId, id))) {
- throwError('You need read access for this box', 403);
- }
- const response = await wrappedBackend.get(boxId, id);
- const { response: hookedResponse } = await applyHooks(
- 'after',
- req,
- {
- store: wrappedBackend,
- },
- { response }
- );
- res.json(hookedResponse);
- })
- );
- // Create / replace object
- router.post(
- `/${prefix}/:boxId/:id?`,
- errorGuard(async (req, res) => {
- const {
- params: { boxId, id },
- siteId,
- authenticatedUser,
- } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- if (boxId[0] === '_') {
- throwError(
- "'_' char is forbidden for first letter of a box id parameter",
- 400
- );
- }
- const { body, allow = false } = await applyHooks('before', req, {
- store: wrappedBackend,
- });
- if (!allow && !(await wrappedBackend.checkSecurity(boxId, id, true))) {
- throwError('You need write access for this box', 403);
- }
- const response = await wrappedBackend.save(boxId, id, body);
- const { response: hookedResponse } = await applyHooks('after', req, {
- response,
- store: wrappedBackend,
- });
- return res.json(hookedResponse);
- })
- );
- // Update existing object
- router.put(
- `/${prefix}/:boxId/:id`,
- errorGuard(async (req, res) => {
- const { boxId, id } = req.params;
- const { siteId, authenticatedUser } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- if (boxId[0] === '_') {
- throwError(
- "'_' char is forbidden for first letter of a letter of a box id parameter",
- 400
- );
- }
- const { body, allow = false } = await applyHooks('before', req, {
- store: wrappedBackend,
- });
- if (!allow && !(await wrappedBackend.checkSecurity(boxId, id, true))) {
- throwError('You need write access for this resource', 403);
- }
- const response = await wrappedBackend.update(boxId, id, body);
- const { response: hookedResponse } = await applyHooks('after', req, {
- response,
- store: wrappedBackend,
- });
- return res.json(hookedResponse);
- })
- );
- // Delete object
- router.delete(
- `/${prefix}/:boxId/:id`,
- errorGuard(async (req, res) => {
- const { boxId, id } = req.params;
- const { siteId, authenticatedUser } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- if (boxId[0] === '_') {
- throwError(
- "'_' char is forbidden for first letter of a box id parameter",
- 400
- );
- }
- const { allow = false } = await applyHooks('before', req, {
- store: wrappedBackend,
- });
- if (!allow && !(await wrappedBackend.checkSecurity(boxId, id, true))) {
- throwError('You need write access for this resource', 403);
- }
- const result = await wrappedBackend.delete(boxId, id);
- await applyHooks('after', req, {
- store: wrappedBackend,
- });
- if (result === 1) {
- res.json({ message: 'Deleted' });
- return;
- }
- throwError('Box or resource not found', 404);
- })
- );
- router.use(
- `/${prefix}/:boxId/:id/file`,
- errorGuard(async (req, _, next) => {
- const { boxId, id } = req.params;
- const { siteId, authenticatedUser } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- const { allow = false } = await applyHooks('beforeFile', req, {
- store: wrappedBackend,
- });
- if (
- !allow &&
- !(await wrappedBackend.checkSecurity(
- boxId,
- id,
- !SAFE_METHOD.includes(req.method)
- ))
- ) {
- throwError('You need write access for this resource', 403);
- }
- req.boxId = boxId;
- req.resourceId = id;
- next();
- }),
- fileStore(fileBackend, { prefix }),
- errorGuard(async (req, _, next) => {
- const { siteId, authenticatedUser } = req;
- const wrappedBackend = wrapBackend(backend, siteId, authenticatedUser);
- console.log('execute after file hooks');
- await applyHooks('afterFile', req, {
- store: wrappedBackend,
- });
- next();
- })
- );
- router.use(errorMiddleware);
- return router;
- };
- export default store;
|