@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
|
|
2
|
+
import { createError, ErrorCode, ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
|
|
3
3
|
import { isSystemCollection } from '@directus/system-data';
|
|
4
4
|
import { parseJSON, toArray } from '@directus/utils';
|
|
5
5
|
import { createTmpFile } from '@directus/utils/node';
|
|
@@ -28,6 +28,126 @@ import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js'
|
|
|
28
28
|
import { set } from 'lodash-es';
|
|
29
29
|
const env = useEnv();
|
|
30
30
|
const logger = useLogger();
|
|
31
|
+
const MAX_IMPORT_ERRORS = env['MAX_IMPORT_ERRORS'];
|
|
32
|
+
export function createErrorTracker() {
|
|
33
|
+
let genericError;
|
|
34
|
+
// For errors with field / type (joi validation or DB with field)
|
|
35
|
+
const fieldErrors = new Map();
|
|
36
|
+
let capturedErrorCount = 0;
|
|
37
|
+
let isLimitReached = false;
|
|
38
|
+
function convertToRanges(rows, minRangeSize = 4) {
|
|
39
|
+
const sorted = Array.from(new Set(rows)).sort((a, b) => a - b);
|
|
40
|
+
const result = [];
|
|
41
|
+
if (sorted.length === 0)
|
|
42
|
+
return [];
|
|
43
|
+
let start = sorted[0];
|
|
44
|
+
let prev = sorted[0];
|
|
45
|
+
let count = 1;
|
|
46
|
+
const nonConsecutive = [];
|
|
47
|
+
const flush = () => {
|
|
48
|
+
if (count >= minRangeSize) {
|
|
49
|
+
result.push({ type: 'range', start, end: prev });
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
for (let i = start; i <= prev; i++) {
|
|
53
|
+
nonConsecutive.push(i);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
58
|
+
const current = sorted[i];
|
|
59
|
+
if (current === prev + 1) {
|
|
60
|
+
prev = current;
|
|
61
|
+
count++;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
flush();
|
|
65
|
+
start = prev = current;
|
|
66
|
+
count = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
flush();
|
|
70
|
+
// Add non-consecutive rows as a single "lines" entry
|
|
71
|
+
if (nonConsecutive.length > 0) {
|
|
72
|
+
result.push({ type: 'lines', rows: nonConsecutive });
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function addCapturedError(err, rowNumber) {
|
|
77
|
+
const field = err.extensions?.field;
|
|
78
|
+
if (field) {
|
|
79
|
+
const type = err.extensions?.type;
|
|
80
|
+
const substring = err.extensions?.substring;
|
|
81
|
+
const valid = err.extensions?.valid;
|
|
82
|
+
const invalid = err.extensions?.invalid;
|
|
83
|
+
let key = type ? `${field}|${type}` : field;
|
|
84
|
+
if (substring !== undefined)
|
|
85
|
+
key += `|substring:${substring}`;
|
|
86
|
+
if (valid !== undefined)
|
|
87
|
+
key += `|valid:${JSON.stringify(valid)}`;
|
|
88
|
+
if (invalid !== undefined)
|
|
89
|
+
key += `|invalid:${JSON.stringify(invalid)}`;
|
|
90
|
+
if (!fieldErrors.has(err.code)) {
|
|
91
|
+
fieldErrors.set(err.code, new Map());
|
|
92
|
+
}
|
|
93
|
+
const errorsByCode = fieldErrors.get(err.code);
|
|
94
|
+
if (!errorsByCode.has(key)) {
|
|
95
|
+
errorsByCode.set(key, {
|
|
96
|
+
message: err.message,
|
|
97
|
+
rowNumbers: [],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
errorsByCode.get(key).rowNumbers.push(rowNumber);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
genericError = err;
|
|
104
|
+
}
|
|
105
|
+
capturedErrorCount++;
|
|
106
|
+
if (capturedErrorCount >= MAX_IMPORT_ERRORS) {
|
|
107
|
+
isLimitReached = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function hasGenericError() {
|
|
111
|
+
return genericError !== undefined;
|
|
112
|
+
}
|
|
113
|
+
function buildFinalErrors() {
|
|
114
|
+
if (genericError) {
|
|
115
|
+
return [genericError];
|
|
116
|
+
}
|
|
117
|
+
return Array.from(fieldErrors.entries()).flatMap(([code, fieldMap]) => Array.from(fieldMap.entries()).map(([compositeKey, errorData]) => {
|
|
118
|
+
const parts = compositeKey.split('|');
|
|
119
|
+
const field = parts[0];
|
|
120
|
+
const type = parts[1];
|
|
121
|
+
const extensions = {};
|
|
122
|
+
for (let i = 2; i < parts.length; i++) {
|
|
123
|
+
const [paramType, paramValue] = parts[i]?.split(':', 2) ?? [];
|
|
124
|
+
if (!paramType || paramValue === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
extensions[paramType] = JSON.parse(paramValue);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
extensions[paramType] = paramValue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const ErrorClass = createError(code, errorData.message, 400);
|
|
134
|
+
return new ErrorClass({
|
|
135
|
+
field,
|
|
136
|
+
type,
|
|
137
|
+
...extensions,
|
|
138
|
+
rows: convertToRanges(errorData.rowNumbers),
|
|
139
|
+
});
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
addCapturedError,
|
|
144
|
+
buildFinalErrors,
|
|
145
|
+
getCount: () => capturedErrorCount,
|
|
146
|
+
hasErrors: () => capturedErrorCount > 0 || hasGenericError(),
|
|
147
|
+
shouldStop: () => isLimitReached || hasGenericError(),
|
|
148
|
+
hasGenericError,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
31
151
|
export class ImportService {
|
|
32
152
|
knex;
|
|
33
153
|
accountability;
|
|
@@ -71,37 +191,72 @@ export class ImportService {
|
|
|
71
191
|
async importJSON(collection, stream) {
|
|
72
192
|
const extractJSON = StreamArray.withParser();
|
|
73
193
|
const nestedActionEvents = [];
|
|
74
|
-
|
|
194
|
+
const errorTracker = createErrorTracker();
|
|
195
|
+
return transaction(this.knex, async (trx) => {
|
|
75
196
|
const service = getService(collection, {
|
|
76
197
|
knex: trx,
|
|
77
198
|
schema: this.schema,
|
|
78
199
|
accountability: this.accountability,
|
|
79
200
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
201
|
+
try {
|
|
202
|
+
await new Promise((resolve, reject) => {
|
|
203
|
+
let rowNumber = 1;
|
|
204
|
+
const saveQueue = queue(async (task) => {
|
|
205
|
+
if (errorTracker.shouldStop())
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
const result = await service.upsertOne(task.data, {
|
|
209
|
+
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
|
210
|
+
});
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
for (const err of toArray(error)) {
|
|
215
|
+
errorTracker.addCapturedError(err, task.rowNumber);
|
|
216
|
+
if (errorTracker.shouldStop()) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (errorTracker.shouldStop()) {
|
|
221
|
+
saveQueue.kill();
|
|
222
|
+
destroyStream(stream);
|
|
223
|
+
destroyStream(extractJSON);
|
|
224
|
+
reject();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
100
227
|
}
|
|
101
|
-
|
|
228
|
+
});
|
|
229
|
+
stream.pipe(extractJSON);
|
|
230
|
+
extractJSON.on('data', ({ value }) => {
|
|
231
|
+
saveQueue.push({ data: value, rowNumber: rowNumber++ });
|
|
232
|
+
});
|
|
233
|
+
extractJSON.on('error', (err) => {
|
|
234
|
+
destroyStream(stream);
|
|
235
|
+
destroyStream(extractJSON);
|
|
236
|
+
reject(new InvalidPayloadError({ reason: err.message }));
|
|
237
|
+
});
|
|
238
|
+
extractJSON.on('end', () => {
|
|
239
|
+
// In case of empty JSON file
|
|
240
|
+
if (!saveQueue.started)
|
|
241
|
+
return resolve();
|
|
242
|
+
saveQueue.drain(() => {
|
|
243
|
+
if (errorTracker.hasErrors()) {
|
|
244
|
+
return reject();
|
|
245
|
+
}
|
|
246
|
+
for (const nestedActionEvent of nestedActionEvents) {
|
|
247
|
+
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
248
|
+
}
|
|
249
|
+
return resolve();
|
|
250
|
+
});
|
|
102
251
|
});
|
|
103
252
|
});
|
|
104
|
-
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (!error && errorTracker.hasErrors()) {
|
|
256
|
+
throw errorTracker.buildFinalErrors();
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
105
260
|
});
|
|
106
261
|
}
|
|
107
262
|
async importCSV(collection, stream) {
|
|
@@ -109,98 +264,129 @@ export class ImportService {
|
|
|
109
264
|
if (!tmpFile)
|
|
110
265
|
throw new Error('Failed to create temporary file for import');
|
|
111
266
|
const nestedActionEvents = [];
|
|
112
|
-
|
|
267
|
+
const errorTracker = createErrorTracker();
|
|
268
|
+
return transaction(this.knex, async (trx) => {
|
|
113
269
|
const service = getService(collection, {
|
|
114
270
|
knex: trx,
|
|
115
271
|
schema: this.schema,
|
|
116
272
|
accountability: this.accountability,
|
|
117
273
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return value;
|
|
128
|
-
}
|
|
129
|
-
return parsedJson;
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
const PapaOptions = {
|
|
136
|
-
header: true,
|
|
137
|
-
// Trim whitespaces in headers, including the byte order mark (BOM) zero-width no-break space
|
|
138
|
-
transformHeader: (header) => header.trim(),
|
|
139
|
-
transform,
|
|
140
|
-
};
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
const streams = [stream];
|
|
143
|
-
const cleanup = (destroy = true) => {
|
|
144
|
-
if (destroy) {
|
|
145
|
-
for (const stream of streams) {
|
|
146
|
-
destroyStream(stream);
|
|
274
|
+
try {
|
|
275
|
+
await new Promise((resolve, reject) => {
|
|
276
|
+
const streams = [stream];
|
|
277
|
+
let rowNumber = 0;
|
|
278
|
+
const cleanup = (destroy = true) => {
|
|
279
|
+
if (destroy) {
|
|
280
|
+
for (const stream of streams) {
|
|
281
|
+
destroyStream(stream);
|
|
282
|
+
}
|
|
147
283
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const field in obj) {
|
|
173
|
-
if (obj[field] !== undefined) {
|
|
174
|
-
set(result, field, obj[field]);
|
|
284
|
+
tmpFile.cleanup().catch(() => {
|
|
285
|
+
logger.warn(`Failed to cleanup temporary import file (${tmpFile.path})`);
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
const saveQueue = queue(async (task) => {
|
|
289
|
+
if (errorTracker.shouldStop())
|
|
290
|
+
return;
|
|
291
|
+
try {
|
|
292
|
+
const result = await service.upsertOne(task.data, {
|
|
293
|
+
bypassEmitAction: (action) => nestedActionEvents.push(action),
|
|
294
|
+
});
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
for (const err of toArray(error)) {
|
|
299
|
+
errorTracker.addCapturedError(err, task.rowNumber);
|
|
300
|
+
if (errorTracker.shouldStop()) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (errorTracker.shouldStop()) {
|
|
305
|
+
saveQueue.kill();
|
|
306
|
+
cleanup(true);
|
|
307
|
+
reject();
|
|
175
308
|
}
|
|
309
|
+
return;
|
|
176
310
|
}
|
|
177
|
-
|
|
178
|
-
|
|
311
|
+
});
|
|
312
|
+
const fileWriteStream = createWriteStream(tmpFile.path)
|
|
179
313
|
.on('error', (error) => {
|
|
180
314
|
cleanup();
|
|
181
|
-
reject(new
|
|
315
|
+
reject(new Error('Error while writing import data to temporary file', { cause: error }));
|
|
182
316
|
})
|
|
183
|
-
.on('
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
317
|
+
.on('finish', () => {
|
|
318
|
+
const fileReadStream = createReadStream(tmpFile.path).on('error', (error) => {
|
|
319
|
+
cleanup();
|
|
320
|
+
reject(new Error('Error while reading import data from temporary file', { cause: error }));
|
|
321
|
+
});
|
|
322
|
+
streams.push(fileReadStream);
|
|
323
|
+
fileReadStream
|
|
324
|
+
.pipe(Papa.parse(Papa.NODE_STREAM_INPUT, {
|
|
325
|
+
header: true,
|
|
326
|
+
transformHeader: (header) => header.trim(),
|
|
327
|
+
transform: (value) => {
|
|
328
|
+
if (value.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
try {
|
|
331
|
+
const parsedJson = parseJSON(value);
|
|
332
|
+
if (typeof parsedJson === 'number') {
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
return parsedJson;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
}))
|
|
342
|
+
.on('data', (obj) => {
|
|
343
|
+
rowNumber++;
|
|
344
|
+
const result = {};
|
|
345
|
+
for (const field in obj) {
|
|
346
|
+
if (obj[field] !== undefined) {
|
|
347
|
+
set(result, field, obj[field]);
|
|
348
|
+
}
|
|
191
349
|
}
|
|
192
|
-
|
|
350
|
+
saveQueue.push({ data: result, rowNumber });
|
|
351
|
+
})
|
|
352
|
+
.on('error', (error) => {
|
|
353
|
+
cleanup();
|
|
354
|
+
reject(new InvalidPayloadError({ reason: error.message }));
|
|
355
|
+
})
|
|
356
|
+
.on('end', () => {
|
|
357
|
+
// In case of empty CSV file
|
|
358
|
+
if (!saveQueue.started) {
|
|
359
|
+
cleanup(false);
|
|
360
|
+
return resolve();
|
|
361
|
+
}
|
|
362
|
+
saveQueue.drain(() => {
|
|
363
|
+
if (!errorTracker.shouldStop())
|
|
364
|
+
cleanup(false);
|
|
365
|
+
if (errorTracker.hasErrors()) {
|
|
366
|
+
return reject();
|
|
367
|
+
}
|
|
368
|
+
for (const nestedActionEvent of nestedActionEvents) {
|
|
369
|
+
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
370
|
+
}
|
|
371
|
+
return resolve();
|
|
372
|
+
});
|
|
193
373
|
});
|
|
194
374
|
});
|
|
375
|
+
streams.push(fileWriteStream);
|
|
376
|
+
stream
|
|
377
|
+
.on('error', (error) => {
|
|
378
|
+
cleanup();
|
|
379
|
+
reject(new Error('Error while retrieving import data', { cause: error }));
|
|
380
|
+
})
|
|
381
|
+
.pipe(fileWriteStream);
|
|
195
382
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (!error && errorTracker.hasErrors()) {
|
|
386
|
+
throw errorTracker.buildFinalErrors();
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
204
390
|
});
|
|
205
391
|
}
|
|
206
392
|
}
|
package/dist/services/items.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AbstractService, AbstractServiceOptions, Accountability, Item as AnyItem,
|
|
1
|
+
import type { AbstractService, AbstractServiceOptions, Accountability, Item as AnyItem, MutationOptions, MutationTracker, PrimaryKey, Query, QueryOptions, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type Keyv from 'keyv';
|
|
3
3
|
import type { Knex } from 'knex';
|
|
4
4
|
export declare class ItemsService<Item extends AnyItem = AnyItem, Collection extends string = string> implements AbstractService<Item> {
|
package/dist/services/items.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getCache } from '../cache.js';
|
|
|
8
8
|
import { translateDatabaseError } from '../database/errors/translate.js';
|
|
9
9
|
import { getAstFromQuery } from '../database/get-ast-from-query/get-ast-from-query.js';
|
|
10
10
|
import { getHelpers } from '../database/helpers/index.js';
|
|
11
|
-
import getDatabase from '../database/index.js';
|
|
11
|
+
import getDatabase, { getDatabaseClient } from '../database/index.js';
|
|
12
12
|
import { runAst } from '../database/run-ast/run-ast.js';
|
|
13
13
|
import emitter from '../emitter.js';
|
|
14
14
|
import { processAst } from '../permissions/modules/process-ast/process-ast.js';
|
|
@@ -173,10 +173,15 @@ export class ItemsService {
|
|
|
173
173
|
autoIncrementSequenceNeedsToBeReset = true;
|
|
174
174
|
}
|
|
175
175
|
try {
|
|
176
|
+
let returningOptions = undefined;
|
|
177
|
+
// Support MSSQL tables that have triggers.
|
|
178
|
+
if (getDatabaseClient(trx) === 'mssql') {
|
|
179
|
+
returningOptions = { includeTriggerModifications: true };
|
|
180
|
+
}
|
|
176
181
|
const result = await trx
|
|
177
182
|
.insert(payloadWithoutAliases)
|
|
178
183
|
.into(this.collection)
|
|
179
|
-
.returning(primaryKeyField)
|
|
184
|
+
.returning(primaryKeyField, returningOptions)
|
|
180
185
|
.then((result) => result[0]);
|
|
181
186
|
const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
|
|
182
187
|
if (pkField.type === 'uuid') {
|
|
@@ -436,7 +441,7 @@ export class ItemsService {
|
|
|
436
441
|
const filterWithKey = assign({}, query.filter, { [primaryKeyField]: { _eq: key } });
|
|
437
442
|
const queryWithKey = assign({}, query, { filter: filterWithKey });
|
|
438
443
|
let results = [];
|
|
439
|
-
if (query.version) {
|
|
444
|
+
if (query.version && query.version !== 'main') {
|
|
440
445
|
results = [await handleVersion(this, key, queryWithKey, opts)];
|
|
441
446
|
}
|
|
442
447
|
else {
|
|
@@ -806,12 +811,23 @@ export class ItemsService {
|
|
|
806
811
|
}
|
|
807
812
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
808
813
|
validateKeys(this.schema, this.collection, primaryKeyField, keys);
|
|
814
|
+
const keysAfterHooks = opts.emitEvents !== false
|
|
815
|
+
? await emitter.emitFilter(this.eventScope === 'items'
|
|
816
|
+
? ['items.delete', `${this.collection}.items.delete`]
|
|
817
|
+
: `${this.eventScope}.delete`, keys, {
|
|
818
|
+
collection: this.collection,
|
|
819
|
+
}, {
|
|
820
|
+
database: this.knex,
|
|
821
|
+
schema: this.schema,
|
|
822
|
+
accountability: this.accountability,
|
|
823
|
+
})
|
|
824
|
+
: keys;
|
|
809
825
|
if (this.accountability) {
|
|
810
826
|
await validateAccess({
|
|
811
827
|
accountability: this.accountability,
|
|
812
828
|
action: 'delete',
|
|
813
829
|
collection: this.collection,
|
|
814
|
-
primaryKeys:
|
|
830
|
+
primaryKeys: keysAfterHooks,
|
|
815
831
|
}, {
|
|
816
832
|
knex: this.knex,
|
|
817
833
|
schema: this.schema,
|
|
@@ -820,17 +836,8 @@ export class ItemsService {
|
|
|
820
836
|
if (opts.preMutationError) {
|
|
821
837
|
throw opts.preMutationError;
|
|
822
838
|
}
|
|
823
|
-
if (opts.emitEvents !== false) {
|
|
824
|
-
await emitter.emitFilter(this.eventScope === 'items' ? ['items.delete', `${this.collection}.items.delete`] : `${this.eventScope}.delete`, keys, {
|
|
825
|
-
collection: this.collection,
|
|
826
|
-
}, {
|
|
827
|
-
database: this.knex,
|
|
828
|
-
schema: this.schema,
|
|
829
|
-
accountability: this.accountability,
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
839
|
await transaction(this.knex, async (trx) => {
|
|
833
|
-
await trx(this.collection).whereIn(primaryKeyField,
|
|
840
|
+
await trx(this.collection).whereIn(primaryKeyField, keysAfterHooks).delete();
|
|
834
841
|
if (opts.userIntegrityCheckFlags) {
|
|
835
842
|
if (opts.onRequireUserIntegrityCheck) {
|
|
836
843
|
opts.onRequireUserIntegrityCheck(opts.userIntegrityCheckFlags);
|
|
@@ -847,7 +854,7 @@ export class ItemsService {
|
|
|
847
854
|
knex: trx,
|
|
848
855
|
schema: this.schema,
|
|
849
856
|
});
|
|
850
|
-
await activityService.createMany(
|
|
857
|
+
await activityService.createMany(keysAfterHooks.map((key) => ({
|
|
851
858
|
action: Action.DELETE,
|
|
852
859
|
user: this.accountability.user,
|
|
853
860
|
collection: this.collection,
|
|
@@ -867,8 +874,8 @@ export class ItemsService {
|
|
|
867
874
|
? ['items.delete', `${this.collection}.items.delete`]
|
|
868
875
|
: `${this.eventScope}.delete`,
|
|
869
876
|
meta: {
|
|
870
|
-
payload:
|
|
871
|
-
keys:
|
|
877
|
+
payload: keysAfterHooks,
|
|
878
|
+
keys: keysAfterHooks,
|
|
872
879
|
collection: this.collection,
|
|
873
880
|
},
|
|
874
881
|
context: {
|
|
@@ -884,7 +891,7 @@ export class ItemsService {
|
|
|
884
891
|
emitter.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
|
|
885
892
|
}
|
|
886
893
|
}
|
|
887
|
-
return
|
|
894
|
+
return keysAfterHooks;
|
|
888
895
|
}
|
|
889
896
|
/**
|
|
890
897
|
* Read/treat collection as singleton.
|
|
@@ -892,8 +899,17 @@ export class ItemsService {
|
|
|
892
899
|
async readSingleton(query, opts) {
|
|
893
900
|
query = clone(query);
|
|
894
901
|
query.limit = 1;
|
|
895
|
-
|
|
896
|
-
|
|
902
|
+
let record;
|
|
903
|
+
if (query.version && query.version !== 'main') {
|
|
904
|
+
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
905
|
+
const key = (await this.knex.select(primaryKeyField).from(this.collection).first())?.[primaryKeyField];
|
|
906
|
+
if (key) {
|
|
907
|
+
record = await handleVersion(this, key, query, opts);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
record = (await this.readByQuery(query, opts))[0];
|
|
912
|
+
}
|
|
897
913
|
if (!record) {
|
|
898
914
|
let fields = Object.entries(this.schema.collections[this.collection].fields);
|
|
899
915
|
const defaults = {};
|
|
@@ -10,6 +10,7 @@ import emitter from '../../emitter.js';
|
|
|
10
10
|
import { useLogger } from '../../logger/index.js';
|
|
11
11
|
import getMailer from '../../mailer.js';
|
|
12
12
|
import { Url } from '../../utils/url.js';
|
|
13
|
+
import { useEmailRateLimiterQueue } from './rate-limiter.js';
|
|
13
14
|
const env = useEnv();
|
|
14
15
|
const logger = useLogger();
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -37,6 +38,7 @@ export class MailService {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
async send(options) {
|
|
41
|
+
await useEmailRateLimiterQueue();
|
|
40
42
|
const payload = await emitter.emitFilter(`email.send`, options, {});
|
|
41
43
|
if (!payload)
|
|
42
44
|
return null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useEmailRateLimiterQueue(): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { RateLimiterQueue } 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 emailRateLimiterQueue;
|
|
7
|
+
const env = useEnv();
|
|
8
|
+
if (toBoolean(env['RATE_LIMITER_EMAIL_ENABLED']) === true) {
|
|
9
|
+
emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('RATE_LIMITER_EMAIL'), {
|
|
10
|
+
maxQueueSize: Number(env['RATE_LIMITER_EMAIL_QUEUE_SIZE']),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function useEmailRateLimiterQueue() {
|
|
14
|
+
if (!emailRateLimiterQueue)
|
|
15
|
+
return;
|
|
16
|
+
try {
|
|
17
|
+
await emailRateLimiterQueue.removeTokens(1);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (err instanceof Error) {
|
|
21
|
+
throw new EmailLimitExceededError({
|
|
22
|
+
points: 'RATE_LIMITER_EMAIL_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_POINTS']) : undefined,
|
|
23
|
+
duration: 'RATE_LIMITER_EMAIL_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_DURATION']) : undefined,
|
|
24
|
+
message: 'RATE_LIMITER_EMAIL_ERROR_MESSAGE' in env ? String(env['RATE_LIMITER_EMAIL_ERROR_MESSAGE']) : undefined,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|