@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.
Files changed (68) hide show
  1. package/dist/app.js +4 -3
  2. package/dist/auth/drivers/oauth2.js +1 -1
  3. package/dist/auth/drivers/openid.js +1 -1
  4. package/dist/cli/utils/create-env/env-stub.liquid +7 -0
  5. package/dist/constants.d.ts +0 -1
  6. package/dist/constants.js +0 -1
  7. package/dist/controllers/assets.js +6 -10
  8. package/dist/controllers/files.js +19 -1
  9. package/dist/controllers/permissions.js +7 -4
  10. package/dist/controllers/translations.d.ts +2 -0
  11. package/dist/controllers/translations.js +149 -0
  12. package/dist/controllers/users.js +1 -1
  13. package/dist/database/migrations/20230525A-add-preview-settings.d.ts +3 -0
  14. package/dist/database/migrations/20230525A-add-preview-settings.js +10 -0
  15. package/dist/database/migrations/20230526A-migrate-translation-strings.d.ts +3 -0
  16. package/dist/database/migrations/20230526A-migrate-translation-strings.js +54 -0
  17. package/dist/database/run-ast.js +3 -3
  18. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +3 -0
  19. package/dist/database/system-data/collections/collections.yaml +23 -0
  20. package/dist/database/system-data/fields/collections.yaml +16 -0
  21. package/dist/database/system-data/fields/settings.yaml +0 -5
  22. package/dist/database/system-data/fields/translations.yaml +27 -0
  23. package/dist/env.js +17 -0
  24. package/dist/exceptions/content-too-large.d.ts +4 -0
  25. package/dist/exceptions/content-too-large.js +6 -0
  26. package/dist/extensions.js +13 -11
  27. package/dist/flows.d.ts +1 -1
  28. package/dist/flows.js +20 -19
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +6 -6
  31. package/dist/server.js +0 -11
  32. package/dist/services/assets.d.ts +2 -2
  33. package/dist/services/collections.js +8 -7
  34. package/dist/services/fields.js +7 -5
  35. package/dist/services/files.d.ts +2 -2
  36. package/dist/services/files.js +4 -9
  37. package/dist/services/graphql/index.js +4 -41
  38. package/dist/services/index.d.ts +1 -0
  39. package/dist/services/index.js +1 -0
  40. package/dist/services/items.js +10 -9
  41. package/dist/services/revisions.d.ts +6 -1
  42. package/dist/services/revisions.js +24 -0
  43. package/dist/services/server.js +3 -17
  44. package/dist/services/specifications.d.ts +2 -2
  45. package/dist/services/specifications.js +6 -5
  46. package/dist/services/translations.d.ts +10 -0
  47. package/dist/services/translations.js +36 -0
  48. package/dist/synchronization.d.ts +7 -0
  49. package/dist/synchronization.js +120 -0
  50. package/dist/types/assets.d.ts +6 -1
  51. package/dist/types/events.d.ts +2 -2
  52. package/dist/utils/apply-query.d.ts +9 -2
  53. package/dist/utils/apply-query.js +43 -16
  54. package/dist/utils/md.js +1 -1
  55. package/dist/utils/redact.d.ts +11 -0
  56. package/dist/utils/redact.js +75 -0
  57. package/dist/utils/sanitize-query.js +10 -1
  58. package/dist/utils/schedule.d.ts +5 -0
  59. package/dist/utils/schedule.js +27 -0
  60. package/dist/utils/should-clear-cache.d.ts +10 -0
  61. package/dist/utils/should-clear-cache.js +18 -0
  62. package/dist/utils/should-skip-cache.js +18 -2
  63. package/dist/utils/transformations.d.ts +2 -2
  64. package/dist/utils/transformations.js +27 -10
  65. package/dist/utils/validate-query.js +3 -1
  66. package/package.json +49 -53
  67. package/dist/utils/get-os-info.d.ts +0 -9
  68. 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:', 'https://cdn.directus.io'],
118
- mediaSrc: ["'self'", 'https://cdn.directus.io'],
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
 
@@ -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
@@ -67,4 +67,3 @@ export const SUPPORTED_IMAGE_METADATA_FORMATS = [
67
67
  'image/tiff',
68
68
  'image/avif',
69
69
  ];
70
- export const REDACT_TEXT = '--redact--';
@@ -106,21 +106,17 @@ asyncHandler(async (req, res) => {
106
106
  schema: req.schema,
107
107
  });
108
108
  const vary = ['Origin', 'Cache-Control'];
109
- const transformation = res.locals['transformation'].key
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
- if (transformation.format === 'auto') {
113
- let format;
112
+ let acceptFormat;
113
+ if (transformationParams.format === 'auto') {
114
114
  if (req.headers.accept?.includes('image/avif')) {
115
- format = 'avif';
115
+ acceptFormat = 'avif';
116
116
  }
117
117
  else if (req.headers.accept?.includes('image/webp')) {
118
- format = 'webp';
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, transformation, range);
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({ headers, defParamCharset: 'utf8' });
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(req.sanitizedQuery);
58
+ result = await service.readSingleton(temporaryQuery);
56
59
  }
57
60
  else if (req.body.keys) {
58
- result = await service.readMany(req.body.keys, req.sanitizedQuery);
61
+ result = await service.readMany(req.body.keys, temporaryQuery);
59
62
  }
60
63
  else {
61
- result = await service.readByQuery(req.sanitizedQuery);
64
+ result = await service.readByQuery(temporaryQuery);
62
65
  }
63
- const meta = await metaService.getMetaForQuery('directus_permissions', req.sanitizedQuery);
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,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -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,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -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,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -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
+ }
@@ -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 : 100;
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 ?? 100) * (nestedNode.query.page - 1));
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 ?? 100);
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
@@ -57,6 +57,9 @@
57
57
  - collection: directus_settings
58
58
  action: read
59
59
 
60
+ - collection: directus_translations
61
+ action: read
62
+
60
63
  - collection: directus_notifications
61
64
  action: read
62
65
  permissions:
@@ -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'