@directus/api 31.0.0 → 32.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 (135) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +11 -8
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +11 -8
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/server.js +26 -1
  20. package/dist/controllers/settings.js +9 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/database/helpers/fn/types.js +3 -3
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  33. package/dist/database/helpers/schema/types.d.ts +5 -0
  34. package/dist/database/helpers/schema/types.js +6 -0
  35. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  36. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  37. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  38. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  39. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  40. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  41. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  43. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  44. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  45. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  46. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  47. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  48. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  49. package/dist/flows.js +1 -0
  50. package/dist/mcp/schema.d.ts +14 -14
  51. package/dist/mcp/schema.js +6 -6
  52. package/dist/mcp/server.d.ts +9 -3
  53. package/dist/mcp/server.js +1 -1
  54. package/dist/mcp/tools/collections.d.ts +1 -1
  55. package/dist/mcp/tools/fields.d.ts +1 -1
  56. package/dist/mcp/tools/files.d.ts +25 -25
  57. package/dist/mcp/tools/flows.d.ts +36 -36
  58. package/dist/mcp/tools/folders.d.ts +18 -18
  59. package/dist/mcp/tools/items.d.ts +18 -18
  60. package/dist/mcp/tools/operations.d.ts +19 -19
  61. package/dist/mcp/tools/prompts/items.md +1 -1
  62. package/dist/metrics/lib/create-metrics.js +16 -25
  63. package/dist/middleware/collection-exists.js +2 -2
  64. package/dist/operations/mail/index.js +3 -1
  65. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  66. package/dist/operations/mail/rate-limiter.js +29 -0
  67. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  68. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  69. package/dist/schedules/metrics.js +6 -2
  70. package/dist/schedules/project.d.ts +4 -0
  71. package/dist/schedules/project.js +27 -0
  72. package/dist/services/collections.d.ts +3 -3
  73. package/dist/services/collections.js +16 -1
  74. package/dist/services/fields.d.ts +21 -5
  75. package/dist/services/fields.js +105 -28
  76. package/dist/services/graphql/resolvers/query.js +1 -1
  77. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  78. package/dist/services/graphql/schema/parse-query.js +8 -8
  79. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  80. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  81. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  82. package/dist/services/import-export.d.ts +9 -1
  83. package/dist/services/import-export.js +287 -101
  84. package/dist/services/items.d.ts +1 -1
  85. package/dist/services/items.js +36 -20
  86. package/dist/services/mail/index.js +2 -0
  87. package/dist/services/mail/rate-limiter.d.ts +1 -0
  88. package/dist/services/mail/rate-limiter.js +29 -0
  89. package/dist/services/meta.js +28 -24
  90. package/dist/services/schema.js +4 -1
  91. package/dist/services/server.d.ts +1 -0
  92. package/dist/services/server.js +14 -18
  93. package/dist/services/settings.d.ts +2 -1
  94. package/dist/services/settings.js +15 -0
  95. package/dist/services/tus/server.js +14 -9
  96. package/dist/telemetry/lib/get-report.js +4 -4
  97. package/dist/telemetry/lib/send-report.d.ts +6 -1
  98. package/dist/telemetry/lib/send-report.js +3 -1
  99. package/dist/telemetry/types/report.d.ts +17 -1
  100. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  101. package/dist/telemetry/utils/get-settings.js +14 -0
  102. package/dist/test-utils/README.md +760 -0
  103. package/dist/test-utils/cache.d.ts +51 -0
  104. package/dist/test-utils/cache.js +59 -0
  105. package/dist/test-utils/database.d.ts +48 -0
  106. package/dist/test-utils/database.js +52 -0
  107. package/dist/test-utils/emitter.d.ts +35 -0
  108. package/dist/test-utils/emitter.js +38 -0
  109. package/dist/test-utils/fields-service.d.ts +28 -0
  110. package/dist/test-utils/fields-service.js +36 -0
  111. package/dist/test-utils/items-service.d.ts +23 -0
  112. package/dist/test-utils/items-service.js +37 -0
  113. package/dist/test-utils/knex.d.ts +164 -0
  114. package/dist/test-utils/knex.js +268 -0
  115. package/dist/test-utils/schema.d.ts +26 -0
  116. package/dist/test-utils/schema.js +35 -0
  117. package/dist/types/auth.d.ts +0 -2
  118. package/dist/utils/apply-diff.js +15 -0
  119. package/dist/utils/create-admin.d.ts +11 -0
  120. package/dist/utils/create-admin.js +50 -0
  121. package/dist/utils/get-schema.js +5 -3
  122. package/dist/utils/get-snapshot-diff.js +49 -5
  123. package/dist/utils/get-snapshot.js +13 -7
  124. package/dist/utils/sanitize-schema.d.ts +11 -4
  125. package/dist/utils/sanitize-schema.js +9 -6
  126. package/dist/utils/schedule.js +15 -19
  127. package/dist/utils/validate-diff.js +31 -0
  128. package/dist/utils/validate-snapshot.js +7 -0
  129. package/dist/websocket/controllers/hooks.js +12 -20
  130. package/dist/websocket/messages.d.ts +3 -3
  131. package/package.json +63 -65
  132. package/dist/cli/utils/defaults.d.ts +0 -4
  133. package/dist/cli/utils/defaults.js +0 -17
  134. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  135. package/dist/telemetry/utils/get-project-id.js +0 -4
@@ -28,17 +28,17 @@ export declare const OperationsValidationSchema: z.ZodDiscriminatedUnion<[z.ZodO
28
28
  deep: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
29
29
  alias: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
30
30
  aggregate: z.ZodOptional<z.ZodObject<{
31
- count: z.ZodArray<z.ZodString>;
32
- sum: z.ZodArray<z.ZodString>;
33
- avg: z.ZodArray<z.ZodString>;
34
- min: z.ZodArray<z.ZodString>;
35
- max: z.ZodArray<z.ZodString>;
31
+ count: z.ZodOptional<z.ZodArray<z.ZodString>>;
32
+ sum: z.ZodOptional<z.ZodArray<z.ZodString>>;
33
+ avg: z.ZodOptional<z.ZodArray<z.ZodString>>;
34
+ min: z.ZodOptional<z.ZodArray<z.ZodString>>;
35
+ max: z.ZodOptional<z.ZodArray<z.ZodString>>;
36
36
  }, z.core.$strip>>;
37
37
  backlink: z.ZodOptional<z.ZodBoolean>;
38
38
  version: z.ZodOptional<z.ZodString>;
39
39
  versionRaw: z.ZodOptional<z.ZodBoolean>;
40
40
  export: z.ZodOptional<z.ZodString>;
41
- group: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
+ groupBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
42
  }, z.core.$strip>>;
43
43
  }, z.core.$strict>, z.ZodObject<{
44
44
  action: z.ZodLiteral<"update">;
@@ -60,7 +60,7 @@ export declare const OperationsValidationSchema: z.ZodDiscriminatedUnion<[z.ZodO
60
60
  }, z.core.$strict>, z.ZodObject<{
61
61
  action: z.ZodLiteral<"delete">;
62
62
  key: z.ZodString;
63
- }, z.core.$strict>]>;
63
+ }, z.core.$strict>], "action">;
64
64
  export declare const OperationsInputSchema: z.ZodObject<{
65
65
  action: z.ZodEnum<{
66
66
  delete: "delete";
@@ -79,17 +79,17 @@ export declare const OperationsInputSchema: z.ZodObject<{
79
79
  deep: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
80
80
  alias: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
81
81
  aggregate: z.ZodOptional<z.ZodObject<{
82
- count: z.ZodArray<z.ZodString>;
83
- sum: z.ZodArray<z.ZodString>;
84
- avg: z.ZodArray<z.ZodString>;
85
- min: z.ZodArray<z.ZodString>;
86
- max: z.ZodArray<z.ZodString>;
82
+ count: z.ZodOptional<z.ZodArray<z.ZodString>>;
83
+ sum: z.ZodOptional<z.ZodArray<z.ZodString>>;
84
+ avg: z.ZodOptional<z.ZodArray<z.ZodString>>;
85
+ min: z.ZodOptional<z.ZodArray<z.ZodString>>;
86
+ max: z.ZodOptional<z.ZodArray<z.ZodString>>;
87
87
  }, z.core.$strip>>;
88
88
  backlink: z.ZodOptional<z.ZodBoolean>;
89
89
  version: z.ZodOptional<z.ZodString>;
90
90
  versionRaw: z.ZodOptional<z.ZodBoolean>;
91
91
  export: z.ZodOptional<z.ZodString>;
92
- group: z.ZodOptional<z.ZodArray<z.ZodString>>;
92
+ groupBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
93
93
  }, z.core.$strip>>;
94
94
  data: z.ZodOptional<z.ZodObject<{
95
95
  id: z.ZodOptional<z.ZodString>;
@@ -136,17 +136,17 @@ export declare const operations: import("../types.js").ToolConfig<{
136
136
  deep?: Record<string, any> | undefined;
137
137
  alias?: Record<string, string> | undefined;
138
138
  aggregate?: {
139
- count: string[];
140
- sum: string[];
141
- avg: string[];
142
- min: string[];
143
- max: string[];
139
+ count?: string[] | undefined;
140
+ sum?: string[] | undefined;
141
+ avg?: string[] | undefined;
142
+ min?: string[] | undefined;
143
+ max?: string[] | undefined;
144
144
  } | undefined;
145
145
  backlink?: boolean | undefined;
146
146
  version?: string | undefined;
147
147
  versionRaw?: boolean | undefined;
148
148
  export?: string | undefined;
149
- group?: string[] | undefined;
149
+ groupBy?: string[] | undefined;
150
150
  } | undefined;
151
151
  } | {
152
152
  action: "update";
@@ -307,7 +307,7 @@ Date: `year(field)`, `month(field)`, `day(field)`, `hour(field)` Aggregate: `cou
307
307
 
308
308
  ```json
309
309
  {"filter": {"year(date_created)": {"_eq": 2024}}}
310
- {"aggregate": {"count": "*", "sum": "price"}, "groupBy": ["category"]}
310
+ {"aggregate": {"count": ["*"], "sum": ["price"]}, "groupBy": ["category"]}
311
311
  ```
312
312
 
313
313
  ## Restrictions
@@ -18,6 +18,7 @@ export function createMetrics() {
18
18
  const env = useEnv();
19
19
  const logger = useLogger();
20
20
  const services = env['METRICS_SERVICES'] ?? [];
21
+ const metricNamePrefix = env['METRICS_NAME_PREFIX'] ?? 'directus_';
21
22
  const aggregates = new Map();
22
23
  /**
23
24
  * Listen for PM2 metric data sync messages and add them to the aggregate
@@ -92,10 +93,10 @@ export function createMetrics() {
92
93
  return null;
93
94
  }
94
95
  const client = env['DB_CLIENT'];
95
- let metric = register.getSingleMetric(`directus_db_${client}_connection_errors`);
96
+ let metric = register.getSingleMetric(`${metricNamePrefix}db_${client}_connection_errors`);
96
97
  if (!metric) {
97
98
  metric = new Counter({
98
- name: `directus_db_${client}_connection_errors`,
99
+ name: `${metricNamePrefix}db_${client}_connection_errors`,
99
100
  help: `${client} Database connection error count`,
100
101
  });
101
102
  }
@@ -106,10 +107,10 @@ export function createMetrics() {
106
107
  return null;
107
108
  }
108
109
  const client = env['DB_CLIENT'];
109
- let metric = register.getSingleMetric(`directus_db_${client}_response_time_ms`);
110
+ let metric = register.getSingleMetric(`${metricNamePrefix}db_${client}_response_time_ms`);
110
111
  if (!metric) {
111
112
  metric = new Histogram({
112
- name: `directus_db_${client}_response_time_ms`,
113
+ name: `${metricNamePrefix}db_${client}_response_time_ms`,
113
114
  help: `${client} Database connection response time`,
114
115
  buckets: [1, 10, 20, 40, 60, 80, 100, 200, 500, 750, 1000],
115
116
  });
@@ -123,10 +124,10 @@ export function createMetrics() {
123
124
  if (env['CACHE_STORE'] === 'redis' && redisConfigAvailable() !== true) {
124
125
  return null;
125
126
  }
126
- let metric = register.getSingleMetric(`directus_cache_${env['CACHE_STORE']}_connection_errors`);
127
+ let metric = register.getSingleMetric(`${metricNamePrefix}cache_${env['CACHE_STORE']}_connection_errors`);
127
128
  if (!metric) {
128
129
  metric = new Counter({
129
- name: `directus_cache_${env['CACHE_STORE']}_connection_errors`,
130
+ name: `${metricNamePrefix}cache_${env['CACHE_STORE']}_connection_errors`,
130
131
  help: 'Cache connection error count',
131
132
  });
132
133
  }
@@ -136,10 +137,10 @@ export function createMetrics() {
136
137
  if (services.includes('redis') === false || redisConfigAvailable() !== true) {
137
138
  return null;
138
139
  }
139
- let metric = register.getSingleMetric('directus_redis_connection_errors');
140
+ let metric = register.getSingleMetric(`${metricNamePrefix}redis_connection_errors`);
140
141
  if (!metric) {
141
142
  metric = new Counter({
142
- name: `directus_redis_connection_errors`,
143
+ name: `${metricNamePrefix}redis_connection_errors`,
143
144
  help: 'Redis connection error count',
144
145
  });
145
146
  }
@@ -149,10 +150,10 @@ export function createMetrics() {
149
150
  if (services.includes('storage') === false) {
150
151
  return null;
151
152
  }
152
- let metric = register.getSingleMetric(`directus_storage_${location}_connection_errors`);
153
+ let metric = register.getSingleMetric(`${metricNamePrefix}storage_${location}_connection_errors`);
153
154
  if (!metric) {
154
155
  metric = new Counter({
155
- name: `directus_storage_${location}_connection_errors`,
156
+ name: `${metricNamePrefix}storage_${location}_connection_errors`,
156
157
  help: `${location} storage connection error count`,
157
158
  });
158
159
  }
@@ -182,8 +183,8 @@ export function createMetrics() {
182
183
  return;
183
184
  }
184
185
  try {
185
- await cache.set(`metrics-${checkId}`, '1', 5);
186
- await cache.delete(`metrics-${checkId}`);
186
+ await cache.set(`directus-metric-${checkId}`, '1', 5);
187
+ await cache.delete(`directus-metric-${checkId}`);
187
188
  }
188
189
  catch {
189
190
  metric.inc();
@@ -196,8 +197,8 @@ export function createMetrics() {
196
197
  }
197
198
  const redis = useRedis();
198
199
  try {
199
- await redis.set(`metrics-${checkId}`, '1');
200
- await redis.del(`metrics-${checkId}`);
200
+ await redis.set(`directus-metric-${checkId}`, '1');
201
+ await redis.del(`directus-metric-${checkId}`);
201
202
  }
202
203
  catch {
203
204
  metric.inc();
@@ -215,17 +216,7 @@ export function createMetrics() {
215
216
  continue;
216
217
  }
217
218
  try {
218
- await disk.write(`metric-${checkId}`, Readable.from(['check']));
219
- const fileStream = await disk.read(`metric-${checkId}`);
220
- fileStream.on('data', async () => {
221
- try {
222
- fileStream.destroy();
223
- await disk.delete(`metric-${checkId}`);
224
- }
225
- catch (error) {
226
- logger.error(error);
227
- }
228
- });
219
+ await disk.write('directus-metric-file', Readable.from([checkId]));
229
220
  }
230
221
  catch {
231
222
  metric.inc();
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Check if requested collection exists, and save it to req.collection
3
3
  */
4
- import { ForbiddenError } from '@directus/errors';
5
4
  import { systemCollectionRows } from '@directus/system-data';
6
5
  import asyncHandler from '../utils/async-handler.js';
6
+ import { createCollectionForbiddenError } from '../permissions/modules/process-ast/utils/validate-path/create-error.js';
7
7
  const collectionExists = asyncHandler(async (req, _res, next) => {
8
8
  if (!req.params['collection'])
9
9
  return next();
10
10
  if (req.params['collection'] in req.schema.collections === false) {
11
- throw new ForbiddenError();
11
+ throw createCollectionForbiddenError('', req.params['collection']);
12
12
  }
13
13
  req.collection = req.params['collection'];
14
14
  const systemCollectionRow = systemCollectionRows.find((collection) => {
@@ -2,10 +2,12 @@ import { defineOperationApi } from '@directus/extensions';
2
2
  import { MailService } from '../../services/mail/index.js';
3
3
  import { md } from '../../utils/md.js';
4
4
  import { useLogger } from '../../logger/index.js';
5
+ import { useFlowsEmailRateLimiter } from './rate-limiter.js';
5
6
  const logger = useLogger();
6
7
  export default defineOperationApi({
7
8
  id: 'mail',
8
- handler: async ({ body, template, data, to, type, subject, cc, bcc, replyTo }, { accountability, database, getSchema }) => {
9
+ handler: async ({ body, template, data, to, type, subject, cc, bcc, replyTo }, { accountability, database, getSchema, flow }) => {
10
+ await useFlowsEmailRateLimiter(flow.id);
9
11
  const mailService = new MailService({ schema: await getSchema({ database }), accountability, knex: database });
10
12
  const mailObject = { to, subject, cc, bcc, replyTo };
11
13
  const safeBody = typeof body !== 'string' ? JSON.stringify(body) : body;
@@ -0,0 +1 @@
1
+ export declare function useFlowsEmailRateLimiter(flow_id: string): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { RateLimiterMemory, RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible';
3
+ import { createRateLimiter } from '../../rate-limiter.js';
4
+ import { toBoolean } from '@directus/utils';
5
+ import { EmailLimitExceededError } from '@directus/errors';
6
+ let emailRateLimiter;
7
+ const env = useEnv();
8
+ if (toBoolean(env['RATE_LIMITER_EMAIL_FLOWS_ENABLED']) === true) {
9
+ emailRateLimiter = createRateLimiter('RATE_LIMITER_EMAIL_FLOWS');
10
+ }
11
+ export async function useFlowsEmailRateLimiter(flow_id) {
12
+ if (!emailRateLimiter)
13
+ return;
14
+ try {
15
+ await emailRateLimiter.consume(flow_id, 1);
16
+ }
17
+ catch (err) {
18
+ if (err instanceof RateLimiterRes) {
19
+ throw new EmailLimitExceededError({
20
+ points: 'RATE_LIMITER_EMAIL_FLOWS_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_FLOWS_POINTS']) : undefined,
21
+ duration: 'RATE_LIMITER_EMAIL_FLOWS_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_FLOWS_DURATION']) : undefined,
22
+ message: 'RATE_LIMITER_EMAIL_FLOWS_ERROR_MESSAGE' in env
23
+ ? String(env['RATE_LIMITER_EMAIL_FLOWS_ERROR_MESSAGE'])
24
+ : undefined,
25
+ });
26
+ }
27
+ throw err;
28
+ }
29
+ }
@@ -1,4 +1,3 @@
1
- import { ForbiddenError } from '@directus/errors';
2
1
  import { parseFilter, validatePayload } from '@directus/utils';
3
2
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
4
3
  import { assign, difference, uniq } from 'lodash-es';
@@ -8,6 +7,7 @@ import { extractRequiredDynamicVariableContext } from '../../utils/extract-requi
8
7
  import { fetchDynamicVariableData } from '../../utils/fetch-dynamic-variable-data.js';
9
8
  import { contextHasDynamicVariables } from '../process-ast/utils/context-has-dynamic-variables.js';
10
9
  import { isFieldNullable } from './lib/is-field-nullable.js';
10
+ import { createCollectionForbiddenError, createFieldsForbiddenError, } from '../process-ast/utils/validate-path/create-error.js';
11
11
  /**
12
12
  * @note this only validates the top-level fields. The expectation is that this function is called
13
13
  * for each level of nested insert separately
@@ -20,21 +20,14 @@ export async function processPayload(options, context) {
20
20
  policies = await fetchPolicies(options.accountability, context);
21
21
  permissions = await fetchPermissions({ action: options.action, policies, collections: [options.collection], accountability: options.accountability }, context);
22
22
  if (permissions.length === 0) {
23
- throw new ForbiddenError({
24
- reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
25
- });
23
+ throw createCollectionForbiddenError('', options.collection);
26
24
  }
27
25
  const fieldsAllowed = uniq(permissions.map(({ fields }) => fields ?? []).flat());
28
26
  if (fieldsAllowed.includes('*') === false) {
29
27
  const fieldsUsed = Object.keys(options.payload);
30
28
  const notAllowed = difference(fieldsUsed, fieldsAllowed);
31
29
  if (notAllowed.length > 0) {
32
- const fieldStr = notAllowed.map((field) => `"${field}"`).join(', ');
33
- throw new ForbiddenError({
34
- reason: notAllowed.length === 1
35
- ? `You don't have permission to access field ${fieldStr} in collection "${options.collection}" or it does not exist.`
36
- : `You don't have permission to access fields ${fieldStr} in collection "${options.collection}" or they do not exist.`,
37
- });
30
+ throw createFieldsForbiddenError('', options.collection, notAllowed);
38
31
  }
39
32
  }
40
33
  permissionValidationRules = permissions.map(({ validation }) => validation);
@@ -1,6 +1,7 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
2
  import { validateCollectionAccess } from './lib/validate-collection-access.js';
3
3
  import { validateItemAccess } from './lib/validate-item-access.js';
4
+ import { createCollectionForbiddenError } from '../process-ast/utils/validate-path/create-error.js';
4
5
  /**
5
6
  * Validate if the current user has access to perform action against the given collection and
6
7
  * optional primary keys. This is done by reading the item from the database using the access
@@ -9,9 +10,7 @@ import { validateItemAccess } from './lib/validate-item-access.js';
9
10
  export async function validateAccess(options, context) {
10
11
  // Skip further validation if the collection does not exist
11
12
  if (!options.skipCollectionExistsCheck && options.collection in context.schema.collections === false) {
12
- throw new ForbiddenError({
13
- reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
14
- });
13
+ throw createCollectionForbiddenError('', options.collection);
15
14
  }
16
15
  if (options.accountability.admin === true) {
17
16
  return;
@@ -1,6 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { toBoolean } from '@directus/utils';
3
- import { scheduleJob } from 'node-schedule';
3
+ import { CronJob } from 'cron';
4
4
  import { useLogger } from '../logger/index.js';
5
5
  import { useMetrics } from '../metrics/index.js';
6
6
  import { validateCron } from '../utils/schedule.js';
@@ -39,6 +39,10 @@ export default async function schedule() {
39
39
  if (!validateCron(String(env['METRICS_SCHEDULE']))) {
40
40
  return false;
41
41
  }
42
- scheduleJob('metrics', String(env['METRICS_SCHEDULE']), handleMetricsJob);
42
+ CronJob.from({
43
+ cronTime: String(env['METRICS_SCHEDULE']),
44
+ onTick: handleMetricsJob,
45
+ start: true,
46
+ });
43
47
  return true;
44
48
  }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Schedule the project status job
3
+ */
4
+ export default function schedule(): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import { random } from 'lodash-es';
2
+ import getDatabase from '../database/index.js';
3
+ import { sendReport } from '../telemetry/index.js';
4
+ import { scheduleSynchronizedJob } from '../utils/schedule.js';
5
+ import { version } from 'directus/version';
6
+ /**
7
+ * Schedule the project status job
8
+ */
9
+ export default async function schedule() {
10
+ const db = getDatabase();
11
+ // Schedules a job at a random time of the day to avoid overloading the telemetry server
12
+ scheduleSynchronizedJob('project-status', `${random(59)} ${random(23)} * * *`, async () => {
13
+ const { project_status, ...ownerInfo } = await db
14
+ .select('project_status', 'project_owner', 'project_usage', 'org_name', 'product_updates', 'project_id')
15
+ .from('directus_settings')
16
+ .first();
17
+ if (project_status !== 'pending')
18
+ return;
19
+ try {
20
+ await sendReport({ version, ...ownerInfo });
21
+ await db.update('project_status', '').from('directus_settings');
22
+ }
23
+ catch {
24
+ // Empty catch
25
+ }
26
+ });
27
+ }
@@ -1,5 +1,5 @@
1
1
  import type { SchemaInspector } from '@directus/schema';
2
- import type { AbstractServiceOptions, Accountability, MutationOptions, SchemaOverview, RawCollection } from '@directus/types';
2
+ import type { AbstractServiceOptions, Accountability, FieldMutationOptions, MutationOptions, RawCollection, SchemaOverview } from '@directus/types';
3
3
  import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Helpers } from '../database/helpers/index.js';
@@ -16,11 +16,11 @@ export declare class CollectionsService {
16
16
  /**
17
17
  * Create a single new collection
18
18
  */
19
- createOne(payload: RawCollection, opts?: MutationOptions): Promise<string>;
19
+ createOne(payload: RawCollection, opts?: FieldMutationOptions): Promise<string>;
20
20
  /**
21
21
  * Create multiple new collections
22
22
  */
23
- createMany(payloads: RawCollection[], opts?: MutationOptions): Promise<string[]>;
23
+ createMany(payloads: RawCollection[], opts?: FieldMutationOptions): Promise<string[]>;
24
24
  /**
25
25
  * Read all collections. Currently doesn't support any query.
26
26
  */
@@ -62,6 +62,7 @@ export class CollectionsService {
62
62
  if (existingCollections.includes(payload.collection)) {
63
63
  throw new InvalidPayloadError({ reason: `Collection "${payload.collection}" already exists` });
64
64
  }
65
+ const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
65
66
  // Create the collection/fields in a transaction so it'll be reverted in case of errors or
66
67
  // permission problems. This might not work reliably in MySQL, as it doesn't support DDL in
67
68
  // transactions.
@@ -114,7 +115,9 @@ export class CollectionsService {
114
115
  await trx.schema.createTable(payload.collection, (table) => {
115
116
  for (const field of payload.fields) {
116
117
  if (field.type && ALIAS_TYPES.includes(field.type) === false) {
117
- fieldsService.addColumnToTable(table, payload.collection, field);
118
+ fieldsService.addColumnToTable(table, payload.collection, field, {
119
+ attemptConcurrentIndex,
120
+ });
118
121
  }
119
122
  }
120
123
  });
@@ -159,6 +162,17 @@ export class CollectionsService {
159
162
  }
160
163
  return payload.collection;
161
164
  });
165
+ // concurrent index creation cannot be done inside the transaction
166
+ if (attemptConcurrentIndex && payload.schema && Array.isArray(payload.fields)) {
167
+ const fieldsService = new FieldsService({ schema: this.schema });
168
+ for (const field of payload.fields) {
169
+ if (field.type && ALIAS_TYPES.includes(field.type) === false) {
170
+ await fieldsService.addColumnIndex(payload.collection, field, {
171
+ attemptConcurrentIndex,
172
+ });
173
+ }
174
+ }
175
+ }
162
176
  return payload.collection;
163
177
  }
164
178
  finally {
@@ -195,6 +209,7 @@ export class CollectionsService {
195
209
  autoPurgeCache: false,
196
210
  autoPurgeSystemCache: false,
197
211
  bypassEmitAction: (params) => nestedActionEvents.push(params),
212
+ attemptConcurrentIndex: Boolean(opts?.attemptConcurrentIndex),
198
213
  });
199
214
  collectionNames.push(name);
200
215
  }
@@ -1,10 +1,18 @@
1
1
  import type { Column, SchemaInspector } from '@directus/schema';
2
- import type { AbstractServiceOptions, Accountability, Field, MutationOptions, RawField, SchemaOverview, Type } from '@directus/types';
2
+ import type { AbstractServiceOptions, Accountability, Field, FieldMutationOptions, MutationOptions, RawField, SchemaOverview, Type } from '@directus/types';
3
3
  import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
+ import { z } from 'zod';
5
6
  import type { Helpers } from '../database/helpers/index.js';
6
7
  import { ItemsService } from './items.js';
7
8
  import { PayloadService } from './payload.js';
9
+ export declare const systemFieldUpdateSchema: z.ZodObject<{
10
+ collection: z.ZodOptional<z.ZodString>;
11
+ field: z.ZodOptional<z.ZodString>;
12
+ schema: z.ZodObject<{
13
+ is_indexed: z.ZodOptional<z.ZodBoolean>;
14
+ }, z.core.$strict>;
15
+ }, z.core.$strict>;
8
16
  export declare class FieldsService {
9
17
  knex: Knex;
10
18
  helpers: Helpers;
@@ -25,9 +33,17 @@ export declare class FieldsService {
25
33
  field: string;
26
34
  type: Type | null;
27
35
  }, table?: Knex.CreateTableBuilder, // allows collection creation to
28
- opts?: MutationOptions): Promise<void>;
29
- updateField(collection: string, field: RawField, opts?: MutationOptions): Promise<string>;
30
- updateFields(collection: string, fields: RawField[], opts?: MutationOptions): Promise<string[]>;
36
+ opts?: FieldMutationOptions): Promise<void>;
37
+ updateField(collection: string, field: RawField, opts?: FieldMutationOptions): Promise<string>;
38
+ updateFields(collection: string, fields: RawField[], opts?: FieldMutationOptions): Promise<string[]>;
31
39
  deleteField(collection: string, field: string, opts?: MutationOptions): Promise<void>;
32
- addColumnToTable(table: Knex.CreateTableBuilder, collection: string, field: RawField | Field, existing?: Column | null): void;
40
+ addColumnToTable(table: Knex.CreateTableBuilder, collection: string, field: RawField | Field, options?: {
41
+ attemptConcurrentIndex?: boolean;
42
+ existing?: Column | null;
43
+ }): void;
44
+ addColumnIndex(collection: string, field: Field | RawField, options?: {
45
+ attemptConcurrentIndex?: boolean;
46
+ knex?: Knex;
47
+ existing?: Column | null;
48
+ }): Promise<void>;
33
49
  }