@directus/api 10.1.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +4 -3
- package/dist/auth/drivers/oauth2.js +1 -1
- package/dist/auth/drivers/openid.js +1 -1
- package/dist/cli/utils/create-env/env-stub.liquid +7 -0
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -1
- package/dist/controllers/assets.js +6 -10
- package/dist/controllers/files.js +19 -1
- package/dist/controllers/permissions.js +7 -4
- package/dist/controllers/translations.d.ts +2 -0
- package/dist/controllers/translations.js +149 -0
- package/dist/controllers/users.js +1 -1
- package/dist/database/migrations/20230525A-add-preview-settings.d.ts +3 -0
- package/dist/database/migrations/20230525A-add-preview-settings.js +10 -0
- package/dist/database/migrations/20230526A-migrate-translation-strings.d.ts +3 -0
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +54 -0
- package/dist/database/run-ast.js +3 -3
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +3 -0
- package/dist/database/system-data/collections/collections.yaml +23 -0
- package/dist/database/system-data/fields/collections.yaml +16 -0
- package/dist/database/system-data/fields/settings.yaml +0 -5
- package/dist/database/system-data/fields/translations.yaml +27 -0
- package/dist/env.js +17 -0
- package/dist/exceptions/content-too-large.d.ts +4 -0
- package/dist/exceptions/content-too-large.js +6 -0
- package/dist/extensions.js +13 -11
- package/dist/flows.d.ts +1 -1
- package/dist/flows.js +20 -19
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +6 -6
- package/dist/server.js +0 -11
- package/dist/services/assets.d.ts +2 -2
- package/dist/services/collections.js +8 -7
- package/dist/services/fields.js +7 -5
- package/dist/services/files.d.ts +2 -2
- package/dist/services/files.js +4 -9
- package/dist/services/graphql/index.js +4 -41
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +10 -9
- package/dist/services/revisions.d.ts +6 -1
- package/dist/services/revisions.js +24 -0
- package/dist/services/server.js +3 -17
- package/dist/services/specifications.d.ts +2 -2
- package/dist/services/specifications.js +6 -5
- package/dist/services/translations.d.ts +10 -0
- package/dist/services/translations.js +36 -0
- package/dist/synchronization.d.ts +7 -0
- package/dist/synchronization.js +120 -0
- package/dist/types/assets.d.ts +6 -1
- package/dist/types/events.d.ts +2 -2
- package/dist/utils/apply-query.d.ts +9 -2
- package/dist/utils/apply-query.js +43 -16
- package/dist/utils/md.js +1 -1
- package/dist/utils/redact.d.ts +11 -0
- package/dist/utils/redact.js +75 -0
- package/dist/utils/sanitize-query.js +10 -1
- package/dist/utils/schedule.d.ts +5 -0
- package/dist/utils/schedule.js +27 -0
- package/dist/utils/should-clear-cache.d.ts +10 -0
- package/dist/utils/should-clear-cache.js +18 -0
- package/dist/utils/should-skip-cache.js +18 -2
- package/dist/utils/transformations.d.ts +2 -2
- package/dist/utils/transformations.js +27 -10
- package/dist/utils/validate-query.js +3 -1
- package/package.json +49 -53
- package/dist/utils/get-os-info.d.ts +0 -9
- package/dist/utils/get-os-info.js +0 -40
package/dist/app.js
CHANGED
|
@@ -32,6 +32,7 @@ import schemaRouter from './controllers/schema.js';
|
|
|
32
32
|
import serverRouter from './controllers/server.js';
|
|
33
33
|
import settingsRouter from './controllers/settings.js';
|
|
34
34
|
import sharesRouter from './controllers/shares.js';
|
|
35
|
+
import translationsRouter from './controllers/translations.js';
|
|
35
36
|
import usersRouter from './controllers/users.js';
|
|
36
37
|
import utilsRouter from './controllers/utils.js';
|
|
37
38
|
import webhooksRouter from './controllers/webhooks.js';
|
|
@@ -111,11 +112,10 @@ export default async function createApp() {
|
|
|
111
112
|
// friendly. Ref #10806
|
|
112
113
|
upgradeInsecureRequests: null,
|
|
113
114
|
// These are required for MapLibre
|
|
114
|
-
// https://cdn.directus.io is required for images/videos in the official docs
|
|
115
115
|
workerSrc: ["'self'", 'blob:'],
|
|
116
116
|
childSrc: ["'self'", 'blob:'],
|
|
117
|
-
imgSrc: ["'self'", 'data:', 'blob:'
|
|
118
|
-
mediaSrc: ["'self'"
|
|
117
|
+
imgSrc: ["'self'", 'data:', 'blob:'],
|
|
118
|
+
mediaSrc: ["'self'"],
|
|
119
119
|
connectSrc: ["'self'", 'https://*'],
|
|
120
120
|
},
|
|
121
121
|
}, getConfigFromEnv('CONTENT_SECURITY_POLICY_'))));
|
|
@@ -213,6 +213,7 @@ export default async function createApp() {
|
|
|
213
213
|
app.use('/panels', panelsRouter);
|
|
214
214
|
app.use('/permissions', permissionsRouter);
|
|
215
215
|
app.use('/presets', presetsRouter);
|
|
216
|
+
app.use('/translations', translationsRouter);
|
|
216
217
|
app.use('/relations', relationsRouter);
|
|
217
218
|
app.use('/revisions', revisionsRouter);
|
|
218
219
|
app.use('/roles', rolesRouter);
|
|
@@ -117,7 +117,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
117
117
|
if (userId) {
|
|
118
118
|
// Run hook so the end user has the chance to augment the
|
|
119
119
|
// user that is about to be updated
|
|
120
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
|
|
120
|
+
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data ?? null }, {
|
|
121
121
|
identifier,
|
|
122
122
|
provider: this.config['provider'],
|
|
123
123
|
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
@@ -136,7 +136,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
136
136
|
if (userId) {
|
|
137
137
|
// Run hook so the end user has the chance to augment the
|
|
138
138
|
// user that is about to be updated
|
|
139
|
-
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
|
|
139
|
+
const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data ?? null }, {
|
|
140
140
|
identifier,
|
|
141
141
|
provider: this.config['provider'],
|
|
142
142
|
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
@@ -43,6 +43,10 @@ PUBLIC_URL="/"
|
|
|
43
43
|
# Whether or not to enable GraphQL Introspection [true]
|
|
44
44
|
# GRAPHQL_INTROSPECTION=true
|
|
45
45
|
|
|
46
|
+
# Limit the maximum amount of items that can get requested in one query.
|
|
47
|
+
# QUERY_LIMIT_DEFAULT=100
|
|
48
|
+
# QUERY_LIMIT_MAX=Infinity
|
|
49
|
+
|
|
46
50
|
# The maximum number of items for batch mutations when creating, updating and deleting. ["Infinity"]
|
|
47
51
|
# MAX_BATCH_MUTATION="Infinity"
|
|
48
52
|
|
|
@@ -149,6 +153,9 @@ CACHE_ENABLED=false
|
|
|
149
153
|
# Automatically purge the cache on create, update, and delete actions. [false]
|
|
150
154
|
# CACHE_AUTO_PURGE=true
|
|
151
155
|
|
|
156
|
+
# List of collections that prevent cache purging when `CACHE_AUTO_PURGE` is enabled. ["directus_activity,directus_presets"]
|
|
157
|
+
# CACHE_AUTO_PURGE_IGNORE_LIST="directus_activity,directus_presets"
|
|
158
|
+
|
|
152
159
|
# memory | redis | memcache
|
|
153
160
|
CACHE_STORE=memory
|
|
154
161
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -14,4 +14,3 @@ export declare const OAS_REQUIRED_SCHEMAS: string[];
|
|
|
14
14
|
export declare const SUPPORTED_IMAGE_TRANSFORM_FORMATS: string[];
|
|
15
15
|
/** Formats where metadata extraction is supported */
|
|
16
16
|
export declare const SUPPORTED_IMAGE_METADATA_FORMATS: string[];
|
|
17
|
-
export declare const REDACT_TEXT = "--redact--";
|
package/dist/constants.js
CHANGED
|
@@ -106,21 +106,17 @@ asyncHandler(async (req, res) => {
|
|
|
106
106
|
schema: req.schema,
|
|
107
107
|
});
|
|
108
108
|
const vary = ['Origin', 'Cache-Control'];
|
|
109
|
-
const
|
|
109
|
+
const transformationParams = res.locals['transformation'].key
|
|
110
110
|
? res.locals['shortcuts'].find((transformation) => transformation['key'] === res.locals['transformation'].key)
|
|
111
111
|
: res.locals['transformation'];
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
let acceptFormat;
|
|
113
|
+
if (transformationParams.format === 'auto') {
|
|
114
114
|
if (req.headers.accept?.includes('image/avif')) {
|
|
115
|
-
|
|
115
|
+
acceptFormat = 'avif';
|
|
116
116
|
}
|
|
117
117
|
else if (req.headers.accept?.includes('image/webp')) {
|
|
118
|
-
|
|
118
|
+
acceptFormat = 'webp';
|
|
119
119
|
}
|
|
120
|
-
else {
|
|
121
|
-
format = 'jpg';
|
|
122
|
-
}
|
|
123
|
-
transformation.format = format;
|
|
124
120
|
vary.push('Accept');
|
|
125
121
|
}
|
|
126
122
|
let range = undefined;
|
|
@@ -140,7 +136,7 @@ asyncHandler(async (req, res) => {
|
|
|
140
136
|
}
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
|
-
const { stream, file, stat } = await service.getAsset(id,
|
|
139
|
+
const { stream, file, stat } = await service.getAsset(id, { transformationParams, acceptFormat }, range);
|
|
144
140
|
const filename = req.params['filename'] ?? file.filename_download;
|
|
145
141
|
res.attachment(filename);
|
|
146
142
|
res.setHeader('Content-Type', file.type);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import formatTitle from '@directus/format-title';
|
|
2
2
|
import { toArray } from '@directus/utils';
|
|
3
3
|
import Busboy from 'busboy';
|
|
4
|
+
import bytes from 'bytes';
|
|
4
5
|
import express from 'express';
|
|
5
6
|
import Joi from 'joi';
|
|
7
|
+
import { minimatch } from 'minimatch';
|
|
6
8
|
import path from 'path';
|
|
7
9
|
import env from '../env.js';
|
|
10
|
+
import { ContentTooLargeException } from '../exceptions/content-too-large.js';
|
|
8
11
|
import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
|
|
9
12
|
import { respond } from '../middleware/respond.js';
|
|
10
13
|
import useCollection from '../middleware/use-collection.js';
|
|
@@ -28,7 +31,13 @@ export const multipartHandler = (req, res, next) => {
|
|
|
28
31
|
'content-type': 'application/octet-stream',
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
|
-
const busboy = Busboy({
|
|
34
|
+
const busboy = Busboy({
|
|
35
|
+
headers,
|
|
36
|
+
defParamCharset: 'utf8',
|
|
37
|
+
limits: {
|
|
38
|
+
fileSize: env['FILES_MAX_UPLOAD_SIZE'] ? bytes(env['FILES_MAX_UPLOAD_SIZE']) : undefined,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
32
41
|
const savedFiles = [];
|
|
33
42
|
const service = new FilesService({ accountability: req.accountability, schema: req.schema });
|
|
34
43
|
const existingPrimaryKey = req.params['pk'] || undefined;
|
|
@@ -57,6 +66,11 @@ export const multipartHandler = (req, res, next) => {
|
|
|
57
66
|
if (!filename) {
|
|
58
67
|
return busboy.emit('error', new InvalidPayloadException(`File is missing filename`));
|
|
59
68
|
}
|
|
69
|
+
const allowedPatterns = toArray(env['FILES_MIME_TYPE_ALLOW_LIST']);
|
|
70
|
+
const mimeTypeAllowed = allowedPatterns.some((pattern) => minimatch(mimeType, pattern));
|
|
71
|
+
if (mimeTypeAllowed === false) {
|
|
72
|
+
return busboy.emit('error', new InvalidPayloadException(`File is of invalid content type`));
|
|
73
|
+
}
|
|
60
74
|
fileCount++;
|
|
61
75
|
if (!existingPrimaryKey) {
|
|
62
76
|
if (!payload.title) {
|
|
@@ -71,6 +85,10 @@ export const multipartHandler = (req, res, next) => {
|
|
|
71
85
|
};
|
|
72
86
|
// Clear the payload for the next to-be-uploaded file
|
|
73
87
|
payload = {};
|
|
88
|
+
fileStream.on('limit', () => {
|
|
89
|
+
const error = new ContentTooLargeException(`Uploaded file is too large`);
|
|
90
|
+
next(error);
|
|
91
|
+
});
|
|
74
92
|
try {
|
|
75
93
|
const primaryKey = await service.uploadOne(fileStream, payloadWithRequiredFields, existingPrimaryKey);
|
|
76
94
|
savedFiles.push(primaryKey);
|
|
@@ -51,16 +51,19 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
|
|
51
51
|
schema: req.schema,
|
|
52
52
|
});
|
|
53
53
|
let result;
|
|
54
|
+
// TODO fix this at the service level
|
|
55
|
+
// temporary fix for missing permissions https://github.com/directus/directus/issues/18654
|
|
56
|
+
const temporaryQuery = { ...req.sanitizedQuery, limit: -1 };
|
|
54
57
|
if (req.singleton) {
|
|
55
|
-
result = await service.readSingleton(
|
|
58
|
+
result = await service.readSingleton(temporaryQuery);
|
|
56
59
|
}
|
|
57
60
|
else if (req.body.keys) {
|
|
58
|
-
result = await service.readMany(req.body.keys,
|
|
61
|
+
result = await service.readMany(req.body.keys, temporaryQuery);
|
|
59
62
|
}
|
|
60
63
|
else {
|
|
61
|
-
result = await service.readByQuery(
|
|
64
|
+
result = await service.readByQuery(temporaryQuery);
|
|
62
65
|
}
|
|
63
|
-
const meta = await metaService.getMetaForQuery('directus_permissions',
|
|
66
|
+
const meta = await metaService.getMetaForQuery('directus_permissions', temporaryQuery);
|
|
64
67
|
res.locals['payload'] = { data: result, meta };
|
|
65
68
|
return next();
|
|
66
69
|
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { ForbiddenException } from '../exceptions/index.js';
|
|
3
|
+
import { respond } from '../middleware/respond.js';
|
|
4
|
+
import useCollection from '../middleware/use-collection.js';
|
|
5
|
+
import { validateBatch } from '../middleware/validate-batch.js';
|
|
6
|
+
import { TranslationsService } from '../services/translations.js';
|
|
7
|
+
import { MetaService } from '../services/meta.js';
|
|
8
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
9
|
+
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
router.use(useCollection('directus_translations'));
|
|
12
|
+
router.post('/', asyncHandler(async (req, res, next) => {
|
|
13
|
+
const service = new TranslationsService({
|
|
14
|
+
accountability: req.accountability,
|
|
15
|
+
schema: req.schema,
|
|
16
|
+
});
|
|
17
|
+
const savedKeys = [];
|
|
18
|
+
if (Array.isArray(req.body)) {
|
|
19
|
+
const keys = await service.createMany(req.body);
|
|
20
|
+
savedKeys.push(...keys);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const primaryKey = await service.createOne(req.body);
|
|
24
|
+
savedKeys.push(primaryKey);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (Array.isArray(req.body)) {
|
|
28
|
+
const records = await service.readMany(savedKeys, req.sanitizedQuery);
|
|
29
|
+
res.locals['payload'] = { data: records };
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const record = await service.readOne(savedKeys[0], req.sanitizedQuery);
|
|
33
|
+
res.locals['payload'] = { data: record };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof ForbiddenException) {
|
|
38
|
+
return next();
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
return next();
|
|
43
|
+
}), respond);
|
|
44
|
+
const readHandler = asyncHandler(async (req, res, next) => {
|
|
45
|
+
const service = new TranslationsService({
|
|
46
|
+
accountability: req.accountability,
|
|
47
|
+
schema: req.schema,
|
|
48
|
+
});
|
|
49
|
+
const metaService = new MetaService({
|
|
50
|
+
accountability: req.accountability,
|
|
51
|
+
schema: req.schema,
|
|
52
|
+
});
|
|
53
|
+
let result;
|
|
54
|
+
if (req.singleton) {
|
|
55
|
+
result = await service.readSingleton(req.sanitizedQuery);
|
|
56
|
+
}
|
|
57
|
+
else if (req.body.keys) {
|
|
58
|
+
result = await service.readMany(req.body.keys, req.sanitizedQuery);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
result = await service.readByQuery(req.sanitizedQuery);
|
|
62
|
+
}
|
|
63
|
+
const meta = await metaService.getMetaForQuery('directus_translations', req.sanitizedQuery);
|
|
64
|
+
res.locals['payload'] = { data: result, meta };
|
|
65
|
+
return next();
|
|
66
|
+
});
|
|
67
|
+
router.get('/', validateBatch('read'), readHandler, respond);
|
|
68
|
+
router.search('/', validateBatch('read'), readHandler, respond);
|
|
69
|
+
router.get('/:pk', asyncHandler(async (req, res, next) => {
|
|
70
|
+
const service = new TranslationsService({
|
|
71
|
+
accountability: req.accountability,
|
|
72
|
+
schema: req.schema,
|
|
73
|
+
});
|
|
74
|
+
const record = await service.readOne(req.params['pk'], req.sanitizedQuery);
|
|
75
|
+
res.locals['payload'] = { data: record || null };
|
|
76
|
+
return next();
|
|
77
|
+
}), respond);
|
|
78
|
+
router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
|
|
79
|
+
const service = new TranslationsService({
|
|
80
|
+
accountability: req.accountability,
|
|
81
|
+
schema: req.schema,
|
|
82
|
+
});
|
|
83
|
+
let keys = [];
|
|
84
|
+
if (Array.isArray(req.body)) {
|
|
85
|
+
keys = await service.updateBatch(req.body);
|
|
86
|
+
}
|
|
87
|
+
else if (req.body.keys) {
|
|
88
|
+
keys = await service.updateMany(req.body.keys, req.body.data);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
92
|
+
keys = await service.updateByQuery(sanitizedQuery, req.body.data);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const result = await service.readMany(keys, req.sanitizedQuery);
|
|
96
|
+
res.locals['payload'] = { data: result || null };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error instanceof ForbiddenException) {
|
|
100
|
+
return next();
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return next();
|
|
105
|
+
}), respond);
|
|
106
|
+
router.patch('/:pk', asyncHandler(async (req, res, next) => {
|
|
107
|
+
const service = new TranslationsService({
|
|
108
|
+
accountability: req.accountability,
|
|
109
|
+
schema: req.schema,
|
|
110
|
+
});
|
|
111
|
+
const primaryKey = await service.updateOne(req.params['pk'], req.body);
|
|
112
|
+
try {
|
|
113
|
+
const record = await service.readOne(primaryKey, req.sanitizedQuery);
|
|
114
|
+
res.locals['payload'] = { data: record || null };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof ForbiddenException) {
|
|
118
|
+
return next();
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return next();
|
|
123
|
+
}), respond);
|
|
124
|
+
router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next) => {
|
|
125
|
+
const service = new TranslationsService({
|
|
126
|
+
accountability: req.accountability,
|
|
127
|
+
schema: req.schema,
|
|
128
|
+
});
|
|
129
|
+
if (Array.isArray(req.body)) {
|
|
130
|
+
await service.deleteMany(req.body);
|
|
131
|
+
}
|
|
132
|
+
else if (req.body.keys) {
|
|
133
|
+
await service.deleteMany(req.body.keys);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
137
|
+
await service.deleteByQuery(sanitizedQuery);
|
|
138
|
+
}
|
|
139
|
+
return next();
|
|
140
|
+
}), respond);
|
|
141
|
+
router.delete('/:pk', asyncHandler(async (req, _res, next) => {
|
|
142
|
+
const service = new TranslationsService({
|
|
143
|
+
accountability: req.accountability,
|
|
144
|
+
schema: req.schema,
|
|
145
|
+
});
|
|
146
|
+
await service.deleteOne(req.params['pk']);
|
|
147
|
+
return next();
|
|
148
|
+
}), respond);
|
|
149
|
+
export default router;
|
|
@@ -126,7 +126,7 @@ router.patch('/me/track/page', asyncHandler(async (req, _res, next) => {
|
|
|
126
126
|
throw new InvalidPayloadException(`"last_page" key is required.`);
|
|
127
127
|
}
|
|
128
128
|
const service = new UsersService({ schema: req.schema });
|
|
129
|
-
await service.updateOne(req.accountability.user, { last_page: req.body.last_page });
|
|
129
|
+
await service.updateOne(req.accountability.user, { last_page: req.body.last_page }, { autoPurgeCache: false });
|
|
130
130
|
return next();
|
|
131
131
|
}), respond);
|
|
132
132
|
router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_collections', (table) => {
|
|
3
|
+
table.string('preview_url').nullable();
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export async function down(knex) {
|
|
7
|
+
await knex.schema.alterTable('directus_collections', (table) => {
|
|
8
|
+
table.dropColumn('preview_url');
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { set } from 'lodash-es';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
function transformStringsNewFormat(oldStrings) {
|
|
4
|
+
return oldStrings.reduce((result, item) => {
|
|
5
|
+
if (!item.key || !item.translations)
|
|
6
|
+
return result;
|
|
7
|
+
for (const [language, value] of Object.entries(item.translations)) {
|
|
8
|
+
result.push({ id: uuid(), key: item.key, language, value });
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}, []);
|
|
12
|
+
}
|
|
13
|
+
function transformStringsOldFormat(newStrings) {
|
|
14
|
+
const keyCache = {};
|
|
15
|
+
for (const { key, language, value } of newStrings) {
|
|
16
|
+
set(keyCache, [key, language], value);
|
|
17
|
+
}
|
|
18
|
+
return Object.entries(keyCache).map(([key, translations]) => ({ key, translations }));
|
|
19
|
+
}
|
|
20
|
+
export async function up(knex) {
|
|
21
|
+
await knex.schema.createTable('directus_translations', (table) => {
|
|
22
|
+
table.uuid('id').primary().notNullable();
|
|
23
|
+
table.string('language').notNullable();
|
|
24
|
+
table.string('key').notNullable();
|
|
25
|
+
table.text('value').notNullable();
|
|
26
|
+
});
|
|
27
|
+
const data = await knex.select('translation_strings', 'id').from('directus_settings').first();
|
|
28
|
+
if (data?.translation_strings && data?.id) {
|
|
29
|
+
const parsedTranslationStrings = typeof data.translation_strings === 'string' ? JSON.parse(data.translation_strings) : data.translation_strings;
|
|
30
|
+
const newTranslationStrings = transformStringsNewFormat(parsedTranslationStrings);
|
|
31
|
+
for (const item of newTranslationStrings) {
|
|
32
|
+
await knex('directus_translations').insert(item);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
36
|
+
table.dropColumn('translation_strings');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function down(knex) {
|
|
40
|
+
const data = await knex.select('language', 'key', 'value').from('directus_translations');
|
|
41
|
+
const settingsId = await knex.select('id').from('directus_settings').first();
|
|
42
|
+
await knex.schema.alterTable('directus_settings', (table) => {
|
|
43
|
+
table.json('translation_strings');
|
|
44
|
+
});
|
|
45
|
+
if (settingsId?.id && data) {
|
|
46
|
+
const oldTranslationStrings = transformStringsOldFormat(data);
|
|
47
|
+
await knex('directus_settings')
|
|
48
|
+
.where({ id: settingsId.id })
|
|
49
|
+
.update({
|
|
50
|
+
translation_strings: JSON.stringify(oldTranslationStrings),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
await knex.schema.dropTable('directus_translations');
|
|
54
|
+
}
|
package/dist/database/run-ast.js
CHANGED
|
@@ -151,7 +151,7 @@ async function getDBQuery(schema, knex, table, fieldNodes, query) {
|
|
|
151
151
|
const preProcess = getColumnPreprocessor(knex, schema, table);
|
|
152
152
|
const queryCopy = clone(query);
|
|
153
153
|
const helpers = getHelpers(knex);
|
|
154
|
-
queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit :
|
|
154
|
+
queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit : Number(env['QUERY_LIMIT_DEFAULT']);
|
|
155
155
|
// Queries with aggregates and groupBy will not have duplicate result
|
|
156
156
|
if (queryCopy.aggregate || queryCopy.group) {
|
|
157
157
|
const flatQuery = knex.select(fieldNodes.map(preProcess)).from(table);
|
|
@@ -322,13 +322,13 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode) {
|
|
|
322
322
|
});
|
|
323
323
|
parentItem[nestedNode.fieldKey].push(...itemChildren);
|
|
324
324
|
if (nestedNode.query.page && nestedNode.query.page > 1) {
|
|
325
|
-
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice((nestedNode.query.limit ??
|
|
325
|
+
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice((nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT'])) * (nestedNode.query.page - 1));
|
|
326
326
|
}
|
|
327
327
|
if (nestedNode.query.offset && nestedNode.query.offset >= 0) {
|
|
328
328
|
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(nestedNode.query.offset);
|
|
329
329
|
}
|
|
330
330
|
if (nestedNode.query.limit !== -1) {
|
|
331
|
-
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0, nestedNode.query.limit ??
|
|
331
|
+
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0, nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT']));
|
|
332
332
|
}
|
|
333
333
|
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].sort((a, b) => {
|
|
334
334
|
// This is pre-filled in get-ast-from-query
|
|
@@ -14,42 +14,55 @@ data:
|
|
|
14
14
|
- collection: directus_activity
|
|
15
15
|
note: $t:directus_collection.directus_activity
|
|
16
16
|
accountability: null
|
|
17
|
+
|
|
17
18
|
- collection: directus_collections
|
|
18
19
|
icon: list_alt
|
|
19
20
|
note: $t:directus_collection.directus_collections
|
|
21
|
+
|
|
20
22
|
- collection: directus_fields
|
|
21
23
|
icon: input
|
|
22
24
|
note: $t:directus_collection.directus_fields
|
|
25
|
+
|
|
23
26
|
- collection: directus_files
|
|
24
27
|
icon: folder
|
|
25
28
|
note: $t:directus_collection.directus_files
|
|
26
29
|
display_template: '{{ $thumbnail }} {{ title }}'
|
|
30
|
+
|
|
27
31
|
- collection: directus_folders
|
|
28
32
|
note: $t:directus_collection.directus_folders
|
|
29
33
|
display_template: '{{ name }}'
|
|
34
|
+
|
|
30
35
|
- collection: directus_migrations
|
|
31
36
|
note: $t:directus_collection.directus_migrations
|
|
37
|
+
|
|
32
38
|
- collection: directus_permissions
|
|
33
39
|
icon: admin_panel_settings
|
|
34
40
|
note: $t:directus_collection.directus_permissions
|
|
41
|
+
|
|
35
42
|
- collection: directus_presets
|
|
36
43
|
icon: bookmark
|
|
37
44
|
note: $t:directus_collection.directus_presets
|
|
38
45
|
accountability: null
|
|
46
|
+
|
|
39
47
|
- collection: directus_relations
|
|
40
48
|
icon: merge_type
|
|
41
49
|
note: $t:directus_collection.directus_relations
|
|
50
|
+
|
|
42
51
|
- collection: directus_revisions
|
|
43
52
|
note: $t:directus_collection.directus_revisions
|
|
44
53
|
accountability: null
|
|
54
|
+
|
|
45
55
|
- collection: directus_roles
|
|
46
56
|
icon: supervised_user_circle
|
|
47
57
|
note: $t:directus_collection.directus_roles
|
|
58
|
+
|
|
48
59
|
- collection: directus_sessions
|
|
49
60
|
note: $t:directus_collection.directus_sessions
|
|
61
|
+
|
|
50
62
|
- collection: directus_settings
|
|
51
63
|
singleton: true
|
|
52
64
|
note: $t:directus_collection.directus_settings
|
|
65
|
+
|
|
53
66
|
- collection: directus_users
|
|
54
67
|
archive_field: status
|
|
55
68
|
archive_value: archived
|
|
@@ -57,18 +70,28 @@ data:
|
|
|
57
70
|
icon: people_alt
|
|
58
71
|
note: $t:directus_collection.directus_users
|
|
59
72
|
display_template: '{{ first_name }} {{ last_name }}'
|
|
73
|
+
|
|
60
74
|
- collection: directus_webhooks
|
|
61
75
|
note: $t:directus_collection.directus_webhooks
|
|
76
|
+
|
|
62
77
|
- collection: directus_dashboards
|
|
63
78
|
note: $t:directus_collection.directus_dashboards
|
|
79
|
+
|
|
64
80
|
- collection: directus_panels
|
|
65
81
|
note: $t:directus_collection.directus_panels
|
|
82
|
+
|
|
66
83
|
- collection: directus_notifications
|
|
67
84
|
note: $t:directus_collection.directus_notifications
|
|
85
|
+
|
|
68
86
|
- collection: directus_shares
|
|
69
87
|
icon: share
|
|
70
88
|
note: $t:directus_collection.directus_shares
|
|
89
|
+
|
|
71
90
|
- collection: directus_flows
|
|
72
91
|
note: $t:directus_collection.directus_flows
|
|
92
|
+
|
|
73
93
|
- collection: directus_operations
|
|
74
94
|
note: $t:directus_collection.directus_operations
|
|
95
|
+
|
|
96
|
+
- collection: directus_translations
|
|
97
|
+
note: $t:directus_collection.directus_translations
|
|
@@ -100,6 +100,22 @@ fields:
|
|
|
100
100
|
placeholder: $t:field_options.directus_collections.translation_placeholder
|
|
101
101
|
width: full
|
|
102
102
|
|
|
103
|
+
- field: preview_divider
|
|
104
|
+
special:
|
|
105
|
+
- alias
|
|
106
|
+
- no-data
|
|
107
|
+
interface: presentation-divider
|
|
108
|
+
options:
|
|
109
|
+
icon: preview
|
|
110
|
+
title: $t:field_options.directus_collections.preview_divider
|
|
111
|
+
width: full
|
|
112
|
+
|
|
113
|
+
- field: preview_url
|
|
114
|
+
interface: system-display-template
|
|
115
|
+
options:
|
|
116
|
+
collectionField: collection
|
|
117
|
+
width: full
|
|
118
|
+
|
|
103
119
|
- field: archive_divider
|
|
104
120
|
special:
|
|
105
121
|
- alias
|
|
@@ -375,11 +375,6 @@ fields:
|
|
|
375
375
|
options:
|
|
376
376
|
placeholder: $t:fields.directus_settings.attribution_placeholder
|
|
377
377
|
|
|
378
|
-
- field: translation_strings
|
|
379
|
-
special:
|
|
380
|
-
- cast-json
|
|
381
|
-
hidden: true
|
|
382
|
-
|
|
383
378
|
- field: image_editor
|
|
384
379
|
interface: presentation-divider
|
|
385
380
|
options:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
table: directus_translations
|
|
2
|
+
|
|
3
|
+
fields:
|
|
4
|
+
- field: id
|
|
5
|
+
hidden: true
|
|
6
|
+
sort: 1
|
|
7
|
+
special:
|
|
8
|
+
- uuid
|
|
9
|
+
- field: key
|
|
10
|
+
width: half
|
|
11
|
+
sort: 2
|
|
12
|
+
required: true
|
|
13
|
+
interface: input
|
|
14
|
+
options:
|
|
15
|
+
font: monospace
|
|
16
|
+
placeholder: '$t:translation_key_placeholder'
|
|
17
|
+
- field: language
|
|
18
|
+
interface: system-language
|
|
19
|
+
width: half
|
|
20
|
+
sort: 3
|
|
21
|
+
required: true
|
|
22
|
+
- field: value
|
|
23
|
+
interface: input-multiline
|
|
24
|
+
sort: 4
|
|
25
|
+
required: true
|
|
26
|
+
options:
|
|
27
|
+
placeholder: '$t:enter_a_value'
|