@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
129 lines (128 loc) • 5.03 kB
JavaScript
import { InvalidPayloadError, InvalidQueryError, UnsupportedMediaTypeError } from '@directus/errors';
import argon2 from 'argon2';
import Busboy from 'busboy';
import { Router } from 'express';
import Joi from 'joi';
import collectionExists from '../middleware/collection-exists.js';
import { respond } from '../middleware/respond.js';
import { ExportService, ImportService } from '../services/import-export.js';
import { RevisionsService } from '../services/revisions.js';
import { UtilsService } from '../services/utils.js';
import asyncHandler from '../utils/async-handler.js';
import { generateHash } from '../utils/generate-hash.js';
import { sanitizeQuery } from '../utils/sanitize-query.js';
const router = Router();
const randomStringSchema = Joi.object({
length: Joi.number().integer().min(1).max(500).default(32),
});
router.get('/random/string', asyncHandler(async (req, res) => {
const { nanoid } = await import('nanoid');
const { error, value } = randomStringSchema.validate(req.query, { allowUnknown: true });
if (error)
throw new InvalidQueryError({ reason: error.message });
return res.json({ data: nanoid(value.length) });
}));
router.post('/hash/generate', asyncHandler(async (req, res) => {
if (!req.body?.string) {
throw new InvalidPayloadError({ reason: `"string" is required` });
}
const hash = await generateHash(req.body.string);
return res.json({ data: hash });
}));
router.post('/hash/verify', asyncHandler(async (req, res) => {
if (!req.body?.string) {
throw new InvalidPayloadError({ reason: `"string" is required` });
}
if (!req.body?.hash) {
throw new InvalidPayloadError({ reason: `"hash" is required` });
}
try {
const result = await argon2.verify(req.body.hash, req.body.string);
return res.json({ data: result });
}
catch {
throw new InvalidPayloadError({ reason: `Invalid "hash" or "string"` });
}
}));
const SortSchema = Joi.object({
item: Joi.alternatives(Joi.string(), Joi.number()).required(),
to: Joi.alternatives(Joi.string(), Joi.number()).required(),
});
router.post('/sort/:collection', collectionExists, asyncHandler(async (req, res) => {
const { error } = SortSchema.validate(req.body);
if (error)
throw new InvalidPayloadError({ reason: error.message });
const service = new UtilsService({
accountability: req.accountability,
schema: req.schema,
});
await service.sort(req.collection, req.body);
return res.status(200).end();
}));
router.post('/revert/:revision', asyncHandler(async (req, _res, next) => {
const service = new RevisionsService({
accountability: req.accountability,
schema: req.schema,
});
await service.revert(req.params['revision']);
next();
}), respond);
router.post('/import/:collection', collectionExists, asyncHandler(async (req, res, next) => {
if (req.is('multipart/form-data') === false) {
throw new UnsupportedMediaTypeError({ mediaType: req.headers['content-type'], where: 'Content-Type header' });
}
const service = new ImportService({
accountability: req.accountability,
schema: req.schema,
});
let headers;
if (req.headers['content-type']) {
headers = req.headers;
}
else {
headers = {
...req.headers,
'content-type': 'application/octet-stream',
};
}
const busboy = Busboy({ headers });
busboy.on('file', async (_fieldname, fileStream, { mimeType }) => {
try {
await service.import(req.params['collection'], mimeType, fileStream);
}
catch (err) {
return next(err);
}
return res.status(200).end();
});
busboy.on('error', (err) => next(err));
req.pipe(busboy);
}));
router.post('/export/:collection', collectionExists, asyncHandler(async (req, _res, next) => {
if (!req.body.query) {
throw new InvalidPayloadError({ reason: `"query" is required` });
}
if (!req.body.format) {
throw new InvalidPayloadError({ reason: `"format" is required` });
}
const service = new ExportService({
accountability: req.accountability,
schema: req.schema,
});
const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability ?? null);
// We're not awaiting this, as it's supposed to run async in the background
service.exportToFile(req.params['collection'], sanitizedQuery, req.body.format, {
file: req.body.file,
});
return next();
}), respond);
router.post('/cache/clear', asyncHandler(async (req, res) => {
const service = new UtilsService({
accountability: req.accountability,
schema: req.schema,
});
const clearSystemCache = 'system' in req.query && (req.query['system'] === '' || Boolean(req.query['system']));
await service.clearCache({ system: clearSystemCache });
res.status(200).end();
}));
export default router;