@directus/api 25.0.1 → 26.0.1

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 (115) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/auth/drivers/oauth2.d.ts +2 -0
  3. package/dist/auth/drivers/oauth2.js +40 -2
  4. package/dist/auth/drivers/openid.js +8 -1
  5. package/dist/controllers/access.js +2 -2
  6. package/dist/controllers/comments.js +2 -2
  7. package/dist/controllers/dashboards.js +2 -2
  8. package/dist/controllers/files.js +2 -2
  9. package/dist/controllers/flows.js +2 -2
  10. package/dist/controllers/folders.js +2 -2
  11. package/dist/controllers/items.js +2 -2
  12. package/dist/controllers/notifications.js +2 -2
  13. package/dist/controllers/operations.js +2 -2
  14. package/dist/controllers/panels.js +2 -2
  15. package/dist/controllers/permissions.js +2 -2
  16. package/dist/controllers/policies.js +2 -2
  17. package/dist/controllers/presets.js +2 -2
  18. package/dist/controllers/roles.js +2 -2
  19. package/dist/controllers/shares.js +2 -2
  20. package/dist/controllers/translations.js +2 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/controllers/utils.js +8 -3
  23. package/dist/controllers/versions.js +2 -2
  24. package/dist/controllers/webhooks.js +1 -1
  25. package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
  26. package/dist/database/helpers/capabilities/dialects/default.js +3 -0
  27. package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
  28. package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
  29. package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
  30. package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
  31. package/dist/database/helpers/capabilities/index.d.ts +7 -0
  32. package/dist/database/helpers/capabilities/index.js +7 -0
  33. package/dist/database/helpers/capabilities/types.d.ts +11 -0
  34. package/dist/database/helpers/capabilities/types.js +15 -0
  35. package/dist/database/helpers/index.d.ts +2 -0
  36. package/dist/database/helpers/index.js +2 -0
  37. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
  38. package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
  39. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
  40. package/dist/database/helpers/schema/dialects/postgres.js +0 -4
  41. package/dist/database/index.js +1 -1
  42. package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
  43. package/dist/database/migrations/20250224A-visual-editor.js +35 -0
  44. package/dist/database/run-ast/lib/get-db-query.js +16 -4
  45. package/dist/logger/index.js +3 -3
  46. package/dist/middleware/sanitize-query.js +17 -7
  47. package/dist/middleware/validate-batch.js +1 -1
  48. package/dist/operations/item-delete/index.js +1 -1
  49. package/dist/operations/item-read/index.js +1 -1
  50. package/dist/operations/item-update/index.js +1 -1
  51. package/dist/permissions/lib/fetch-permissions.js +6 -4
  52. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
  53. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
  54. package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
  55. package/dist/permissions/modules/process-payload/process-payload.js +19 -4
  56. package/dist/permissions/types.d.ts +2 -1
  57. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
  58. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
  59. package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
  60. package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +13 -12
  61. package/dist/rate-limiter.js +1 -1
  62. package/dist/services/assets.js +12 -2
  63. package/dist/services/authentication.js +2 -2
  64. package/dist/services/collections.js +8 -2
  65. package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
  66. package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
  67. package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
  68. package/dist/services/graphql/resolvers/get-field-type.js +51 -0
  69. package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
  70. package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
  71. package/dist/services/graphql/resolvers/mutation.js +1 -1
  72. package/dist/services/graphql/resolvers/query.js +4 -4
  73. package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
  74. package/dist/services/graphql/resolvers/system-admin.js +207 -199
  75. package/dist/services/graphql/resolvers/system.d.ts +1 -7
  76. package/dist/services/graphql/resolvers/system.js +12 -113
  77. package/dist/services/graphql/schema/index.js +1 -1
  78. package/dist/services/graphql/schema/parse-query.d.ts +2 -2
  79. package/dist/services/graphql/schema/parse-query.js +6 -6
  80. package/dist/services/graphql/schema/read.d.ts +2 -2
  81. package/dist/services/graphql/schema/read.js +86 -2
  82. package/dist/services/graphql/schema-cache.d.ts +2 -2
  83. package/dist/services/graphql/schema-cache.js +1 -3
  84. package/dist/services/graphql/subscription.d.ts +3 -3
  85. package/dist/services/graphql/subscription.js +3 -3
  86. package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
  87. package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
  88. package/dist/services/items.d.ts +1 -0
  89. package/dist/services/items.js +30 -16
  90. package/dist/services/payload.d.ts +1 -0
  91. package/dist/services/payload.js +32 -17
  92. package/dist/services/shares.js +1 -1
  93. package/dist/services/specifications.js +10 -5
  94. package/dist/services/tus/lockers.d.ts +1 -1
  95. package/dist/services/tus/lockers.js +6 -5
  96. package/dist/services/tus/server.js +24 -0
  97. package/dist/services/users.js +1 -0
  98. package/dist/types/services.d.ts +2 -0
  99. package/dist/utils/apply-query.d.ts +1 -0
  100. package/dist/utils/apply-query.js +21 -12
  101. package/dist/utils/generate-hash.js +1 -1
  102. package/dist/utils/get-config-from-env.d.ts +6 -1
  103. package/dist/utils/get-config-from-env.js +16 -11
  104. package/dist/utils/get-graphql-type.js +3 -1
  105. package/dist/utils/is-login-redirect-allowed.js +2 -0
  106. package/dist/utils/redact-object.js +5 -1
  107. package/dist/utils/sanitize-query.d.ts +5 -2
  108. package/dist/utils/sanitize-query.js +34 -9
  109. package/dist/websocket/controllers/base.d.ts +2 -2
  110. package/dist/websocket/handlers/items.js +4 -4
  111. package/dist/websocket/handlers/subscribe.js +2 -2
  112. package/dist/websocket/messages.d.ts +7 -7
  113. package/dist/websocket/messages.js +1 -1
  114. package/package.json +59 -59
  115. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
@@ -1,4 +1,5 @@
1
1
  import { getDatabaseClient } from '../index.js';
2
+ import * as capabilitiesHelpers from './capabilities/index.js';
2
3
  import * as dateHelpers from './date/index.js';
3
4
  import * as fnHelpers from './fn/index.js';
4
5
  import * as geometryHelpers from './geometry/index.js';
@@ -13,6 +14,7 @@ export function getHelpers(database) {
13
14
  schema: new schemaHelpers[client](database),
14
15
  sequence: new sequenceHelpers[client](database),
15
16
  number: new numberHelpers[client](database),
17
+ capabilities: new capabilitiesHelpers[client](database),
16
18
  };
17
19
  }
18
20
  export function getFunctions(database, schema) {
@@ -1,11 +1,10 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
2
  import { type Knex } from 'knex';
3
- import type { Options, SortRecord, Sql } from '../types.js';
3
+ import type { Options, SortRecord } from '../types.js';
4
4
  import { SchemaHelper } from '../types.js';
5
5
  export declare class SchemaHelperCockroachDb extends SchemaHelper {
6
6
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
7
7
  constraintName(existingName: string): string;
8
8
  getDatabaseSize(): Promise<number | null>;
9
- prepQueryParams(queryParams: Sql): Sql;
10
9
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
11
10
  }
@@ -1,7 +1,6 @@
1
1
  import {} from 'knex';
2
2
  import { SchemaHelper } from '../types.js';
3
3
  import { useEnv } from '@directus/env';
4
- import { prepQueryParams } from '../utils/prep-query-params.js';
5
4
  const env = useEnv();
6
5
  export class SchemaHelperCockroachDb extends SchemaHelper {
7
6
  async changeToType(table, column, type, options = {}) {
@@ -29,9 +28,6 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
29
28
  return null;
30
29
  }
31
30
  }
32
- prepQueryParams(queryParams) {
33
- return prepQueryParams(queryParams, { format: (index) => `$${index + 1}` });
34
- }
35
31
  addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
36
32
  if (hasRelationalSort) {
37
33
  /*
@@ -1,8 +1,7 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
2
+ import { SchemaHelper, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
4
4
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
5
5
  getDatabaseSize(): Promise<number | null>;
6
- prepQueryParams(queryParams: Sql): Sql;
7
6
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
8
7
  }
@@ -1,7 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { getDefaultIndexName } from '../../../../utils/get-default-index-name.js';
3
3
  import { SchemaHelper } from '../types.js';
4
- import { prepQueryParams } from '../utils/prep-query-params.js';
5
4
  const env = useEnv();
6
5
  export class SchemaHelperPostgres extends SchemaHelper {
7
6
  generateIndexName(type, collection, fields) {
@@ -16,9 +15,6 @@ export class SchemaHelperPostgres extends SchemaHelper {
16
15
  return null;
17
16
  }
18
17
  }
19
- prepQueryParams(queryParams) {
20
- return prepQueryParams(queryParams, { format: (index) => `$${index + 1}` });
21
- }
22
18
  addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
23
19
  if (hasRelationalSort) {
24
20
  /*
@@ -27,7 +27,7 @@ export function getDatabase() {
27
27
  const env = useEnv();
28
28
  const logger = useLogger();
29
29
  const metrics = useMetrics();
30
- const { client, version, searchPath, connectionString, pool: poolConfig = {}, ...connectionConfig } = getConfigFromEnv('DB_', ['DB_EXCLUDE_TABLES']);
30
+ const { client, version, searchPath, connectionString, pool: poolConfig = {}, ...connectionConfig } = getConfigFromEnv('DB_', { omitPrefix: 'DB_EXCLUDE_TABLES' });
31
31
  const requiredEnvVars = ['DB_CLIENT'];
32
32
  switch (client) {
33
33
  case 'sqlite3':
@@ -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,35 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_settings', (table) => {
3
+ table.json('visual_editor_urls').nullable();
4
+ });
5
+ await updateModuleBar(knex, (moduleBar) => {
6
+ if (moduleBar.find(({ id }) => id === 'visual'))
7
+ return;
8
+ const visualEditorModule = {
9
+ type: 'module',
10
+ id: 'visual',
11
+ enabled: false,
12
+ };
13
+ const contentModuleIndex = moduleBar.findIndex(({ id }) => id === 'content');
14
+ moduleBar.splice(contentModuleIndex + 1, 0, visualEditorModule);
15
+ return moduleBar;
16
+ });
17
+ }
18
+ export async function down(knex) {
19
+ await knex.schema.alterTable('directus_settings', (table) => {
20
+ table.dropColumns('visual_editor_urls');
21
+ });
22
+ await updateModuleBar(knex, (moduleBar) => moduleBar.filter(({ id }) => id !== 'visual'));
23
+ }
24
+ async function updateModuleBar(knex, modify) {
25
+ const result = await knex('directus_settings').select('module_bar', 'id').first();
26
+ if (result && result.module_bar) {
27
+ const moduleBar = typeof result.module_bar === 'string' ? JSON.parse(result.module_bar) : result.module_bar;
28
+ const updatedModuleBar = modify(moduleBar);
29
+ if (!updatedModuleBar)
30
+ return;
31
+ await knex('directus_settings')
32
+ .update({ module_bar: JSON.stringify(updatedModuleBar) })
33
+ .where('id', result.id);
34
+ }
35
+ }
@@ -21,16 +21,28 @@ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissi
21
21
  // Queries with aggregates and groupBy will not have duplicate result
22
22
  if (queryCopy.aggregate || queryCopy.group) {
23
23
  const flatQuery = knex.from(table);
24
+ const fieldNodeMap = Object.fromEntries(fieldNodes.map((node, index) => [
25
+ node.fieldKey,
26
+ [node, index],
27
+ ]));
28
+ const groupFieldNodes = queryCopy.group?.map((field) => fieldNodeMap[field][0]) ?? [];
24
29
  // Map the group fields to their respective field nodes
25
- const groupWhenCases = hasCaseWhen
26
- ? queryCopy.group?.map((field) => fieldNodes.find(({ fieldKey }) => fieldKey === field)?.whenCase ?? [])
27
- : undefined;
30
+ const groupWhenCases = hasCaseWhen ? groupFieldNodes.map((node) => node.whenCase ?? []) : undefined;
31
+ // Determine the number of aggregates that will be selected
32
+ const aggregateCount = Object.entries(queryCopy.aggregate ?? {}).reduce((acc, [_, fields]) => acc + fields.length, 0);
33
+ // Map the group field to their respective select column positions (1 based, offset by the number of aggregate terms that are applied in applyQuery)
34
+ // The positions need to be offset by the number of aggregate terms, since the aggregate terms are selected first
35
+ const groupColumnPositions = queryCopy.group?.map((field) => fieldNodeMap[field][1] + 1 + aggregateCount) ?? [];
28
36
  const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, permissions, {
29
37
  aliasMap,
30
38
  groupWhenCases,
39
+ groupColumnPositions,
31
40
  }).query;
32
41
  flatQuery.select(fieldNodes.map((node) => preProcess(node)));
33
- withPreprocessBindings(knex, dbQuery);
42
+ if (helpers.capabilities.supportsDeduplicationOfParameters() &&
43
+ !helpers.capabilities.supportsColumnPositionInGroupBy()) {
44
+ withPreprocessBindings(knex, dbQuery);
45
+ }
34
46
  return dbQuery;
35
47
  }
36
48
  const primaryKey = schema.collections[table].primary;
@@ -43,7 +43,7 @@ export const createLogger = () => {
43
43
  censor: REDACTED_TEXT,
44
44
  },
45
45
  };
46
- const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
46
+ const loggerEnvConfig = getConfigFromEnv('LOGGER_', { omitPrefix: 'LOGGER_HTTP' });
47
47
  // Expose custom log levels into formatter function
48
48
  if (loggerEnvConfig['levels']) {
49
49
  const customLogLevels = {};
@@ -91,8 +91,8 @@ export const createLogger = () => {
91
91
  };
92
92
  export const createExpressLogger = () => {
93
93
  const env = useEnv();
94
- const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', ['LOGGER_HTTP_LOGGER']);
95
- const loggerEnvConfig = getConfigFromEnv('LOGGER_', 'LOGGER_HTTP');
94
+ const httpLoggerEnvConfig = getConfigFromEnv('LOGGER_HTTP', { omitPrefix: 'LOGGER_HTTP_LOGGER' });
95
+ const loggerEnvConfig = getConfigFromEnv('LOGGER_', { omitPrefix: 'LOGGER_HTTP' });
96
96
  const httpLoggerOptions = {
97
97
  level: env['LOG_LEVEL'] || 'info',
98
98
  redact: {
@@ -4,16 +4,26 @@
4
4
  */
5
5
  import { sanitizeQuery } from '../utils/sanitize-query.js';
6
6
  import { validateQuery } from '../utils/validate-query.js';
7
- const sanitizeQueryMiddleware = (req, _res, next) => {
7
+ const sanitizeQueryMiddleware = async (req, _res, next) => {
8
8
  req.sanitizedQuery = {};
9
9
  if (!req.query)
10
10
  return;
11
- req.sanitizedQuery = sanitizeQuery({
12
- fields: req.query['fields'] || '*',
13
- ...req.query,
14
- }, req.accountability || null);
15
- Object.freeze(req.sanitizedQuery);
16
- validateQuery(req.sanitizedQuery);
11
+ // Skip sanitization and validation if query is empty
12
+ if (Object.keys(req.query).length === 0) {
13
+ Object.freeze(req.sanitizedQuery);
14
+ return next();
15
+ }
16
+ try {
17
+ req.sanitizedQuery = await sanitizeQuery({
18
+ fields: req.query['fields'] || '*',
19
+ ...req.query,
20
+ }, req.schema, req.accountability || null);
21
+ Object.freeze(req.sanitizedQuery);
22
+ validateQuery(req.sanitizedQuery);
23
+ }
24
+ catch (error) {
25
+ return next(error);
26
+ }
17
27
  return next();
18
28
  };
19
29
  export default sanitizeQueryMiddleware;
@@ -18,7 +18,7 @@ export const validateBatch = (scope) => asyncHandler(async (req, _res, next) =>
18
18
  }
19
19
  // In reads, the query in the body should override the query params for searching
20
20
  if (scope === 'read' && req.body.query) {
21
- req.sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
21
+ req.sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
22
22
  validateQuery(req.sanitizedQuery);
23
23
  }
24
24
  // Every cRUD action has either keys or query
@@ -26,7 +26,7 @@ export default defineOperationApi({
26
26
  knex: database,
27
27
  });
28
28
  const queryObject = query ? optionToObject(query) : {};
29
- const sanitizedQueryObject = sanitizeQuery(queryObject, customAccountability);
29
+ const sanitizedQueryObject = await sanitizeQuery(queryObject, schema, customAccountability);
30
30
  let result;
31
31
  if (!key || (Array.isArray(key) && key.length === 0)) {
32
32
  result = await itemsService.deleteByQuery(sanitizedQueryObject, { emitEvents: !!emitEvents });
@@ -26,7 +26,7 @@ export default defineOperationApi({
26
26
  knex: database,
27
27
  });
28
28
  const queryObject = query ? optionToObject(query) : {};
29
- const sanitizedQueryObject = sanitizeQuery(queryObject, customAccountability);
29
+ const sanitizedQueryObject = await sanitizeQuery(queryObject, schema, customAccountability);
30
30
  let result;
31
31
  if (!key || (Array.isArray(key) && key.length === 0)) {
32
32
  result = await itemsService.readByQuery(sanitizedQueryObject, { emitEvents: !!emitEvents });
@@ -27,7 +27,7 @@ export default defineOperationApi({
27
27
  });
28
28
  const payloadObject = optionToObject(payload) ?? null;
29
29
  const queryObject = query ? optionToObject(query) : {};
30
- const sanitizedQueryObject = sanitizeQuery(queryObject, customAccountability);
30
+ const sanitizedQueryObject = await sanitizeQuery(queryObject, schema, customAccountability);
31
31
  if (!payloadObject) {
32
32
  return null;
33
33
  }
@@ -1,14 +1,16 @@
1
- import { fetchDynamicVariableContext } from '../utils/fetch-dynamic-variable-context.js';
1
+ import { extractRequiredDynamicVariableContextForPermissions } from '../utils/extract-required-dynamic-variable-context.js';
2
+ import { fetchDynamicVariableData } from '../utils/fetch-dynamic-variable-data.js';
2
3
  import { fetchRawPermissions } from '../utils/fetch-raw-permissions.js';
3
- import { processPermissions } from '../utils/process-permissions.js';
4
4
  import { getPermissionsForShare } from '../utils/get-permissions-for-share.js';
5
+ import { processPermissions } from '../utils/process-permissions.js';
5
6
  export async function fetchPermissions(options, context) {
6
7
  const permissions = await fetchRawPermissions({ ...options, bypassMinimalAppPermissions: options.bypassDynamicVariableProcessing ?? false }, context);
7
8
  if (options.accountability && !options.bypassDynamicVariableProcessing) {
8
- const permissionsContext = await fetchDynamicVariableContext({
9
+ const dynamicVariableContext = extractRequiredDynamicVariableContextForPermissions(permissions);
10
+ const permissionsContext = await fetchDynamicVariableData({
9
11
  accountability: options.accountability,
10
12
  policies: options.policies,
11
- permissions,
13
+ dynamicVariableContext,
12
14
  }, context);
13
15
  // Replace dynamic variables with their actual values
14
16
  const processedPermissions = processPermissions({
@@ -0,0 +1,2 @@
1
+ import type { DynamicVariableContext } from '../../../utils/extract-required-dynamic-variable-context.js';
2
+ export declare function contextHasDynamicVariables(context: DynamicVariableContext): boolean;
@@ -0,0 +1,3 @@
1
+ export function contextHasDynamicVariables(context) {
2
+ return Object.values(context).some((v) => v.size > 0);
3
+ }
@@ -5,6 +5,7 @@ export interface ProcessPayloadOptions {
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
7
  payload: Item;
8
+ nested: string[];
8
9
  }
9
10
  /**
10
11
  * @note this only validates the top-level fields. The expectation is that this function is called
@@ -1,9 +1,12 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
- import { validatePayload } from '@directus/utils';
2
+ import { parseFilter, validatePayload } from '@directus/utils';
3
3
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
4
4
  import { assign, difference, uniq } from 'lodash-es';
5
5
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
6
6
  import { fetchPolicies } from '../../lib/fetch-policies.js';
7
+ import { extractRequiredDynamicVariableContext } from '../../utils/extract-required-dynamic-variable-context.js';
8
+ import { fetchDynamicVariableData } from '../../utils/fetch-dynamic-variable-data.js';
9
+ import { contextHasDynamicVariables } from '../process-ast/utils/context-has-dynamic-variables.js';
7
10
  import { isFieldNullable } from './lib/is-field-nullable.js';
8
11
  /**
9
12
  * @note this only validates the top-level fields. The expectation is that this function is called
@@ -12,8 +15,9 @@ import { isFieldNullable } from './lib/is-field-nullable.js';
12
15
  export async function processPayload(options, context) {
13
16
  let permissions;
14
17
  let permissionValidationRules = [];
18
+ let policies = [];
15
19
  if (!options.accountability.admin) {
16
- const policies = await fetchPolicies(options.accountability, context);
20
+ policies = await fetchPolicies(options.accountability, context);
17
21
  permissions = await fetchPermissions({ action: options.action, policies, collections: [options.collection], accountability: options.accountability }, context);
18
22
  if (permissions.length === 0) {
19
23
  throw new ForbiddenError({
@@ -53,7 +57,18 @@ export async function processPayload(options, context) {
53
57
  },
54
58
  });
55
59
  }
56
- fieldValidationRules.push(field.validation);
60
+ if (field.validation) {
61
+ const permissionContext = extractRequiredDynamicVariableContext(field.validation);
62
+ const filterContext = contextHasDynamicVariables(permissionContext)
63
+ ? await fetchDynamicVariableData({
64
+ accountability: options.accountability,
65
+ policies,
66
+ dynamicVariableContext: permissionContext,
67
+ }, context)
68
+ : undefined;
69
+ const validationFilter = parseFilter(field.validation, options.accountability, filterContext);
70
+ fieldValidationRules.push(validationFilter);
71
+ }
57
72
  }
58
73
  const presets = (permissions ?? []).map((permission) => permission.presets);
59
74
  const payloadWithPresets = assign({}, ...presets, options.payload);
@@ -67,7 +82,7 @@ export async function processPayload(options, context) {
67
82
  if (validationRules.length > 0) {
68
83
  const validationErrors = [];
69
84
  validationErrors.push(...validatePayload({ _and: validationRules }, payloadWithPresets)
70
- .map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
85
+ .map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details, options.nested))))
71
86
  .flat());
72
87
  if (validationErrors.length > 0)
73
88
  throw validationErrors;
@@ -1,6 +1,7 @@
1
- import type { SchemaOverview } from '@directus/types';
1
+ import type { Accountability, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  export interface Context {
4
4
  schema: SchemaOverview;
5
5
  knex: Knex;
6
+ accountability?: Accountability;
6
7
  }
@@ -1,8 +1,9 @@
1
1
  import type { Permission } from '@directus/types';
2
- export interface RequiredPermissionContext {
2
+ export interface DynamicVariableContext {
3
3
  $CURRENT_USER: Set<string>;
4
4
  $CURRENT_ROLE: Set<string>;
5
5
  $CURRENT_ROLES: Set<string>;
6
6
  $CURRENT_POLICIES: Set<string>;
7
7
  }
8
- export declare function extractRequiredDynamicVariableContext(permissions: Permission[]): RequiredPermissionContext;
8
+ export declare function extractRequiredDynamicVariableContextForPermissions(permissions: Permission[]): DynamicVariableContext;
9
+ export declare function extractRequiredDynamicVariableContext(val: any): DynamicVariableContext;
@@ -1,17 +1,27 @@
1
1
  import { deepMap } from '@directus/utils';
2
- export function extractRequiredDynamicVariableContext(permissions) {
3
- const permissionContext = {
2
+ export function extractRequiredDynamicVariableContextForPermissions(permissions) {
3
+ let permissionContext = {
4
4
  $CURRENT_USER: new Set(),
5
5
  $CURRENT_ROLE: new Set(),
6
6
  $CURRENT_ROLES: new Set(),
7
7
  $CURRENT_POLICIES: new Set(),
8
8
  };
9
9
  for (const permission of permissions) {
10
- deepMap(permission.permissions, extractPermissionData);
11
- deepMap(permission.validation, extractPermissionData);
12
- deepMap(permission.presets, extractPermissionData);
10
+ permissionContext = mergeContexts(permissionContext, extractRequiredDynamicVariableContext(permission.permissions));
11
+ permissionContext = mergeContexts(permissionContext, extractRequiredDynamicVariableContext(permission.validation));
12
+ permissionContext = mergeContexts(permissionContext, extractRequiredDynamicVariableContext(permission.presets));
13
13
  }
14
14
  return permissionContext;
15
+ }
16
+ export function extractRequiredDynamicVariableContext(val) {
17
+ const permissionContext = {
18
+ $CURRENT_USER: new Set(),
19
+ $CURRENT_ROLE: new Set(),
20
+ $CURRENT_ROLES: new Set(),
21
+ $CURRENT_POLICIES: new Set(),
22
+ };
23
+ deepMap(val, extractPermissionData);
24
+ return permissionContext;
15
25
  function extractPermissionData(val) {
16
26
  for (const placeholder of [
17
27
  '$CURRENT_USER',
@@ -25,3 +35,12 @@ export function extractRequiredDynamicVariableContext(permissions) {
25
35
  }
26
36
  }
27
37
  }
38
+ function mergeContexts(context1, context2) {
39
+ const permissionContext = {
40
+ $CURRENT_USER: new Set([...context1.$CURRENT_USER, ...context2.$CURRENT_USER]),
41
+ $CURRENT_ROLE: new Set([...context1.$CURRENT_ROLE, ...context2.$CURRENT_ROLE]),
42
+ $CURRENT_ROLES: new Set([...context1.$CURRENT_ROLES, ...context2.$CURRENT_ROLES]),
43
+ $CURRENT_POLICIES: new Set([...context1.$CURRENT_POLICIES, ...context2.$CURRENT_POLICIES]),
44
+ };
45
+ return permissionContext;
46
+ }
@@ -0,0 +1,9 @@
1
+ import type { Accountability } from '@directus/types';
2
+ import type { Context } from '../types.js';
3
+ import { type DynamicVariableContext } from './extract-required-dynamic-variable-context.js';
4
+ export interface FetchDynamicVariableContext {
5
+ accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
6
+ policies: string[];
7
+ dynamicVariableContext: DynamicVariableContext;
8
+ }
9
+ export declare function fetchDynamicVariableData(options: FetchDynamicVariableContext, context: Context): Promise<Record<string, any>>;
@@ -1,31 +1,32 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { getSimpleHash } from '@directus/utils';
3
3
  import { getCache, getCacheValue, setCacheValue } from '../../cache.js';
4
- import { extractRequiredDynamicVariableContext, } from './extract-required-dynamic-variable-context.js';
5
- export async function fetchDynamicVariableContext(options, context) {
4
+ import {} from './extract-required-dynamic-variable-context.js';
5
+ export async function fetchDynamicVariableData(options, context) {
6
6
  const { UsersService } = await import('../../services/users.js');
7
7
  const { RolesService } = await import('../../services/roles.js');
8
8
  const { PoliciesService } = await import('../../services/policies.js');
9
9
  const contextData = {};
10
- const permissionContext = extractRequiredDynamicVariableContext(options.permissions);
11
- if (options.accountability.user && (permissionContext.$CURRENT_USER?.size ?? 0) > 0) {
12
- contextData['$CURRENT_USER'] = await fetchContextData('$CURRENT_USER', permissionContext, { user: options.accountability.user }, async (fields) => {
10
+ if (options.accountability.user && (options.dynamicVariableContext.$CURRENT_USER?.size ?? 0) > 0) {
11
+ contextData['$CURRENT_USER'] = await fetchContextData('$CURRENT_USER', options.dynamicVariableContext, { user: options.accountability.user }, async (fields) => {
13
12
  const usersService = new UsersService(context);
14
13
  return await usersService.readOne(options.accountability.user, {
15
14
  fields,
16
15
  });
17
16
  });
18
17
  }
19
- if (options.accountability.role && (permissionContext.$CURRENT_ROLE?.size ?? 0) > 0) {
20
- contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE', permissionContext, { role: options.accountability.role }, async (fields) => {
18
+ if (options.accountability.role && (options.dynamicVariableContext.$CURRENT_ROLE?.size ?? 0) > 0) {
19
+ contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE', options.dynamicVariableContext, { role: options.accountability.role }, async (fields) => {
21
20
  const rolesService = new RolesService(context);
22
21
  return await rolesService.readOne(options.accountability.role, {
23
22
  fields,
24
23
  });
25
24
  });
26
25
  }
27
- if (options.accountability.roles.length > 0 && (permissionContext.$CURRENT_ROLES?.size ?? 0) > 0) {
28
- contextData['$CURRENT_ROLES'] = await fetchContextData('$CURRENT_ROLES', permissionContext, { roles: options.accountability.roles }, async (fields) => {
26
+ if (options.accountability.roles &&
27
+ options.accountability.roles.length > 0 &&
28
+ (options.dynamicVariableContext.$CURRENT_ROLES?.size ?? 0) > 0) {
29
+ contextData['$CURRENT_ROLES'] = await fetchContextData('$CURRENT_ROLES', options.dynamicVariableContext, { roles: options.accountability.roles }, async (fields) => {
29
30
  const rolesService = new RolesService(context);
30
31
  return await rolesService.readMany(options.accountability.roles, {
31
32
  fields,
@@ -33,10 +34,10 @@ export async function fetchDynamicVariableContext(options, context) {
33
34
  });
34
35
  }
35
36
  if (options.policies.length > 0) {
36
- if ((permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
37
+ if ((options.dynamicVariableContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
37
38
  // Always add the id field
38
- permissionContext.$CURRENT_POLICIES.add('id');
39
- contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', permissionContext, { policies: options.policies }, async (fields) => {
39
+ options.dynamicVariableContext.$CURRENT_POLICIES.add('id');
40
+ contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', options.dynamicVariableContext, { policies: options.policies }, async (fields) => {
40
41
  const policiesService = new PoliciesService(context);
41
42
  return await policiesService.readMany(options.policies, {
42
43
  fields,
@@ -16,7 +16,7 @@ export function createRateLimiter(configPrefix = 'RATE_LIMITER', configOverrides
16
16
  }
17
17
  export { RateLimiterRes };
18
18
  function getConfig(store = 'memory', configPrefix = 'RATE_LIMITER', overrides) {
19
- const config = getConfigFromEnv(`${configPrefix}_`, `${configPrefix}_${store}_`);
19
+ const config = getConfigFromEnv(`${configPrefix}_`, { omitPrefix: `${configPrefix}_${store}_` });
20
20
  if (store === 'redis') {
21
21
  const Redis = require('ioredis');
22
22
  const env = useEnv();
@@ -1,5 +1,5 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { ForbiddenError, IllegalAssetTransformationError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
2
+ import { ForbiddenError, IllegalAssetTransformationError, InvalidQueryError, RangeNotSatisfiableError, ServiceUnavailableError, } from '@directus/errors';
3
3
  import { clamp } from 'lodash-es';
4
4
  import { contentType } from 'mime-types';
5
5
  import hash from 'object-hash';
@@ -130,7 +130,17 @@ export class AssetsService {
130
130
  });
131
131
  if (transforms.find((transform) => transform[0] === 'rotate') === undefined)
132
132
  transformer.rotate();
133
- transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
133
+ try {
134
+ for (const [method, ...args] of transforms) {
135
+ transformer[method].apply(transformer, args);
136
+ }
137
+ }
138
+ catch (error) {
139
+ if (error instanceof Error && error.message.startsWith('Expected')) {
140
+ throw new InvalidQueryError({ reason: error.message });
141
+ }
142
+ throw error;
143
+ }
134
144
  const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
135
145
  readStream.on('error', (e) => {
136
146
  logger.error(e, `Couldn't transform file ${file.id}`);
@@ -1,5 +1,3 @@
1
- import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
2
- import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
3
1
  import { Action } from '@directus/constants';
4
2
  import { useEnv } from '@directus/env';
5
3
  import { InvalidCredentialsError, InvalidOtpError, ServiceUnavailableError, UserSuspendedError, } from '@directus/errors';
@@ -10,6 +8,8 @@ import { getAuthProvider } from '../auth.js';
10
8
  import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
11
9
  import getDatabase from '../database/index.js';
12
10
  import emitter from '../emitter.js';
11
+ import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
12
+ import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
13
13
  import { RateLimiterRes, createRateLimiter } from '../rate-limiter.js';
14
14
  import { getMilliseconds } from '../utils/get-milliseconds.js';
15
15
  import { getSecret } from '../utils/get-secret.js';
@@ -44,8 +44,11 @@ export class CollectionsService {
44
44
  if (this.accountability && this.accountability.admin !== true) {
45
45
  throw new ForbiddenError();
46
46
  }
47
- if (!payload.collection)
47
+ if (!('collection' in payload))
48
48
  throw new InvalidPayloadError({ reason: `"collection" is required` });
49
+ if (typeof payload.collection !== 'string' || payload.collection === '') {
50
+ throw new InvalidPayloadError({ reason: `"collection" must be a non-empty string` });
51
+ }
49
52
  if (payload.collection.startsWith('directus_')) {
50
53
  throw new InvalidPayloadError({ reason: `Collections can't start with "directus_"` });
51
54
  }
@@ -64,10 +67,13 @@ export class CollectionsService {
64
67
  // transactions.
65
68
  await transaction(this.knex, async (trx) => {
66
69
  if (payload.schema) {
70
+ if ('fields' in payload && !Array.isArray(payload.fields)) {
71
+ throw new InvalidPayloadError({ reason: `"fields" must be an array` });
72
+ }
67
73
  // Directus heavily relies on the primary key of a collection, so we have to make sure that
68
74
  // every collection that is created has a primary key. If no primary key field is created
69
75
  // while making the collection, we default to an auto incremented id named `id`
70
- if (!payload.fields) {
76
+ if (!payload.fields || payload.fields.length === 0) {
71
77
  payload.fields = [
72
78
  {
73
79
  field: 'id',
@@ -0,0 +1,3 @@
1
+ import type { SchemaComposer } from 'graphql-compose';
2
+ import type { Schema } from '../schema/index.js';
3
+ export declare function getCollectionType(schemaComposer: SchemaComposer, schema: Schema, action: 'read' | 'write'): import("graphql-compose").ObjectTypeComposer<any, any>;
@@ -0,0 +1,34 @@
1
+ import { GraphQLNonNull, GraphQLString } from 'graphql';
2
+ import { getGraphQLType } from '../../../utils/get-graphql-type.js';
3
+ export function getCollectionType(schemaComposer, schema, action) {
4
+ const prefix = action === 'read' ? '' : 'write_';
5
+ const Collection = schemaComposer.createObjectTC({
6
+ name: `${prefix}directus_collections`,
7
+ });
8
+ if ('directus_collections' in schema.read.collections === false) {
9
+ return Collection;
10
+ }
11
+ Collection.addFields({
12
+ collection: GraphQLString,
13
+ meta: schemaComposer.createObjectTC({
14
+ name: `${prefix}directus_collections_meta`,
15
+ fields: Object.values(schema.read.collections['directus_collections'].fields).reduce((acc, field) => {
16
+ acc[field.field] = {
17
+ type: field.nullable || action === 'write'
18
+ ? getGraphQLType(field.type, field.special)
19
+ : new GraphQLNonNull(getGraphQLType(field.type, field.special)),
20
+ description: field.note,
21
+ };
22
+ return acc;
23
+ }, {}),
24
+ }),
25
+ schema: schemaComposer.createObjectTC({
26
+ name: `${prefix}directus_collections_schema`,
27
+ fields: {
28
+ name: GraphQLString,
29
+ comment: GraphQLString,
30
+ },
31
+ }),
32
+ });
33
+ return Collection;
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { SchemaComposer } from 'graphql-compose';
2
+ import type { Schema } from '../schema/index.js';
3
+ export declare function getFieldType(schemaComposer: SchemaComposer, schema: Schema, action: 'read' | 'write'): import("graphql-compose").ObjectTypeComposer<any, any>;