Merge pull request #151 from directus/feature-redis-cache

Feature redis cache
This commit is contained in:
Rijk van Zanten
2020-09-08 19:14:21 -04:00
committed by GitHub
38 changed files with 655 additions and 387 deletions

View File

@@ -46,6 +46,25 @@ RATE_LIMITER_STORE=memory # memory | redis | memcache
## https://www.npmjs.com/package/memcached)
# RATE_LIMITER_MEMCACHE='localhost:11211'
####################################################################################################
# Caching
CACHE_ENABLED=true
CACHE_TTL="30m"
CACHE_NAMESPACE="directus-cache"
CACHE_STORE=memory # memory | redis | memcache
# CACHE_REDIS="redis://:authpassword@127.0.0.1:6380/4"
# --OR--
# CACHE_REDIS_HOST="127.0.0.1"
# CACHE_REDIS_PORT="127.0.0.1"
# CACHE_REDIS_PASSWORD="127.0.0.1"
# CACHE_REDIS_DB="127.0.0.1"
## Memcache (see https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache and
## https://www.npmjs.com/package/memcached)
# CACHE_MEMCACHE='localhost:11211'
####################################################################################################
# File Storage

120
api/package-lock.json generated
View File

@@ -25723,6 +25723,15 @@
"@hapi/hoek": "^9.0.0"
}
},
"@keyv/redis": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-2.1.2.tgz",
"integrity": "sha512-D6vNOuyH/5cBNfHcyxzck1l7V+Qd4RAT7uz2SHYAjutbXQ03o3SSneRyvrp76H4/uvHyutPWTJ1Za3EpGSVe5g==",
"optional": true,
"requires": {
"ioredis": "~4.17.1"
}
},
"@otplib/core": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
@@ -25948,6 +25957,15 @@
"@types/node": "*"
}
},
"@types/keyv": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz",
"integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.161",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz",
@@ -26327,6 +26345,12 @@
"sprintf-js": "~1.0.2"
}
},
"argv": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz",
"integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=",
"optional": true
},
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -26998,6 +27022,66 @@
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"codecov": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.2.tgz",
"integrity": "sha512-fmCjAkTese29DUX3GMIi4EaKGflHa4K51EoMc29g8fBHawdk/+KEq5CWOeXLdd9+AT7o1wO4DIpp/Z1KCqCz1g==",
"optional": true,
"requires": {
"argv": "0.0.2",
"ignore-walk": "3.0.3",
"js-yaml": "3.13.1",
"teeny-request": "6.0.1",
"urlgrey": "0.4.4"
},
"dependencies": {
"agent-base": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==",
"optional": true
},
"https-proxy-agent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz",
"integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==",
"optional": true,
"requires": {
"agent-base": "5",
"debug": "4"
}
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"optional": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"teeny-request": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
"integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==",
"optional": true,
"requires": {
"http-proxy-agent": "^4.0.0",
"https-proxy-agent": "^4.0.0",
"node-fetch": "^2.2.0",
"stream-events": "^1.0.5",
"uuid": "^3.3.2"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"optional": true
}
}
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -29255,6 +29339,11 @@
"bignumber.js": "^9.0.0"
}
},
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -29378,6 +29467,25 @@
"safe-buffer": "^5.0.1"
}
},
"keyv": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz",
"integrity": "sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw==",
"requires": {
"json-buffer": "3.0.1"
}
},
"keyv-memcache": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/keyv-memcache/-/keyv-memcache-0.8.0.tgz",
"integrity": "sha512-303ARXs6vv7v8Z12L9LK58dq8oc1TGcpeOt+CLbh0V1JCQkXLxuqt89F9Owgm3W+K3EqPTrn4NOI0f92vrsFUA==",
"optional": true,
"requires": {
"codecov": "^3.5.0",
"json-buffer": "^3.0.1",
"memjs": "^1.2.2"
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -29812,6 +29920,12 @@
"jackpot": ">=0.0.6"
}
},
"memjs": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/memjs/-/memjs-1.2.2.tgz",
"integrity": "sha512-j6I5cQsjT8izm0FcBZrwga4VmlhTMsBTPKdyKolQenLulHNvKuNcDgDmBhQvScqNLy4tjpCCFwiqFK+5l6J20g==",
"optional": true
},
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
@@ -32842,6 +32956,12 @@
"querystring": "0.2.0"
}
},
"urlgrey": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz",
"integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=",
"optional": true
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",

View File

@@ -93,6 +93,7 @@
"joi": "^17.1.1",
"js-yaml": "^3.14.0",
"jsonwebtoken": "^8.5.1",
"keyv": "^4.0.1",
"knex": "^0.21.4",
"knex-schema-inspector": "0.0.11",
"liquidjs": "^9.14.1",
@@ -112,7 +113,9 @@
"uuid-validate": "0.0.3"
},
"optionalDependencies": {
"@keyv/redis": "^2.1.2",
"ioredis": "^4.17.3",
"keyv-memcache": "^0.8.0",
"memcached": "^2.2.2",
"mssql": "^6.2.0",
"mysql": "^2.18.1",
@@ -134,6 +137,7 @@
"@types/joi": "^14.3.4",
"@types/js-yaml": "^3.12.5",
"@types/jsonwebtoken": "^8.5.0",
"@types/keyv": "^3.1.1",
"@types/lodash": "^4.14.159",
"@types/ms": "^0.7.31",
"@types/nodemailer": "^6.4.0",

View File

@@ -9,9 +9,11 @@ import env from './env';
import errorHandler from './middleware/error-handler';
import cors from './middleware/cors';
import rateLimiter from './middleware/rate-limiter';
import { respond } from './middleware/respond';
import cache from './middleware/cache';
import extractToken from './middleware/extract-token';
import authenticate from './middleware/authenticate';
import rateLimiter from './middleware/rate-limiter';
import activityRouter from './controllers/activity';
import assetsRouter from './controllers/assets';
import authRouter from './controllers/auth';
@@ -33,6 +35,7 @@ import utilsRouter from './controllers/utils';
import webhooksRouter from './controllers/webhooks';
import notFoundHandler from './controllers/not-found';
import sanitizeQuery from './middleware/sanitize-query';
validateEnv(['KEY', 'SECRET']);
@@ -70,6 +73,8 @@ if (env.RATE_LIMITER_ENABLED === true) {
app.use('/auth', authRouter);
app.use(authenticate);
app.use(sanitizeQuery);
app.use(cache);
app.use('/activity', activityRouter);
app.use('/assets', assetsRouter);
app.use('/collections', collectionsRouter);
@@ -89,6 +94,8 @@ app.use('/users', usersRouter);
app.use('/utils', utilsRouter);
app.use('/webhooks', webhooksRouter);
app.use(respond);
app.use(notFoundHandler);
app.use(errorHandler);

50
api/src/cache.ts Normal file
View File

@@ -0,0 +1,50 @@
import env from './env';
import Keyv, { Options } from 'keyv';
import { validateEnv } from './utils/validate-env';
import { getConfigFromEnv } from './utils/get-config-from-env';
import ms from 'ms';
import logger from './logger';
let cache: Keyv | null = null;
if (env.CACHE_ENABLED === true) {
validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
cache = getKevyInstance();
cache.on('error', logger.error);
}
export default cache;
function getKevyInstance() {
switch (env.CACHE_STORE) {
case 'redis':
return new Keyv(getConfig('redis'));
case 'memcache':
return new Keyv(getConfig('memcache'));
case 'memory':
default:
return new Keyv(getConfig());
}
}
function getConfig(
store: 'memory' | 'redis' | 'memcache' = 'memory'
): Options<any> {
const config: Options<any> = { namespace: env.CACHE_NAMESPACE, ttl: ms(env.CACHE_TTL as string) };
if (store === 'redis') {
const Redis = require('ioredis');
const KeyvRedis = require('@keyv/redis');
config.store = new KeyvRedis(new Redis(
env.CACHE_REDIS || getConfigFromEnv('CACHE_REDIS_')
));
}
if (store === 'memcache') {
const KeyvMemcache = require('keyv-memcache');
config.store = new KeyvMemcache(env.CACHE_MEMCACHE);
}
return config;
}

View File

@@ -30,10 +30,10 @@ STORAGE_LOCAL_ROOT="./uploads"
{{ security }}
ACCESS_TOKEN_TTL="15m",
REFRESH_TOKEN_TTL="7d",
REFRESH_TOKEN_COOKIE_SECURE=false,
REFRESH_TOKEN_COOKIE_SAME_SITE="lax",
ACCESS_TOKEN_TTL="15m"
REFRESH_TOKEN_TTL="7d"
REFRESH_TOKEN_COOKIE_SECURE=false
REFRESH_TOKEN_COOKIE_SAME_SITE="lax"
####################################################################################################
## SSO (OAuth) Providers

View File

@@ -1,7 +1,5 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import ActivityService from '../services/activity';
import MetaService from '../services/meta';
import { Action } from '../types';
@@ -10,41 +8,39 @@ const router = express.Router();
router.get(
'/',
useCollection('directus_activity'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ActivityService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
return res.json({
res.locals.payload = {
data: records || null,
meta,
});
})
};
return next();
}),
);
router.get(
'/:pk',
useCollection('directus_activity'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ActivityService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({
res.locals.payload = {
data: record || null,
});
})
};
return next();
}),
);
router.post(
'/comment',
useCollection('directus_activity'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ActivityService({ accountability: req.accountability });
const primaryKey = await service.create({
@@ -57,36 +53,37 @@ router.post(
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({
res.locals.payload = {
data: record || null,
});
})
};
return next();
}),
);
router.patch(
'/comment/:pk',
useCollection('directus_activity'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ActivityService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({
res.locals.payload = {
data: record || null,
});
})
};
return next();
}),
);
router.delete(
'/comment/:pk',
useCollection('directus_activity'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ActivityService({ accountability: req.accountability });
await service.delete(req.params.pk);
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -5,7 +5,7 @@ import { SYSTEM_ASSET_ALLOW_LIST, ASSET_TRANSFORM_QUERY_KEYS } from '../constant
import { InvalidQueryException, ForbiddenException } from '../exceptions';
import AssetsService from '../services/assets';
import validate from 'uuid-validate';
import { pick, merge } from 'lodash';
import { pick } from 'lodash';
import { Transformation } from '../types/assets';
import storage from '../storage';
import PayloadService from '../services/payload';

View File

@@ -23,7 +23,7 @@ const loginSchema = Joi.object({
router.post(
'/login',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const accountability = {
ip: req.ip,
userAgent: req.get('user-agent'),
@@ -72,14 +72,15 @@ router.post(
});
}
return res.status(200).json(payload);
})
res.locals.payload = payload;
return next();
}),
);
router.post(
'/refresh',
cookieParser(),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const accountability = {
ip: req.ip,
userAgent: req.get('user-agent'),
@@ -122,14 +123,15 @@ router.post(
});
}
return res.status(200).json(payload);
})
res.locals.payload = payload;
return next();
}),
);
router.post(
'/logout',
cookieParser(),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const accountability = {
ip: req.ip,
userAgent: req.get('user-agent'),
@@ -149,14 +151,13 @@ router.post(
}
await authenticationService.logout(currentRefreshToken);
res.status(200).end();
})
return next();
}),
);
router.post(
'/password/request',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.body.email) {
throw new InvalidPayloadException(`"email" field is required.`);
}
@@ -175,14 +176,14 @@ router.post(
// We don't want to give away what email addresses exist, so we'll always return a 200
// from this endpoint
} finally {
return res.status(200).end();
return next();
}
})
}),
);
router.post(
'/password/reset',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.body.token) {
throw new InvalidPayloadException(`"token" field is required.`);
}
@@ -199,8 +200,8 @@ router.post(
const service = new UsersService({ accountability });
await service.resetPassword(req.body.token, req.body.password);
return res.status(200).end();
})
return next();
}),
);
router.use(
@@ -215,7 +216,7 @@ router.use(grant.express()(getGrantConfig()));
*/
router.get(
'/sso/:provider/callback',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const accountability = {
ip: req.ip,
userAgent: req.get('user-agent'),
@@ -232,10 +233,12 @@ router.get(
email
);
return res.status(200).json({
res.locals.payload = {
data: { access_token: accessToken, refresh_token: refreshToken, expires },
});
})
};
return next();
}),
);
export default router;

View File

@@ -1,78 +1,75 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import CollectionsService from '../services/collections';
import useCollection from '../middleware/use-collection';
import CollectionsService from '../services/collections'
import MetaService from '../services/meta';
const router = Router();
router.post(
'/',
useCollection('directus_collections'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({ accountability: req.accountability });
const collectionKey = await collectionsService.create(req.body);
const record = await collectionsService.readByKey(collectionKey);
res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.get(
'/',
useCollection('directus_collections'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const collections = await collectionsService.readByQuery();
const meta = await metaService.getMetaForQuery(req.collection, {});
const meta = await metaService.getMetaForQuery('directus_collections', {});
res.json({ data: collections || null, meta });
res.locals.payload = { data: collections || null, meta };
return next();
})
);
router.get(
'/:collection',
useCollection('directus_collections'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({ accountability: req.accountability });
const collectionKey = req.params.collection.includes(',')
? req.params.collection.split(',')
: req.params.collection;
const collection = await collectionsService.readByKey(collectionKey as any);
res.json({ data: collection || null });
res.locals.payload = { data: collection || null };
return next();
})
);
router.patch(
'/:collection',
useCollection('directus_collections'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({ accountability: req.accountability });
const collectionKey = req.params.collection.includes(',')
? req.params.collection.split(',')
: req.params.collection;
await collectionsService.update(req.body, collectionKey as any);
const collection = await collectionsService.readByKey(collectionKey as any);
res.json({ data: collection || null });
res.locals.payload = { data: collection || null };
return next();
})
);
router.delete(
'/:collection',
useCollection('directus_collections'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const collectionsService = new CollectionsService({ accountability: req.accountability });
const collectionKey = req.params.collection.includes(',')
? req.params.collection.split(',')
: req.params.collection;
await collectionsService.delete(collectionKey as any);
res.end();
return next();
})
);

View File

@@ -7,7 +7,7 @@ const router = Router();
router.get(
'/:type',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const typeAllowList = ['interfaces', 'layouts', 'displays', 'modules'];
if (typeAllowList.includes(req.params.type) === false) {
@@ -16,10 +16,12 @@ router.get(
const interfaces = await ExtensionsService.listExtensions(req.params.type);
return res.json({
res.locals.payload = {
data: interfaces,
});
})
};
return next();
}),
);
export default router;

View File

@@ -3,56 +3,50 @@ import asyncHandler from 'express-async-handler';
import FieldsService from '../services/fields';
import validateCollection from '../middleware/collection-exists';
import { schemaInspector } from '../database';
import { FieldNotFoundException, InvalidPayloadException, ForbiddenException } from '../exceptions';
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
import Joi from 'joi';
import { Field } from '../types/field';
import useCollection from '../middleware/use-collection';
import { Accountability, types } from '../types';
import { types } from '../types';
const router = Router();
/**
* @TODO
*
* Add accountability / permissions handling to fields
*/
router.get(
'/',
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
const fields = await service.readAll();
return res.json({ data: fields || null });
})
res.locals.payload = { data: fields || null };
return next();
}),
);
router.get(
'/:collection',
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
const fields = await service.readAll(req.params.collection);
return res.json({ data: fields || null });
})
res.locals.payload = { data: fields || null };
return next();
}),
);
router.get(
'/:collection/:field',
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
const exists = await schemaInspector.hasColumn(req.collection, req.params.field);
const exists = await schemaInspector.hasColumn(req.params.collection, req.params.field);
if (exists === false) throw new ForbiddenException();
const field = await service.readOne(req.params.collection, req.params.field);
return res.json({ data: field || null });
})
res.locals.payload = { data: field || null };
return next();
}),
);
const newFieldSchema = Joi.object({
@@ -72,8 +66,7 @@ const newFieldSchema = Joi.object({
router.post(
'/:collection',
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
const { error } = newFieldSchema.validate(req.body);
@@ -88,15 +81,15 @@ router.post(
const createdField = await service.readOne(req.params.collection, field.field);
return res.json({ data: createdField || null });
})
res.locals.payload = { data: createdField || null };
return next();
}),
);
router.patch(
'/:collection',
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
if (Array.isArray(req.body) === false)
@@ -112,15 +105,16 @@ router.patch(
results.push(updatedField);
}
return res.json({ data: results || null });
})
res.locals.payload = { data: results || null };
return next();
}),
);
router.patch(
'/:collection/:field',
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
// @todo: validate field
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
const fieldData: Partial<Field> & { field: string; type: typeof types[number] } = req.body;
@@ -130,20 +124,19 @@ router.patch(
const updatedField = await service.readOne(req.params.collection, req.params.field);
return res.json({ data: updatedField || null });
})
res.locals.payload = { data: updatedField || null };
return next();
}),
);
router.delete(
'/:collection/:field',
validateCollection,
useCollection('directus_fields'),
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FieldsService({ accountability: req.accountability });
await service.deleteField(req.params.collection, req.params.field);
res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -1,10 +1,8 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import Busboy from 'busboy';
import sanitizeQuery from '../middleware/sanitize-query';
import FilesService from '../services/files';
import MetaService from '../services/meta';
import useCollection from '../middleware/use-collection';
import { File, PrimaryKey } from '../types';
import formatTitle from '@directus/format-title';
import env from '../env';
@@ -16,8 +14,6 @@ import path from 'path';
const router = express.Router();
router.use(useCollection('directus_files'));
const multipartHandler = asyncHandler(async (req, res, next) => {
if (req.is('multipart/form-data') === false) return next();
@@ -100,9 +96,8 @@ const multipartHandler = asyncHandler(async (req, res, next) => {
router.post(
'/',
sanitizeQuery,
multipartHandler,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FilesService({ accountability: req.accountability });
let keys: PrimaryKey | PrimaryKey[] = [];
@@ -115,7 +110,8 @@ router.post(
const record = await service.readByKey(keys as any, req.sanitizedQuery);
return res.json({ data: res.locals.savedFiles.length === 1 ? record[0] : record || null });
res.locals.payload = { data: res.locals.savedFiles.length === 1 ? record[0] : record || null };
return next();
})
);
@@ -125,8 +121,7 @@ const importSchema = Joi.object({
router.post(
'/import',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const { error } = importSchema.validate(req.body);
if (error) {
@@ -154,40 +149,40 @@ router.post(
const primaryKey = await service.upload(fileResponse.data, payload);
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FilesService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
return res.json({ data: records || null, meta });
res.locals.payload = { data: records || null, meta };
return next();
})
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const keys = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
const service = new FilesService({ accountability: req.accountability });
const record = await service.readByKey(keys as any, req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.patch(
'/:pk',
sanitizeQuery,
multipartHandler,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FilesService({ accountability: req.accountability });
let keys: PrimaryKey | PrimaryKey[] = [];
@@ -199,17 +194,18 @@ router.patch(
}
const record = await service.readByKey(keys as any, req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const keys = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
const service = new FilesService({ accountability: req.accountability });
await service.delete(keys as any);
return res.status(200).end();
return next();
})
);

View File

@@ -1,70 +1,66 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import FoldersService from '../services/folders';
import MetaService from '../services/meta';
const router = express.Router();
router.use(useCollection('directus_folders'));
router.post(
'/',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FoldersService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FoldersService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
return res.json({ data: records || null, meta });
})
res.locals.payload = { data: records || null, meta };
return next();
}),
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FoldersService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
router.patch(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FoldersService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new FoldersService({ accountability: req.accountability });
await service.delete(req.params.pk);
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -1,7 +1,6 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import ItemsService from '../services/items';
import sanitizeQuery from '../middleware/sanitize-query';
import collectionExists from '../middleware/collection-exists';
import MetaService from '../services/meta';
import { RouteNotFoundException } from '../exceptions';
@@ -11,8 +10,7 @@ const router = express.Router();
router.post(
'/:collection',
collectionExists,
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (req.singleton) {
throw new RouteNotFoundException(req.path);
}
@@ -21,15 +19,15 @@ router.post(
const primaryKey = await service.create(req.body);
const result = await service.readByKey(primaryKey, req.sanitizedQuery);
res.json({ data: result || null });
})
res.locals.payload = { data: result || null };
return next();
}),
);
router.get(
'/:collection',
collectionExists,
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ItemsService(req.collection, { accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
@@ -39,18 +37,18 @@ router.get(
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
return res.json({
res.locals.payload = {
meta: meta,
data: records || null,
});
})
};
return next();
}),
);
router.get(
'/:collection/:pk',
collectionExists,
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (req.singleton) {
throw new RouteNotFoundException(req.path);
}
@@ -59,37 +57,38 @@ router.get(
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
const result = await service.readByKey(primaryKey as any, req.sanitizedQuery);
return res.json({
res.locals.payload = {
data: result || null,
});
})
};
return next();
}),
);
router.patch(
'/:collection',
collectionExists,
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ItemsService(req.collection, { accountability: req.accountability });
if (req.singleton === true) {
await service.upsertSingleton(req.body);
const item = await service.readSingleton(req.sanitizedQuery);
return res.json({ data: item || null });
res.locals.payload = { data: item || null };
return next();
}
const primaryKeys = await service.update(req.body);
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
return res.json({ data: result || null });
})
res.locals.payload = { data: result || null };
return next();
}),
);
router.patch(
'/:collection/:pk',
collectionExists,
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (req.singleton) {
throw new RouteNotFoundException(req.path);
}
@@ -100,20 +99,20 @@ router.patch(
const updatedPrimaryKey = await service.update(req.body, primaryKey as any);
const result = await service.readByKey(updatedPrimaryKey, req.sanitizedQuery);
res.json({ data: result || null });
})
res.locals.payload = { data: result || null };
return next();
}),
);
router.delete(
'/:collection/:pk',
collectionExists,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new ItemsService(req.collection, { accountability: req.accountability });
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
await service.delete(pk as any);
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -1,50 +1,47 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import PermissionsService from '../services/permissions';
import useCollection from '../middleware/use-collection';
import MetaService from '../services/meta';
import { clone } from 'lodash';
import { InvalidCredentialsException } from '../exceptions';
const router = express.Router();
router.use(useCollection('directus_permissions'));
router.post(
'/',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PermissionsService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PermissionsService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const item = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_permissions', req.sanitizedQuery);
return res.json({ data: item || null, meta });
})
res.locals.payload = { data: item || null, meta };
return next();
}),
);
router.get(
'/me',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user || !req.accountability?.role) {
throw new InvalidCredentialsException();
}
const service = new PermissionsService();
const query = req.sanitizedQuery || {};
const query = clone(req.sanitizedQuery || {});
query.filter = {
...(query.filter || {}),
@@ -55,40 +52,43 @@ router.get(
const items = await service.readByQuery(req.sanitizedQuery);
return res.json({ data: items || null });
})
res.locals.payload = { data: items || null };
return next();
}),
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (req.path.endsWith('me')) return next();
const service = new PermissionsService({ accountability: req.accountability });
const record = await service.readByKey(Number(req.params.pk), req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
router.patch(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PermissionsService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, Number(req.params.pk));
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PermissionsService({ accountability: req.accountability });
await service.delete(Number(req.params.pk));
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -1,69 +1,65 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import PresetsService from '../services/presets';
import MetaService from '../services/meta';
const router = express.Router();
router.use(useCollection('directus_presets'));
router.post(
'/',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PresetsService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PresetsService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_presets', req.sanitizedQuery);
return res.json({ data: records || null, meta });
res.locals.payload = { data: records || null, meta };
return next();
})
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PresetsService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.patch(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PresetsService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new PresetsService({ accountability: req.accountability });
await service.delete(req.params.pk);
return res.status(200).end();
return next();
})
);

View File

@@ -1,68 +1,63 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import RelationsService from '../services/relations';
import MetaService from '../services/meta';
const router = express.Router();
router.use(useCollection('directus_relations'));
router.post(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RelationsService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RelationsService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
return res.json({ data: records || null, meta });
})
res.locals.payload = { data: records || null, meta };
return next();
}),
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RelationsService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
router.patch(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RelationsService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RelationsService({ accountability: req.accountability });
await service.delete(Number(req.params.pk));
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -1,7 +1,5 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import RevisionsService from '../services/revisions';
import MetaService from '../services/meta';
@@ -9,28 +7,26 @@ const router = express.Router();
router.get(
'/',
useCollection('directus_revisions'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RevisionsService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_revisions', req.sanitizedQuery);
return res.json({ data: records || null, meta });
})
res.locals.payload = { data: records || null, meta };
return next();
}),
);
router.get(
'/:pk',
useCollection('directus_revisions'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RevisionsService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
export default router;

View File

@@ -1,68 +1,63 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import RolesService from '../services/roles';
import MetaService from '../services/meta';
const router = express.Router();
router.use(useCollection('directus_roles'));
router.post(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RolesService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RolesService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_roles', req.sanitizedQuery);
return res.json({ data: records || null, meta });
})
res.locals.payload = { data: records || null, meta };
return next();
}),
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RolesService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
return next();
}),
);
router.patch(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RolesService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new RolesService({ accountability: req.accountability });
await service.delete(req.params.pk);
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -5,10 +5,11 @@ const router = Router();
router.get('/ping', (req, res) => res.send('pong'));
router.get('/info', (req, res) => {
router.get('/info', (req, res, next) => {
const service = new ServerService({ accountability: req.accountability });
const data = service.serverInfo();
res.json({ data });
res.locals.payload = data;
return next();
});
export default router;

View File

@@ -1,32 +1,28 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import useCollection from '../middleware/use-collection';
import SettingsService from '../services/settings';
const router = express.Router();
router.get(
'/',
useCollection('directus_settings'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new SettingsService({ accountability: req.accountability });
const records = await service.readSingleton(req.sanitizedQuery);
return res.json({ data: records || null });
res.locals.payload = { data: records || null };
return next();
})
);
router.patch(
'/',
useCollection('directus_settings'),
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new SettingsService({ accountability: req.accountability });
await service.upsertSingleton(req.body);
const record = await service.readSingleton(req.sanitizedQuery);
return res.json({ data: record || null });
res.locals.payload = { data: record || null };
return next();
})
);

View File

@@ -1,46 +1,41 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import Joi from 'joi';
import { InvalidPayloadException, InvalidCredentialsException } from '../exceptions';
import useCollection from '../middleware/use-collection';
import UsersService from '../services/users';
import MetaService from '../services/meta';
import AuthService from '../services/authentication';
const router = express.Router();
router.use(useCollection('directus_users'));
router.post(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new UsersService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new UsersService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const item = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
const meta = await metaService.getMetaForQuery('directus_users', req.sanitizedQuery);
return res.json({ data: item || null, meta });
})
res.locals.payload = { data: item || null, meta };
return next();
}),
);
router.get(
'/me',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
@@ -48,24 +43,25 @@ router.get(
const item = await service.readByKey(req.accountability.user, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (req.path.endsWith('me')) return next();
const service = new UsersService({ accountability: req.accountability });
const items = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: items || null });
})
res.locals.payload = { data: items || null };
return next();
}),
);
router.patch(
'/me',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
@@ -74,14 +70,14 @@ router.patch(
const primaryKey = await service.update(req.body, req.accountability.user);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.patch(
'/me/track/page',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
@@ -93,30 +89,30 @@ router.patch(
const service = new UsersService();
await service.update({ last_page: req.body.last_page }, req.accountability.user);
return res.status(200).end();
return next();
})
);
router.patch(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new UsersService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
return next();
}),
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new UsersService({ accountability: req.accountability });
await service.delete(req.params.pk);
return res.status(200).end();
return next();
})
);
@@ -127,13 +123,13 @@ const inviteSchema = Joi.object({
router.post(
'/invite',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const { error } = inviteSchema.validate(req.body);
if (error) throw new InvalidPayloadException(error.message);
const service = new UsersService({ accountability: req.accountability });
await service.inviteUser(req.body.email, req.body.role);
res.end();
return next();
})
);
@@ -144,18 +140,18 @@ const acceptInviteSchema = Joi.object({
router.post(
'/invite/accept',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const { error } = acceptInviteSchema.validate(req.body);
if (error) throw new InvalidPayloadException(error.message);
const service = new UsersService({ accountability: req.accountability });
await service.acceptInvite(req.body.token, req.body.password);
res.end();
return next();
})
);
router.post(
'/me/tfa/enable/',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
@@ -163,13 +159,14 @@ router.post(
const service = new UsersService({ accountability: req.accountability });
const { url, secret } = await service.enableTFA(req.accountability.user);
return res.json({ data: { secret, otpauth_url: url } });
})
res.locals.payload = { data: { secret, otpauth_url: url } };
return next();
}),
);
router.post(
'/me/tfa/disable',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
@@ -188,8 +185,7 @@ router.post(
}
await service.disableTFA(req.accountability.user);
return res.status(200).end();
return next();
})
);

View File

@@ -1,69 +1,63 @@
import express from 'express';
import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import WebhooksService from '../services/webhooks';
import useCollection from '../middleware/use-collection';
import MetaService from '../services/meta';
const router = express.Router();
router.use(useCollection('directus_webhooks'));
router.post(
'/',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new WebhooksService({ accountability: req.accountability });
const primaryKey = await service.create(req.body);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
}),
);
router.get(
'/',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new WebhooksService({ accountability: req.accountability });
const metaService = new MetaService({ accountability: req.accountability });
const records = await service.readByQuery(req.sanitizedQuery);
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
return res.json({ data: records || null, meta });
})
res.locals.payload = { data: records || null, meta };
}),
);
router.get(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new WebhooksService({ accountability: req.accountability });
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
return res.json({ data: record || null });
})
res.locals.payload = { data: record || null };
}),
);
router.patch(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new WebhooksService({ accountability: req.accountability });
const primaryKey = await service.update(req.body, req.params.pk);
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
return res.json({ data: item || null });
})
res.locals.payload = { data: item || null };
}),
);
router.delete(
'/:pk',
asyncHandler(async (req, res) => {
asyncHandler(async (req, res, next) => {
const service = new WebhooksService({ accountability: req.accountability });
await service.delete(req.params.pk);
return res.status(200).end();
})
return next();
}),
);
export default router;

View File

@@ -29,6 +29,11 @@ const defaults: Record<string, any> = {
CORS_ENABLED: false,
CACHE_ENABLED: false,
CACHE_STORE: false,
CACHE_TTL: '30m',
CACHE_NAMESPACE: 'system-cache',
OAUTH_PROVIDERS: '',
EXTENSIONS_PATH: './extensions',

View File

@@ -0,0 +1,23 @@
import { RequestHandler } from 'express';
import asyncHandler from 'express-async-handler';
import env from '../env';
import { getCacheKey } from '../utils/get-cache-key';
import cache from '../cache';
const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) => {
if (req.method.toLowerCase() !== 'get') return next();
if (env.CACHE_ENABLED !== true) return next();
if (!cache) return next();
const key = getCacheKey(req);
const cachedData = await cache.get(key);
if (cachedData) {
return res.json(cachedData);
} else {
return next();
}
});
export default checkCacheMiddleware;

View File

@@ -6,7 +6,6 @@ import {
RateLimiterMemcache,
IRateLimiterOptions,
IRateLimiterStoreOptions,
RateLimiterStoreAbstract,
} from 'rate-limiter-flexible';
import env from '../env';
import { getConfigFromEnv } from '../utils/get-config-from-env';

View File

@@ -0,0 +1,14 @@
import { RequestHandler } from "express";
import asyncHandler from "express-async-handler";
import env from "../env";
import { getCacheKey } from "../utils/get-cache-key";
import cache from '../cache';
export const respond: RequestHandler = asyncHandler(async (req, res) => {
if (req.method.toLowerCase() === 'get' && env.CACHE_ENABLED === true && cache) {
const key = getCacheKey(req);
await cache.set(key, res.locals.payload);
}
return res.json(res.locals.payload);
});

View File

@@ -57,6 +57,7 @@ const sanitizeQuery: RequestHandler = (req, res, next) => {
}
req.sanitizedQuery = query;
Object.freeze(req.sanitizedQuery);
return next();
};

View File

@@ -6,6 +6,7 @@ import SchemaInspector from 'knex-schema-inspector';
import FieldsService from '../services/fields';
import { omit } from 'lodash';
import ItemsService from '../services/items';
import cache from '../cache';
export default class CollectionsService {
knex: Knex;
@@ -85,6 +86,10 @@ export default class CollectionsService {
}
});
if (cache) {
await cache.clear();
}
return Array.isArray(data) ? createdCollections : createdCollections[0];
}
@@ -233,6 +238,10 @@ export default class CollectionsService {
await collectionItemsService.update(collectionUpdates);
if (cache) {
await cache.clear();
}
return key!;
}
@@ -296,6 +305,10 @@ export default class CollectionsService {
await this.knex.schema.dropTable(collectionKey);
}
if (cache) {
await cache.clear();
}
return collection;
}
}

View File

@@ -5,10 +5,11 @@ import ItemsService from '../services/items';
import { ColumnBuilder } from 'knex';
import getLocalType from '../utils/get-local-type';
import { types } from '../types';
import { FieldNotFoundException, ForbiddenException } from '../exceptions';
import { ForbiddenException } from '../exceptions';
import Knex, { CreateTableBuilder } from 'knex';
import PayloadService from '../services/payload';
import getDefaultValue from '../utils/get-default-value';
import cache from '../cache';
type RawField = Partial<Field> & { field: string; type: typeof types[number] };
@@ -205,6 +206,10 @@ export default class FieldsService {
field: field.field,
});
}
if (cache) {
await cache.clear();
}
}
/** @todo research how to make this happen in SQLite / Redshift */
@@ -272,6 +277,10 @@ export default class FieldsService {
}
}
if (cache) {
await cache.clear();
}
return field.field;
}
@@ -309,6 +318,10 @@ export default class FieldsService {
.where({ one_collection: collection, one_field: field });
}
}
if (cache) {
await cache.clear();
}
}
public addColumnToTable(table: CreateTableBuilder, field: Field) {

View File

@@ -7,6 +7,7 @@ import parseIPTC from '../utils/parse-iptc';
import path from 'path';
import { AbstractServiceOptions, File, PrimaryKey } from '../types';
import { clone } from 'lodash';
import cache from '../cache';
export default class FilesService extends ItemsService {
constructor(options?: AbstractServiceOptions) {
@@ -77,6 +78,10 @@ export default class FilesService extends ItemsService {
const sudoService = new ItemsService('directus_files');
await sudoService.update(payload, primaryKey);
if (cache) {
await cache.clear();
}
return primaryKey;
}
@@ -97,6 +102,10 @@ export default class FilesService extends ItemsService {
await super.delete(keys);
if (cache) {
await cache.clear();
}
return key;
}
}

View File

@@ -13,6 +13,7 @@ import {
AbstractServiceOptions,
} from '../types';
import Knex from 'knex';
import cache from '../cache';
import PayloadService from './payload';
import AuthorizationService from './authorization';
@@ -145,6 +146,10 @@ export default class ItemsService implements AbstractService {
await trx.insert(revisionRecords).into('directus_revisions');
}
if (cache) {
await cache.clear();
}
return primaryKeys;
});
@@ -172,6 +177,7 @@ export default class ItemsService implements AbstractService {
query: Query = {},
action: PermissionsAction = 'read'
): Promise<Item | Item[]> {
query = clone(query);
const schemaInspector = SchemaInspector(this.knex);
const primaryKeyField = await schemaInspector.primary(this.collection);
const keys = Array.isArray(key) ? key : [key];
@@ -301,6 +307,10 @@ export default class ItemsService implements AbstractService {
}
});
if (cache) {
await cache.clear();
}
return key;
}
@@ -359,10 +369,15 @@ export default class ItemsService implements AbstractService {
}
});
if (cache) {
await cache.clear();
}
return key;
}
async readSingleton(query: Query) {
query = clone(query);
const schemaInspector = SchemaInspector(this.knex);
query.limit = 1;

View File

@@ -8,6 +8,7 @@ import { InvalidPayloadException, ForbiddenException } from '../exceptions';
import { Accountability, PrimaryKey, Item, AbstractServiceOptions } from '../types';
import Knex from 'knex';
import env from '../env';
import cache from '../cache';
export default class UsersService extends ItemsService {
knex: Knex;
@@ -42,6 +43,10 @@ export default class UsersService extends ItemsService {
}
}
if (cache) {
await cache.clear();
}
return this.service.update(data, key as any);
}
@@ -78,6 +83,10 @@ export default class UsersService extends ItemsService {
await this.knex('directus_users')
.update({ password: passwordHashed, status: 'active' })
.where({ id: user.id });
if (cache) {
await cache.clear();
}
}
async requestPasswordReset(email: string) {
@@ -114,6 +123,10 @@ export default class UsersService extends ItemsService {
await this.knex('directus_users')
.update({ password: passwordHashed, status: 'active' })
.where({ id: user.id });
if (cache) {
await cache.clear();
}
}
async enableTFA(pk: string) {

View File

@@ -12,6 +12,7 @@ import {
Accountability,
} from '../types';
import database from '../database';
import { clone } from 'lodash';
export default async function getASTFromQuery(
collection: string,
@@ -19,6 +20,7 @@ export default async function getASTFromQuery(
accountability?: Accountability | null,
action?: PermissionsAction
): Promise<AST> {
query = clone(query);
/**
* we might not need al this info at all times, but it's easier to fetch it all once, than trying to fetch it for every
* requested field. @todo look into utilizing graphql/dataloader for this purpose

View File

@@ -0,0 +1,8 @@
import { Request } from "express";
import url from 'url';
export function getCacheKey(req: Request) {
const path = url.parse(req.originalUrl).pathname;
const key = `${req.accountability?.user || 'null'}-${path}-${JSON.stringify(req.sanitizedQuery)}`;
return key;
}

View File

@@ -158,6 +158,7 @@ body {
position: absolute;
top: 0;
left: 0;
z-index: 2;
flex-basis: 220px;
flex-shrink: 0;
width: 220px;
@@ -165,7 +166,6 @@ body {
background-color: var(--background-normal);
transform: translateX(-100%);
transition: transform var(--slow) var(--transition-out);
z-index: 2;
&.active {
transform: translateX(0);
@@ -183,6 +183,7 @@ body {
@include breakpoint(medium) {
--v-overlay-z-index: none;
display: none;
}
}

View File

@@ -79,7 +79,7 @@
width: 'half',
},
schema: {
default_value: 'en-US'
default_value: 'en-US',
},
},
{
@@ -90,7 +90,7 @@
interface: 'text-input',
width: 'half',
options: {
placeholder: 'Enter a translation...'
placeholder: 'Enter a translation...',
},
},
},