@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.
- package/dist/app.js +2 -0
- package/dist/auth/auth.d.ts +2 -1
- package/dist/auth/auth.js +7 -2
- package/dist/auth/drivers/ldap.d.ts +0 -2
- package/dist/auth/drivers/ldap.js +9 -7
- package/dist/auth/drivers/oauth2.d.ts +0 -2
- package/dist/auth/drivers/oauth2.js +11 -8
- package/dist/auth/drivers/openid.d.ts +0 -2
- package/dist/auth/drivers/openid.js +11 -8
- package/dist/auth/drivers/saml.d.ts +0 -2
- package/dist/auth/drivers/saml.js +5 -5
- package/dist/auth.js +1 -2
- package/dist/cli/commands/bootstrap/index.js +12 -33
- package/dist/cli/commands/init/index.js +1 -1
- package/dist/cli/commands/schema/apply.d.ts +4 -0
- package/dist/cli/commands/schema/apply.js +26 -3
- package/dist/controllers/collections.js +7 -2
- package/dist/controllers/fields.js +31 -8
- package/dist/controllers/server.js +26 -1
- package/dist/controllers/settings.js +9 -2
- package/dist/controllers/users.js +2 -2
- package/dist/database/helpers/fn/types.js +3 -3
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +23 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +25 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/oracle.js +13 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/postgres.js +13 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
- package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
- package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
- package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
- package/dist/database/run-ast/lib/apply-query/index.js +4 -6
- package/dist/database/run-ast/lib/apply-query/search.js +2 -0
- package/dist/database/run-ast/lib/get-db-query.js +7 -6
- package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
- package/dist/database/run-ast/utils/generate-alias.js +57 -0
- package/dist/flows.js +1 -0
- package/dist/mcp/schema.d.ts +14 -14
- package/dist/mcp/schema.js +6 -6
- package/dist/mcp/server.d.ts +9 -3
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools/collections.d.ts +1 -1
- package/dist/mcp/tools/fields.d.ts +1 -1
- package/dist/mcp/tools/files.d.ts +25 -25
- package/dist/mcp/tools/flows.d.ts +36 -36
- package/dist/mcp/tools/folders.d.ts +18 -18
- package/dist/mcp/tools/items.d.ts +18 -18
- package/dist/mcp/tools/operations.d.ts +19 -19
- package/dist/mcp/tools/prompts/items.md +1 -1
- package/dist/metrics/lib/create-metrics.js +16 -25
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/operations/mail/index.js +3 -1
- package/dist/operations/mail/rate-limiter.d.ts +1 -0
- package/dist/operations/mail/rate-limiter.js +29 -0
- package/dist/permissions/modules/process-payload/process-payload.js +3 -10
- package/dist/permissions/modules/validate-access/validate-access.js +2 -3
- package/dist/schedules/metrics.js +6 -2
- package/dist/schedules/project.d.ts +4 -0
- package/dist/schedules/project.js +27 -0
- package/dist/services/collections.d.ts +3 -3
- package/dist/services/collections.js +16 -1
- package/dist/services/fields.d.ts +21 -5
- package/dist/services/fields.js +105 -28
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +49 -5
- package/dist/services/graphql/schema/parse-query.js +8 -8
- package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
- package/dist/services/graphql/utils/aggregate-query.js +5 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
- package/dist/services/import-export.d.ts +9 -1
- package/dist/services/import-export.js +287 -101
- package/dist/services/items.d.ts +1 -1
- package/dist/services/items.js +36 -20
- package/dist/services/mail/index.js +2 -0
- package/dist/services/mail/rate-limiter.d.ts +1 -0
- package/dist/services/mail/rate-limiter.js +29 -0
- package/dist/services/meta.js +28 -24
- package/dist/services/schema.js +4 -1
- package/dist/services/server.d.ts +1 -0
- package/dist/services/server.js +14 -18
- package/dist/services/settings.d.ts +2 -1
- package/dist/services/settings.js +15 -0
- package/dist/services/tus/server.js +14 -9
- package/dist/telemetry/lib/get-report.js +4 -4
- package/dist/telemetry/lib/send-report.d.ts +6 -1
- package/dist/telemetry/lib/send-report.js +3 -1
- package/dist/telemetry/types/report.d.ts +17 -1
- package/dist/telemetry/utils/get-settings.d.ts +9 -0
- package/dist/telemetry/utils/get-settings.js +14 -0
- package/dist/test-utils/README.md +760 -0
- package/dist/test-utils/cache.d.ts +51 -0
- package/dist/test-utils/cache.js +59 -0
- package/dist/test-utils/database.d.ts +48 -0
- package/dist/test-utils/database.js +52 -0
- package/dist/test-utils/emitter.d.ts +35 -0
- package/dist/test-utils/emitter.js +38 -0
- package/dist/test-utils/fields-service.d.ts +28 -0
- package/dist/test-utils/fields-service.js +36 -0
- package/dist/test-utils/items-service.d.ts +23 -0
- package/dist/test-utils/items-service.js +37 -0
- package/dist/test-utils/knex.d.ts +164 -0
- package/dist/test-utils/knex.js +268 -0
- package/dist/test-utils/schema.d.ts +26 -0
- package/dist/test-utils/schema.js +35 -0
- package/dist/types/auth.d.ts +0 -2
- package/dist/utils/apply-diff.js +15 -0
- package/dist/utils/create-admin.d.ts +11 -0
- package/dist/utils/create-admin.js +50 -0
- package/dist/utils/get-schema.js +5 -3
- package/dist/utils/get-snapshot-diff.js +49 -5
- package/dist/utils/get-snapshot.js +13 -7
- package/dist/utils/sanitize-schema.d.ts +11 -4
- package/dist/utils/sanitize-schema.js +9 -6
- package/dist/utils/schedule.js +15 -19
- package/dist/utils/validate-diff.js +31 -0
- package/dist/utils/validate-snapshot.js +7 -0
- package/dist/websocket/controllers/hooks.js +12 -20
- package/dist/websocket/messages.d.ts +3 -3
- package/package.json +63 -65
- package/dist/cli/utils/defaults.d.ts +0 -4
- package/dist/cli/utils/defaults.js +0 -17
- package/dist/telemetry/utils/get-project-id.d.ts +0 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
sum
|
|
141
|
-
avg
|
|
142
|
-
min
|
|
143
|
-
max
|
|
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
|
-
|
|
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(
|
|
96
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}db_${client}_connection_errors`);
|
|
96
97
|
if (!metric) {
|
|
97
98
|
metric = new Counter({
|
|
98
|
-
name:
|
|
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(
|
|
110
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}db_${client}_response_time_ms`);
|
|
110
111
|
if (!metric) {
|
|
111
112
|
metric = new Histogram({
|
|
112
|
-
name:
|
|
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(
|
|
127
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}cache_${env['CACHE_STORE']}_connection_errors`);
|
|
127
128
|
if (!metric) {
|
|
128
129
|
metric = new Counter({
|
|
129
|
-
name:
|
|
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(
|
|
140
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}redis_connection_errors`);
|
|
140
141
|
if (!metric) {
|
|
141
142
|
metric = new Counter({
|
|
142
|
-
name:
|
|
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(
|
|
153
|
+
let metric = register.getSingleMetric(`${metricNamePrefix}storage_${location}_connection_errors`);
|
|
153
154
|
if (!metric) {
|
|
154
155
|
metric = new Counter({
|
|
155
|
-
name:
|
|
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(`
|
|
186
|
-
await cache.delete(`
|
|
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(`
|
|
200
|
-
await redis.del(`
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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,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,
|
|
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?:
|
|
19
|
+
createOne(payload: RawCollection, opts?: FieldMutationOptions): Promise<string>;
|
|
20
20
|
/**
|
|
21
21
|
* Create multiple new collections
|
|
22
22
|
*/
|
|
23
|
-
createMany(payloads: RawCollection[], opts?:
|
|
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?:
|
|
29
|
-
updateField(collection: string, field: RawField, opts?:
|
|
30
|
-
updateFields(collection: string, fields: RawField[], opts?:
|
|
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,
|
|
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
|
}
|