@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.
@@ -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 (!key || (Array.isArray(key) && key.length === 0)) {
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 {
@@ -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
- .pipe(Papa.parse(Papa.NODE_STREAM_INPUT, PapaOptions))
121
- .on('data', (obj) => {
122
- // Filter out all undefined fields
123
- for (const field in obj) {
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
- saveQueue.push(obj);
129
- })
130
- .on('error', (err) => {
131
- destroyStream(stream);
132
- reject(new InvalidPayloadError({ reason: err.message }));
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('end', () => {
135
- // In case of empty CSV file
136
- if (!saveQueue.started)
137
- return resolve();
138
- saveQueue.drain(() => {
139
- for (const nestedActionEvent of nestedActionEvents) {
140
- emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
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
- return resolve();
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
- saveQueue.error((err) => {
146
- reject(err);
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
  }
@@ -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
- updateOne(key: PrimaryKey, data: Record<string, any>, opts?: MutationOptions): Promise<PrimaryKey>;
9
- updateBatch(data: Record<string, any>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
10
- updateMany(keys: PrimaryKey[], data: Record<string, any>, opts?: MutationOptions): Promise<PrimaryKey[]>;
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[]>;
@@ -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, async (data) => {
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.1",
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.14",
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.0",
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.5",
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.18.0",
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.11.0",
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.1",
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/extensions": "0.3.2",
153
- "@directus/extensions-sdk": "10.3.3",
154
- "@directus/pressure": "1.0.16",
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
- "@directus/validation": "0.0.12",
168
- "directus": "10.9.2"
167
+ "directus": "10.9.3",
168
+ "@directus/validation": "0.0.12"
169
169
  },
170
170
  "devDependencies": {
171
- "@ngneat/falso": "7.1.1",
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.15",
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.2.2",
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.2.2",
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",