directus
131 строка · 3.8 Кб
1import { InvalidPayloadError, UnsupportedMediaTypeError } from '@directus/errors';
2import { parseJSON } from '@directus/utils';
3import Busboy from 'busboy';
4import type { RequestHandler } from 'express';
5import express from 'express';
6import { load as loadYaml } from 'js-yaml';
7import { useLogger } from '../logger.js';
8import { respond } from '../middleware/respond.js';
9import { SchemaService } from '../services/schema.js';
10import type { Snapshot, SnapshotDiffWithHash } from '../types/index.js';
11import asyncHandler from '../utils/async-handler.js';
12import { getVersionedHash } from '../utils/get-versioned-hash.js';
13
14const router = express.Router();
15
16router.get(
17'/snapshot',
18asyncHandler(async (req, res, next) => {
19const service = new SchemaService({ accountability: req.accountability });
20const currentSnapshot = await service.snapshot();
21res.locals['payload'] = { data: currentSnapshot };
22return next();
23}),
24respond,
25);
26
27const schemaMultipartHandler: RequestHandler = (req, res, next) => {
28if (req.is('application/json')) {
29if (Object.keys(req.body).length === 0) {
30throw new InvalidPayloadError({ reason: `No data was included in the body` });
31}
32
33res.locals['upload'] = req.body;
34return next();
35}
36
37if (!req.is('multipart/form-data')) {
38throw new UnsupportedMediaTypeError({ mediaType: req.headers['content-type']!, where: 'Content-Type header' });
39}
40
41const headers = req.headers['content-type']
42? req.headers
43: {
44...req.headers,
45'content-type': 'application/octet-stream',
46};
47
48const busboy = Busboy({ headers });
49
50let isFileIncluded = false;
51let upload: any | null = null;
52
53busboy.on('file', async (_, fileStream, { mimeType }) => {
54const logger = useLogger();
55
56if (isFileIncluded) return next(new InvalidPayloadError({ reason: `More than one file was included in the body` }));
57
58isFileIncluded = true;
59
60const { readableStreamToString } = await import('@directus/utils/node');
61
62try {
63const uploadedString = await readableStreamToString(fileStream);
64
65if (mimeType === 'application/json') {
66try {
67upload = parseJSON(uploadedString);
68} catch (err: any) {
69logger.warn(err);
70throw new InvalidPayloadError({ reason: 'The provided JSON is invalid' });
71}
72} else {
73try {
74upload = await loadYaml(uploadedString);
75} catch (err: any) {
76logger.warn(err);
77throw new InvalidPayloadError({ reason: 'The provided YAML is invalid' });
78}
79}
80
81if (!upload) {
82throw new InvalidPayloadError({ reason: `No file was included in the body` });
83}
84
85res.locals['upload'] = upload;
86
87return next();
88} catch (error: any) {
89busboy.emit('error', error);
90}
91});
92
93busboy.on('error', (error: Error) => next(error));
94
95busboy.on('close', () => {
96if (!isFileIncluded) return next(new InvalidPayloadError({ reason: `No file was included in the body` }));
97});
98
99req.pipe(busboy);
100};
101
102router.post(
103'/diff',
104asyncHandler(schemaMultipartHandler),
105asyncHandler(async (req, res, next) => {
106const service = new SchemaService({ accountability: req.accountability });
107const snapshot: Snapshot = res.locals['upload'];
108const currentSnapshot = await service.snapshot();
109const snapshotDiff = await service.diff(snapshot, { currentSnapshot, force: 'force' in req.query });
110if (!snapshotDiff) return next();
111
112const currentSnapshotHash = getVersionedHash(currentSnapshot);
113res.locals['payload'] = { data: { hash: currentSnapshotHash, diff: snapshotDiff } };
114return next();
115}),
116respond,
117);
118
119router.post(
120'/apply',
121asyncHandler(schemaMultipartHandler),
122asyncHandler(async (req, res, next) => {
123const service = new SchemaService({ accountability: req.accountability });
124const diff: SnapshotDiffWithHash = res.locals['upload'];
125await service.apply(diff);
126return next();
127}),
128respond,
129);
130
131export default router;
132