@directus/api 17.0.1 → 17.1.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/controllers/fields.js +0 -3
- package/dist/operations/item-update/index.js +4 -1
- package/dist/services/fields.js +6 -0
- package/dist/services/import-export.js +61 -26
- package/dist/services/roles.d.ts +9 -4
- package/dist/services/roles.js +50 -2
- package/dist/websocket/controllers/hooks.js +1 -1
- package/package.json +23 -23
|
@@ -126,9 +126,6 @@ router.patch('/:collection/:field', validateCollection, asyncHandler(async (req,
|
|
|
126
126
|
if (error) {
|
|
127
127
|
throw new InvalidPayloadError({ reason: error.message });
|
|
128
128
|
}
|
|
129
|
-
if (req.body.schema && !req.body.type) {
|
|
130
|
-
throw new InvalidPayloadError({ reason: `You need to provide "type" when providing "schema"` });
|
|
131
|
-
}
|
|
132
129
|
const fieldData = req.body;
|
|
133
130
|
if (!fieldData.field)
|
|
134
131
|
fieldData.field = req.params['field'];
|
|
@@ -32,7 +32,10 @@ export default defineOperationApi({
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
let result;
|
|
35
|
-
if (
|
|
35
|
+
if (Array.isArray(payloadObject)) {
|
|
36
|
+
result = await itemsService.updateBatch(payloadObject, { emitEvents: !!emitEvents });
|
|
37
|
+
}
|
|
38
|
+
else if (!key || (Array.isArray(key) && key.length === 0)) {
|
|
36
39
|
result = await itemsService.updateByQuery(sanitizedQueryObject, payloadObject, { emitEvents: !!emitEvents });
|
|
37
40
|
}
|
|
38
41
|
else {
|
package/dist/services/fields.js
CHANGED
|
@@ -300,6 +300,12 @@ export class FieldsService {
|
|
|
300
300
|
}
|
|
301
301
|
const runPostColumnChange = await this.helpers.schema.preColumnChange();
|
|
302
302
|
const nestedActionEvents = [];
|
|
303
|
+
// 'type' is required for further checks on schema update
|
|
304
|
+
if (field.schema && !field.type) {
|
|
305
|
+
const existingType = this.schema.collections[collection]?.fields[field.field]?.type;
|
|
306
|
+
if (existingType)
|
|
307
|
+
field.type = existingType;
|
|
308
|
+
}
|
|
303
309
|
try {
|
|
304
310
|
const hookAdjustedField = await emitter.emitFilter(`fields.update`, field, {
|
|
305
311
|
keys: [field.field],
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
|
|
3
|
+
import { isSystemCollection } from '@directus/system-data';
|
|
3
4
|
import { parseJSON, toArray } from '@directus/utils';
|
|
5
|
+
import { createTmpFile } from '@directus/utils/node';
|
|
4
6
|
import { queue } from 'async';
|
|
5
7
|
import destroyStream from 'destroy';
|
|
6
8
|
import { dump as toYAML } from 'js-yaml';
|
|
7
9
|
import { parse as toXML } from 'js2xmlparser';
|
|
8
10
|
import { Parser as CSVParser, transforms as CSVTransforms } from 'json2csv';
|
|
9
|
-
import { createReadStream } from 'node:fs';
|
|
11
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
10
12
|
import { appendFile } from 'node:fs/promises';
|
|
11
13
|
import Papa from 'papaparse';
|
|
12
14
|
import StreamArray from 'stream-json/streamers/StreamArray.js';
|
|
@@ -20,7 +22,6 @@ import { FilesService } from './files.js';
|
|
|
20
22
|
import { ItemsService } from './items.js';
|
|
21
23
|
import { NotificationsService } from './notifications.js';
|
|
22
24
|
import { UsersService } from './users.js';
|
|
23
|
-
import { isSystemCollection } from '@directus/system-data';
|
|
24
25
|
const env = useEnv();
|
|
25
26
|
const logger = useLogger();
|
|
26
27
|
export class ImportService {
|
|
@@ -86,7 +87,10 @@ export class ImportService {
|
|
|
86
87
|
});
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
|
-
importCSV(collection, stream) {
|
|
90
|
+
async importCSV(collection, stream) {
|
|
91
|
+
const tmpFile = await createTmpFile().catch(() => null);
|
|
92
|
+
if (!tmpFile)
|
|
93
|
+
throw new Error('Failed to create temporary file for import');
|
|
90
94
|
const nestedActionEvents = [];
|
|
91
95
|
return this.knex.transaction((trx) => {
|
|
92
96
|
const service = new ItemsService(collection, {
|
|
@@ -116,35 +120,66 @@ export class ImportService {
|
|
|
116
120
|
transform,
|
|
117
121
|
};
|
|
118
122
|
return new Promise((resolve, reject) => {
|
|
119
|
-
stream
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (obj[field] === undefined) {
|
|
125
|
-
delete obj[field];
|
|
123
|
+
const streams = [stream];
|
|
124
|
+
const cleanup = (destroy = true) => {
|
|
125
|
+
if (destroy) {
|
|
126
|
+
for (const stream of streams) {
|
|
127
|
+
destroyStream(stream);
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
tmpFile.cleanup().catch(() => {
|
|
131
|
+
logger.warn(`Failed to cleanup temporary import file (${tmpFile.path})`);
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
saveQueue.error((error) => {
|
|
135
|
+
reject(error);
|
|
136
|
+
});
|
|
137
|
+
const fileWriteStream = createWriteStream(tmpFile.path)
|
|
138
|
+
.on('error', (error) => {
|
|
139
|
+
cleanup();
|
|
140
|
+
reject(new Error('Error while writing import data to temporary file', { cause: error }));
|
|
133
141
|
})
|
|
134
|
-
.on('
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
.on('finish', () => {
|
|
143
|
+
const fileReadStream = createReadStream(tmpFile.path).on('error', (error) => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(new Error('Error while reading import data from temporary file', { cause: error }));
|
|
146
|
+
});
|
|
147
|
+
streams.push(fileReadStream);
|
|
148
|
+
fileReadStream
|
|
149
|
+
.pipe(Papa.parse(Papa.NODE_STREAM_INPUT, PapaOptions))
|
|
150
|
+
.on('data', (obj) => {
|
|
151
|
+
// Filter out all undefined fields
|
|
152
|
+
for (const field in obj) {
|
|
153
|
+
if (obj[field] === undefined) {
|
|
154
|
+
delete obj[field];
|
|
155
|
+
}
|
|
141
156
|
}
|
|
142
|
-
|
|
157
|
+
saveQueue.push(obj);
|
|
158
|
+
})
|
|
159
|
+
.on('error', (error) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(new InvalidPayloadError({ reason: error.message }));
|
|
162
|
+
})
|
|
163
|
+
.on('end', () => {
|
|
164
|
+
cleanup(false);
|
|
165
|
+
// In case of empty CSV file
|
|
166
|
+
if (!saveQueue.started)
|
|
167
|
+
return resolve();
|
|
168
|
+
saveQueue.drain(() => {
|
|
169
|
+
for (const nestedActionEvent of nestedActionEvents) {
|
|
170
|
+
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
171
|
+
}
|
|
172
|
+
return resolve();
|
|
173
|
+
});
|
|
143
174
|
});
|
|
144
175
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
streams.push(fileWriteStream);
|
|
177
|
+
stream
|
|
178
|
+
.on('error', (error) => {
|
|
179
|
+
cleanup();
|
|
180
|
+
reject(new Error('Error while retrieving import data', { cause: error }));
|
|
181
|
+
})
|
|
182
|
+
.pipe(fileWriteStream);
|
|
148
183
|
});
|
|
149
184
|
});
|
|
150
185
|
}
|
package/dist/services/roles.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { Query } from '@directus/types';
|
|
2
|
-
import type { AbstractServiceOptions, MutationOptions, PrimaryKey } from '../types/index.js';
|
|
2
|
+
import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
|
|
3
3
|
import { ItemsService } from './items.js';
|
|
4
4
|
export declare class RolesService extends ItemsService {
|
|
5
5
|
constructor(options: AbstractServiceOptions);
|
|
6
6
|
private checkForOtherAdminRoles;
|
|
7
7
|
private checkForOtherAdminUsers;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
private isIpAccessValid;
|
|
9
|
+
private assertValidIpAccess;
|
|
10
|
+
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
11
|
+
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
12
|
+
updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
13
|
+
updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
14
|
+
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
15
|
+
updateByQuery(query: Query, data: Partial<Item>, opts?: MutationOptions | undefined): Promise<PrimaryKey[]>;
|
|
11
16
|
deleteOne(key: PrimaryKey): Promise<PrimaryKey>;
|
|
12
17
|
deleteMany(keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
|
13
18
|
deleteByQuery(query: Query, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
package/dist/services/roles.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ForbiddenError, UnprocessableContentError } from '@directus/errors';
|
|
1
|
+
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
2
|
+
import { getMatch } from 'ip-matching';
|
|
2
3
|
import { ItemsService } from './items.js';
|
|
3
4
|
import { PermissionsService } from './permissions.js';
|
|
4
5
|
import { PresetsService } from './presets.js';
|
|
@@ -74,7 +75,7 @@ export class RolesService extends ItemsService {
|
|
|
74
75
|
.count('*', { as: 'count' })
|
|
75
76
|
.from('directus_users')
|
|
76
77
|
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
|
|
77
|
-
.whereNotIn('directus_users.id', usersAdded)
|
|
78
|
+
.whereNotIn('directus_users.id', usersAdded.map((user) => user.id))
|
|
78
79
|
.andWhere({ 'directus_roles.admin_access': true, status: 'active' })
|
|
79
80
|
.first();
|
|
80
81
|
const otherAdminUsersCount = Number(otherAdminUsers?.count ?? 0);
|
|
@@ -121,7 +122,46 @@ export class RolesService extends ItemsService {
|
|
|
121
122
|
}
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
125
|
+
isIpAccessValid(value) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
if (value === null)
|
|
129
|
+
return true;
|
|
130
|
+
if (Array.isArray(value) && value.length === 0)
|
|
131
|
+
return true;
|
|
132
|
+
for (const ip of value) {
|
|
133
|
+
if (typeof ip !== 'string' || ip.includes('*'))
|
|
134
|
+
return false;
|
|
135
|
+
try {
|
|
136
|
+
const match = getMatch(ip);
|
|
137
|
+
if (match.type == 'IPMask')
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
assertValidIpAccess(partialItem) {
|
|
147
|
+
if ('ip_access' in partialItem && !this.isIpAccessValid(partialItem['ip_access'])) {
|
|
148
|
+
throw new InvalidPayloadError({
|
|
149
|
+
reason: 'IP Access contains an incorrect value. Valid values are: IP addresses, IP ranges and CIDR blocks',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async createOne(data, opts) {
|
|
154
|
+
this.assertValidIpAccess(data);
|
|
155
|
+
return super.createOne(data, opts);
|
|
156
|
+
}
|
|
157
|
+
async createMany(data, opts) {
|
|
158
|
+
for (const partialItem of data) {
|
|
159
|
+
this.assertValidIpAccess(partialItem);
|
|
160
|
+
}
|
|
161
|
+
return super.createMany(data, opts);
|
|
162
|
+
}
|
|
124
163
|
async updateOne(key, data, opts) {
|
|
164
|
+
this.assertValidIpAccess(data);
|
|
125
165
|
try {
|
|
126
166
|
if ('users' in data) {
|
|
127
167
|
await this.checkForOtherAdminUsers(key, data['users']);
|
|
@@ -133,6 +173,9 @@ export class RolesService extends ItemsService {
|
|
|
133
173
|
return super.updateOne(key, data, opts);
|
|
134
174
|
}
|
|
135
175
|
async updateBatch(data, opts) {
|
|
176
|
+
for (const partialItem of data) {
|
|
177
|
+
this.assertValidIpAccess(partialItem);
|
|
178
|
+
}
|
|
136
179
|
const primaryKeyField = this.schema.collections[this.collection].primary;
|
|
137
180
|
const keys = data.map((item) => item[primaryKeyField]);
|
|
138
181
|
const setsToNoAdmin = data.some((item) => item['admin_access'] === false);
|
|
@@ -147,6 +190,7 @@ export class RolesService extends ItemsService {
|
|
|
147
190
|
return super.updateBatch(data, opts);
|
|
148
191
|
}
|
|
149
192
|
async updateMany(keys, data, opts) {
|
|
193
|
+
this.assertValidIpAccess(data);
|
|
150
194
|
try {
|
|
151
195
|
if ('admin_access' in data && data['admin_access'] === false) {
|
|
152
196
|
await this.checkForOtherAdminRoles(keys);
|
|
@@ -157,6 +201,10 @@ export class RolesService extends ItemsService {
|
|
|
157
201
|
}
|
|
158
202
|
return super.updateMany(keys, data, opts);
|
|
159
203
|
}
|
|
204
|
+
async updateByQuery(query, data, opts) {
|
|
205
|
+
this.assertValidIpAccess(data);
|
|
206
|
+
return super.updateByQuery(query, data, opts);
|
|
207
|
+
}
|
|
160
208
|
async deleteOne(key) {
|
|
161
209
|
await this.deleteMany([key]);
|
|
162
210
|
return key;
|
|
@@ -130,7 +130,7 @@ function registerSortHooks() {
|
|
|
130
130
|
*/
|
|
131
131
|
function registerAction(event, transform) {
|
|
132
132
|
const messenger = useBus();
|
|
133
|
-
emitter.onAction(event,
|
|
133
|
+
emitter.onAction(event, (data) => {
|
|
134
134
|
// push the event through the Redis pub/sub
|
|
135
135
|
messenger.publish('websocket.event', transform(data));
|
|
136
136
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "17.0
|
|
3
|
+
"version": "17.1.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"graphql-ws": "5.15.0",
|
|
96
96
|
"helmet": "7.1.0",
|
|
97
97
|
"icc": "3.0.0",
|
|
98
|
-
"inquirer": "9.2.
|
|
98
|
+
"inquirer": "9.2.15",
|
|
99
99
|
"ioredis": "5.3.2",
|
|
100
100
|
"ip-matching": "2.1.2",
|
|
101
101
|
"isolated-vm": "4.7.2",
|
|
@@ -107,14 +107,14 @@
|
|
|
107
107
|
"keyv": "4.5.4",
|
|
108
108
|
"knex": "3.1.0",
|
|
109
109
|
"ldapjs": "2.3.3",
|
|
110
|
-
"liquidjs": "10.10.
|
|
110
|
+
"liquidjs": "10.10.1",
|
|
111
111
|
"lodash-es": "4.17.21",
|
|
112
112
|
"marked": "12.0.0",
|
|
113
113
|
"micromustache": "8.0.3",
|
|
114
114
|
"mime-types": "2.1.35",
|
|
115
115
|
"minimatch": "9.0.3",
|
|
116
116
|
"ms": "2.1.3",
|
|
117
|
-
"nanoid": "5.0.
|
|
117
|
+
"nanoid": "5.0.6",
|
|
118
118
|
"node-machine-id": "1.1.12",
|
|
119
119
|
"node-schedule": "2.1.1",
|
|
120
120
|
"nodemailer": "6.9.9",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"p-limit": "5.0.0",
|
|
127
127
|
"p-queue": "8.0.1",
|
|
128
128
|
"papaparse": "5.4.1",
|
|
129
|
-
"pino": "8.
|
|
129
|
+
"pino": "8.19.0",
|
|
130
130
|
"pino-http": "9.0.0",
|
|
131
131
|
"pino-http-print": "3.1.0",
|
|
132
132
|
"pino-pretty": "10.3.1",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"rate-limiter-flexible": "4.0.1",
|
|
135
135
|
"rollup": "4.10.0",
|
|
136
136
|
"samlify": "2.8.10",
|
|
137
|
-
"sanitize-html": "2.
|
|
137
|
+
"sanitize-html": "2.12.0",
|
|
138
138
|
"sharp": "0.33.2",
|
|
139
139
|
"snappy": "7.2.2",
|
|
140
140
|
"stream-json": "1.8.0",
|
|
@@ -145,30 +145,30 @@
|
|
|
145
145
|
"ws": "8.16.0",
|
|
146
146
|
"zod": "3.22.4",
|
|
147
147
|
"zod-validation-error": "3.0.2",
|
|
148
|
-
"@directus/app": "10.15.
|
|
148
|
+
"@directus/app": "10.15.2",
|
|
149
149
|
"@directus/constants": "11.0.3",
|
|
150
|
-
"@directus/env": "1.0.2",
|
|
151
150
|
"@directus/errors": "0.2.3",
|
|
152
|
-
"@directus/
|
|
153
|
-
"@directus/extensions
|
|
154
|
-
"@directus/
|
|
151
|
+
"@directus/env": "1.0.2",
|
|
152
|
+
"@directus/extensions": "0.3.3",
|
|
153
|
+
"@directus/extensions-sdk": "10.3.4",
|
|
154
|
+
"@directus/memory": "1.0.3",
|
|
155
155
|
"@directus/schema": "11.0.1",
|
|
156
|
-
"@directus/memory": "1.0.2",
|
|
157
156
|
"@directus/specs": "10.2.6",
|
|
157
|
+
"@directus/pressure": "1.0.16",
|
|
158
158
|
"@directus/storage-driver-azure": "10.0.17",
|
|
159
|
-
"@directus/storage-driver-cloudinary": "10.0.17",
|
|
160
159
|
"@directus/storage": "10.0.10",
|
|
160
|
+
"@directus/storage-driver-cloudinary": "10.0.17",
|
|
161
161
|
"@directus/storage-driver-gcs": "10.0.17",
|
|
162
|
+
"@directus/storage-driver-s3": "10.0.18",
|
|
162
163
|
"@directus/storage-driver-local": "10.0.17",
|
|
163
|
-
"@directus/storage-driver-s3": "10.0.17",
|
|
164
|
-
"@directus/system-data": "1.0.0",
|
|
165
164
|
"@directus/storage-driver-supabase": "1.0.9",
|
|
165
|
+
"@directus/system-data": "1.0.0",
|
|
166
166
|
"@directus/utils": "11.0.5",
|
|
167
|
-
"
|
|
168
|
-
"directus": "
|
|
167
|
+
"directus": "10.9.3",
|
|
168
|
+
"@directus/validation": "0.0.12"
|
|
169
169
|
},
|
|
170
170
|
"devDependencies": {
|
|
171
|
-
"@ngneat/falso": "7.
|
|
171
|
+
"@ngneat/falso": "7.2.0",
|
|
172
172
|
"@types/async": "3.2.24",
|
|
173
173
|
"@types/busboy": "1.5.3",
|
|
174
174
|
"@types/bytes": "3.1.4",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"@types/lodash-es": "4.17.12",
|
|
191
191
|
"@types/mime-types": "2.1.4",
|
|
192
192
|
"@types/ms": "0.7.34",
|
|
193
|
-
"@types/node": "18.19.
|
|
193
|
+
"@types/node": "18.19.17",
|
|
194
194
|
"@types/node-schedule": "2.1.6",
|
|
195
195
|
"@types/nodemailer": "6.4.14",
|
|
196
196
|
"@types/object-hash": "3.0.6",
|
|
@@ -202,15 +202,15 @@
|
|
|
202
202
|
"@types/uuid-validate": "0.0.3",
|
|
203
203
|
"@types/wellknown": "0.5.8",
|
|
204
204
|
"@types/ws": "8.5.10",
|
|
205
|
-
"@vitest/coverage-v8": "1.
|
|
205
|
+
"@vitest/coverage-v8": "1.3.1",
|
|
206
206
|
"copyfiles": "2.4.1",
|
|
207
207
|
"form-data": "4.0.0",
|
|
208
208
|
"knex-mock-client": "2.0.1",
|
|
209
209
|
"typescript": "5.3.3",
|
|
210
|
-
"vitest": "1.
|
|
211
|
-
"@directus/types": "11.0.6",
|
|
210
|
+
"vitest": "1.3.0",
|
|
212
211
|
"@directus/tsconfig": "1.0.1",
|
|
213
|
-
"@directus/random": "0.2.6"
|
|
212
|
+
"@directus/random": "0.2.6",
|
|
213
|
+
"@directus/types": "11.0.6"
|
|
214
214
|
},
|
|
215
215
|
"optionalDependencies": {
|
|
216
216
|
"@keyv/redis": "2.8.4",
|