@directus/api 19.2.0 → 19.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/database/helpers/index.d.ts +1 -1
  2. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
  3. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  4. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -0
  5. package/dist/database/helpers/schema/dialects/mssql.js +9 -0
  6. package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
  7. package/dist/database/helpers/schema/dialects/mysql.js +17 -0
  8. package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -0
  9. package/dist/database/helpers/schema/dialects/oracle.js +9 -0
  10. package/dist/database/helpers/schema/dialects/postgres.d.ts +4 -0
  11. package/dist/database/helpers/schema/dialects/postgres.js +14 -0
  12. package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
  13. package/dist/database/helpers/schema/dialects/sqlite.js +9 -0
  14. package/dist/database/helpers/schema/index.d.ts +3 -3
  15. package/dist/database/helpers/schema/index.js +3 -3
  16. package/dist/database/helpers/schema/types.d.ts +4 -0
  17. package/dist/database/helpers/schema/types.js +6 -0
  18. package/dist/middleware/graphql.js +5 -1
  19. package/dist/services/extensions.js +1 -1
  20. package/dist/services/roles.d.ts +1 -0
  21. package/dist/services/roles.js +210 -11
  22. package/dist/services/users.js +66 -3
  23. package/dist/telemetry/lib/get-report.js +22 -10
  24. package/dist/telemetry/types/report.d.ts +12 -0
  25. package/dist/telemetry/utils/check-increased-user-limits.d.ts +7 -0
  26. package/dist/telemetry/utils/check-increased-user-limits.js +22 -0
  27. package/dist/telemetry/utils/get-extension-count.d.ts +9 -0
  28. package/dist/telemetry/utils/get-extension-count.js +19 -0
  29. package/dist/telemetry/utils/get-field-count.d.ts +6 -0
  30. package/dist/telemetry/utils/get-field-count.js +12 -0
  31. package/dist/telemetry/utils/get-item-count.d.ts +10 -6
  32. package/dist/telemetry/utils/get-item-count.js +13 -9
  33. package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +6 -0
  34. package/dist/telemetry/utils/get-role-counts-by-roles.js +27 -0
  35. package/dist/telemetry/utils/get-role-counts-by-users.d.ts +11 -0
  36. package/dist/telemetry/utils/get-role-counts-by-users.js +34 -0
  37. package/dist/telemetry/utils/get-user-count.d.ts +3 -2
  38. package/dist/telemetry/utils/get-user-count.js +7 -4
  39. package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +7 -0
  40. package/dist/telemetry/utils/get-user-counts-by-roles.js +35 -0
  41. package/dist/telemetry/utils/get-user-item-count.js +4 -2
  42. package/dist/telemetry/utils/should-check-user-limits.d.ts +4 -0
  43. package/dist/telemetry/utils/should-check-user-limits.js +13 -0
  44. package/package.json +30 -30
@@ -9,7 +9,7 @@ import * as numberHelpers from './number/index.js';
9
9
  export declare function getHelpers(database: Knex): {
10
10
  date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
11
11
  st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
12
- schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle;
12
+ schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
13
13
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
14
14
  number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
15
15
  };
@@ -4,4 +4,5 @@ import { SchemaHelper } from '../types.js';
4
4
  export declare class SchemaHelperCockroachDb extends SchemaHelper {
5
5
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
6
6
  constraintName(existingName: string): string;
7
+ getDatabaseSize(): Promise<number | null>;
7
8
  }
@@ -1,4 +1,6 @@
1
1
  import { SchemaHelper } from '../types.js';
2
+ import { useEnv } from '@directus/env';
3
+ const env = useEnv();
2
4
  export class SchemaHelperCockroachDb extends SchemaHelper {
3
5
  async changeToType(table, column, type, options = {}) {
4
6
  await this.changeToTypeByCopy(table, column, type, options);
@@ -14,4 +16,15 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
14
16
  return existingName + suffix;
15
17
  }
16
18
  }
19
+ async getDatabaseSize() {
20
+ try {
21
+ const result = await this.knex
22
+ .select(this.knex.raw('round(SUM(range_size_mb) * 1024 * 1024, 0) AS size'))
23
+ .from(this.knex.raw('[SHOW RANGES FROM database ??]', [env['DB_DATABASE']]));
24
+ return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
17
30
  }
@@ -4,4 +4,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
4
4
  applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
5
5
  applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
6
6
  formatUUID(uuid: string): string;
7
+ getDatabaseSize(): Promise<number | null>;
7
8
  }
@@ -17,4 +17,13 @@ export class SchemaHelperMSSQL extends SchemaHelper {
17
17
  formatUUID(uuid) {
18
18
  return uuid.toUpperCase();
19
19
  }
20
+ async getDatabaseSize() {
21
+ try {
22
+ const result = await this.knex.raw('SELECT SUM(size) * 8192 AS size FROM sys.database_files;');
23
+ return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
20
29
  }
@@ -2,4 +2,5 @@ import type { Knex } from 'knex';
2
2
  import { SchemaHelper } from '../types.js';
3
3
  export declare class SchemaHelperMySQL extends SchemaHelper {
4
4
  applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
5
+ getDatabaseSize(): Promise<number | null>;
5
6
  }
@@ -1,5 +1,7 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { getDatabaseVersion } from '../../../index.js';
2
3
  import { SchemaHelper } from '../types.js';
4
+ const env = useEnv();
3
5
  export class SchemaHelperMySQL extends SchemaHelper {
4
6
  applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields) {
5
7
  if (getDatabaseVersion()?.startsWith('5.7')) {
@@ -11,4 +13,19 @@ export class SchemaHelperMySQL extends SchemaHelper {
11
13
  }
12
14
  return super.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
13
15
  }
16
+ async getDatabaseSize() {
17
+ try {
18
+ const result = (await this.knex
19
+ .sum('size AS size')
20
+ .from(this.knex
21
+ .select(this.knex.raw('data_length + index_length AS size'))
22
+ .from('information_schema.TABLES')
23
+ .where('table_schema', '=', String(env['DB_DATABASE']))
24
+ .as('size')));
25
+ return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
14
31
  }
@@ -7,4 +7,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
7
7
  castA2oPrimaryKey(): string;
8
8
  preRelationChange(relation: Partial<Relation>): void;
9
9
  processFieldType(field: Field): Type;
10
+ getDatabaseSize(): Promise<number | null>;
10
11
  }
@@ -29,4 +29,13 @@ export class SchemaHelperOracle extends SchemaHelper {
29
29
  }
30
30
  return field.type;
31
31
  }
32
+ async getDatabaseSize() {
33
+ try {
34
+ const result = await this.knex.raw('select SUM(bytes) from dba_segments');
35
+ return result[0]?.['SUM(BYTES)'] ? Number(result[0]?.['SUM(BYTES)']) : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
32
41
  }
@@ -0,0 +1,4 @@
1
+ import { SchemaHelper } from '../types.js';
2
+ export declare class SchemaHelperPostgres extends SchemaHelper {
3
+ getDatabaseSize(): Promise<number | null>;
4
+ }
@@ -0,0 +1,14 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { SchemaHelper } from '../types.js';
3
+ const env = useEnv();
4
+ export class SchemaHelperPostgres extends SchemaHelper {
5
+ async getDatabaseSize() {
6
+ try {
7
+ const result = await this.knex.select(this.knex.raw(`pg_database_size(?) as size;`, [env['DB_DATABASE']]));
8
+ return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ }
@@ -2,4 +2,5 @@ import { SchemaHelper } from '../types.js';
2
2
  export declare class SchemaHelperSQLite extends SchemaHelper {
3
3
  preColumnChange(): Promise<boolean>;
4
4
  postColumnChange(): Promise<void>;
5
+ getDatabaseSize(): Promise<number | null>;
5
6
  }
@@ -10,4 +10,13 @@ export class SchemaHelperSQLite extends SchemaHelper {
10
10
  async postColumnChange() {
11
11
  await this.knex.raw('PRAGMA foreign_keys = ON');
12
12
  }
13
+ async getDatabaseSize() {
14
+ try {
15
+ const result = await this.knex.raw('SELECT page_count * page_size as "size" FROM pragma_page_count(), pragma_page_size();');
16
+ return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
13
22
  }
@@ -1,7 +1,7 @@
1
- export { SchemaHelperDefault as postgres } from './dialects/default.js';
2
1
  export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
3
2
  export { SchemaHelperDefault as redshift } from './dialects/default.js';
3
+ export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
4
+ export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
4
5
  export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
6
+ export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
5
7
  export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
6
- export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
7
- export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
@@ -1,7 +1,7 @@
1
- export { SchemaHelperDefault as postgres } from './dialects/default.js';
2
1
  export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
3
2
  export { SchemaHelperDefault as redshift } from './dialects/default.js';
3
+ export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
4
+ export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
4
5
  export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
6
+ export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
5
7
  export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
6
- export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
7
- export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
@@ -23,4 +23,8 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
23
23
  castA2oPrimaryKey(): string;
24
24
  applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
25
25
  formatUUID(uuid: string): string;
26
+ /**
27
+ * @returns Size of the database in bytes
28
+ */
29
+ getDatabaseSize(): Promise<number | null>;
26
30
  }
@@ -88,4 +88,10 @@ export class SchemaHelper extends DatabaseHelper {
88
88
  formatUUID(uuid) {
89
89
  return uuid; // no-op by default
90
90
  }
91
+ /**
92
+ * @returns Size of the database in bytes
93
+ */
94
+ async getDatabaseSize() {
95
+ return null;
96
+ }
91
97
  }
@@ -3,6 +3,7 @@ import { getOperationAST, parse, Source } from 'graphql';
3
3
  import { InvalidPayloadError, InvalidQueryError, MethodNotAllowedError } from '@directus/errors';
4
4
  import { GraphQLValidationError } from '../services/graphql/errors/validation.js';
5
5
  import asyncHandler from '../utils/async-handler.js';
6
+ import { useEnv } from '@directus/env';
6
7
  export const parseGraphQL = asyncHandler(async (req, res, next) => {
7
8
  if (req.method !== 'GET' && req.method !== 'POST') {
8
9
  throw new MethodNotAllowedError({ allowed: ['GET', 'POST'], current: req.method });
@@ -35,7 +36,10 @@ export const parseGraphQL = asyncHandler(async (req, res, next) => {
35
36
  throw new InvalidPayloadError({ reason: 'Must provide query string' });
36
37
  }
37
38
  try {
38
- document = parse(new Source(query));
39
+ const env = useEnv();
40
+ document = parse(new Source(query), {
41
+ maxTokens: Number(env['GRAPHQL_QUERY_TOKEN_LIMIT']),
42
+ });
39
43
  }
40
44
  catch (err) {
41
45
  throw new GraphQLValidationError({
@@ -52,7 +52,7 @@ export class ExtensionsService {
52
52
  const points = version.bundled.length ?? 1;
53
53
  const afterInstallCount = currentlyInstalledCount + points;
54
54
  if (afterInstallCount >= limit) {
55
- throw new LimitExceededError();
55
+ throw new LimitExceededError({ category: 'Extensions' });
56
56
  }
57
57
  }
58
58
  return { extension, version };
@@ -7,6 +7,7 @@ export declare class RolesService extends ItemsService {
7
7
  private checkForOtherAdminUsers;
8
8
  private isIpAccessValid;
9
9
  private assertValidIpAccess;
10
+ private getRoleAccessType;
10
11
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
11
12
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
12
13
  updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
@@ -1,5 +1,12 @@
1
- import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
1
+ import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
2
2
  import { getMatch } from 'ip-matching';
3
+ import { omit } from 'lodash-es';
4
+ import { checkIncreasedUserLimits } from '../telemetry/utils/check-increased-user-limits.js';
5
+ import { getRoleCountsByUsers } from '../telemetry/utils/get-role-counts-by-users.js';
6
+ import {} from '../telemetry/utils/get-user-count.js';
7
+ import { getUserCountsByRoles } from '../telemetry/utils/get-user-counts-by-roles.js';
8
+ import { shouldCheckUserLimits } from '../telemetry/utils/should-check-user-limits.js';
9
+ import { shouldClearCache } from '../utils/should-clear-cache.js';
3
10
  import { transaction } from '../utils/transaction.js';
4
11
  import { ItemsService } from './items.js';
5
12
  import { PermissionsService } from './permissions/index.js';
@@ -24,8 +31,9 @@ export class RolesService extends ItemsService {
24
31
  }
25
32
  async checkForOtherAdminUsers(key, users) {
26
33
  const role = await this.knex.select('admin_access').from('directus_roles').where('id', '=', key).first();
34
+ // No-op if role doesn't exist
27
35
  if (!role)
28
- throw new ForbiddenError();
36
+ return;
29
37
  const usersBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map((user) => user.id);
30
38
  const usersAdded = [];
31
39
  const usersUpdated = [];
@@ -151,13 +159,67 @@ export class RolesService extends ItemsService {
151
159
  });
152
160
  }
153
161
  }
162
+ getRoleAccessType(data) {
163
+ if ('admin_access' in data && data['admin_access'] === true) {
164
+ return 'admin';
165
+ }
166
+ else if (('app_access' in data && data['app_access'] === true) || 'app_access' in data === false) {
167
+ return 'app';
168
+ }
169
+ else {
170
+ return 'api';
171
+ }
172
+ }
154
173
  async createOne(data, opts) {
155
174
  this.assertValidIpAccess(data);
175
+ if (shouldCheckUserLimits()) {
176
+ const increasedCounts = {
177
+ admin: 0,
178
+ app: 0,
179
+ api: 0,
180
+ };
181
+ const existingIds = [];
182
+ if ('users' in data) {
183
+ const type = this.getRoleAccessType(data);
184
+ increasedCounts[type] += data['users'].length;
185
+ for (const user of data['users']) {
186
+ if (typeof user === 'string') {
187
+ existingIds.push(user);
188
+ }
189
+ else if (typeof user === 'object' && 'id' in user) {
190
+ existingIds.push(user['id']);
191
+ }
192
+ }
193
+ }
194
+ await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
195
+ }
156
196
  return super.createOne(data, opts);
157
197
  }
158
198
  async createMany(data, opts) {
199
+ const needsUserLimitCheck = shouldCheckUserLimits();
200
+ const increasedCounts = {
201
+ admin: 0,
202
+ app: 0,
203
+ api: 0,
204
+ };
205
+ const existingIds = [];
159
206
  for (const partialItem of data) {
160
207
  this.assertValidIpAccess(partialItem);
208
+ if (needsUserLimitCheck && 'users' in partialItem) {
209
+ const type = this.getRoleAccessType(partialItem);
210
+ increasedCounts[type] += partialItem['users'].length;
211
+ for (const user of partialItem['users']) {
212
+ if (typeof user === 'string') {
213
+ existingIds.push(user);
214
+ }
215
+ else if (typeof user === 'object' && 'id' in user) {
216
+ existingIds.push(user['id']);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ if (needsUserLimitCheck) {
222
+ await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
161
223
  }
162
224
  return super.createMany(data, opts);
163
225
  }
@@ -167,28 +229,150 @@ export class RolesService extends ItemsService {
167
229
  if ('users' in data) {
168
230
  await this.checkForOtherAdminUsers(key, data['users']);
169
231
  }
232
+ if (shouldCheckUserLimits()) {
233
+ const increasedCounts = {
234
+ admin: 0,
235
+ app: 0,
236
+ api: 0,
237
+ };
238
+ let increasedUsers = 0;
239
+ const existingIds = [];
240
+ let existingRole = await this.knex
241
+ .count('directus_users.id', { as: 'count' })
242
+ .select('directus_roles.admin_access', 'directus_roles.app_access')
243
+ .from('directus_users')
244
+ .where('directus_roles.id', '=', key)
245
+ .andWhere('directus_users.status', '=', 'active')
246
+ .leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
247
+ .groupBy('directus_roles.admin_access', 'directus_roles.app_access')
248
+ .first();
249
+ if (!existingRole) {
250
+ try {
251
+ const role = (await this.knex
252
+ .select('admin_access', 'app_access')
253
+ .from('directus_roles')
254
+ .where('id', '=', key)
255
+ .first()) ?? { admin_access: null, app_access: null };
256
+ existingRole = { count: 0, ...role };
257
+ }
258
+ catch {
259
+ existingRole = { count: 0, admin_access: null, app_access: null };
260
+ }
261
+ }
262
+ if ('users' in data) {
263
+ const users = data['users'];
264
+ if (Array.isArray(users)) {
265
+ increasedUsers = users.length - Number(existingRole.count);
266
+ for (const user of users) {
267
+ if (typeof user === 'string') {
268
+ existingIds.push(user);
269
+ }
270
+ else if (typeof user === 'object' && 'id' in user) {
271
+ existingIds.push(user['id']);
272
+ }
273
+ }
274
+ }
275
+ else {
276
+ increasedUsers += users.create.length;
277
+ increasedUsers -= users.delete.length;
278
+ const userIds = [];
279
+ for (const user of users.update) {
280
+ if ('status' in user) {
281
+ // account for users being activated and deactivated
282
+ if (user['status'] === 'active') {
283
+ increasedUsers++;
284
+ }
285
+ else {
286
+ increasedUsers--;
287
+ }
288
+ }
289
+ userIds.push(user.id);
290
+ }
291
+ try {
292
+ const existingCounts = await getRoleCountsByUsers(this.knex, userIds);
293
+ if (existingRole.admin_access) {
294
+ increasedUsers += existingCounts.app + existingCounts.api;
295
+ }
296
+ else if (existingRole.app_access) {
297
+ increasedUsers += existingCounts.admin + existingCounts.api;
298
+ }
299
+ else {
300
+ increasedUsers += existingCounts.admin + existingCounts.app;
301
+ }
302
+ }
303
+ catch {
304
+ // ignore failed user call
305
+ }
306
+ }
307
+ }
308
+ let isAccessChanged = false;
309
+ let accessType = 'api';
310
+ if ('app_access' in data) {
311
+ if (data['app_access'] === true) {
312
+ accessType = 'app';
313
+ if (!existingRole.app_access)
314
+ isAccessChanged = true;
315
+ }
316
+ else if (existingRole.app_access) {
317
+ isAccessChanged = true;
318
+ }
319
+ }
320
+ else if (existingRole.app_access) {
321
+ accessType = 'app';
322
+ }
323
+ if ('admin_access' in data) {
324
+ if (data['admin_access'] === true) {
325
+ accessType = 'admin';
326
+ if (!existingRole.admin_access)
327
+ isAccessChanged = true;
328
+ }
329
+ else if (existingRole.admin_access) {
330
+ isAccessChanged = true;
331
+ }
332
+ }
333
+ else if (existingRole.admin_access) {
334
+ accessType = 'admin';
335
+ }
336
+ if (isAccessChanged) {
337
+ increasedCounts[accessType] += Number(existingRole.count);
338
+ }
339
+ increasedCounts[accessType] += increasedUsers;
340
+ await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
341
+ }
170
342
  }
171
343
  catch (err) {
172
344
  (opts || (opts = {})).preMutationError = err;
173
345
  }
174
346
  return super.updateOne(key, data, opts);
175
347
  }
176
- async updateBatch(data, opts) {
348
+ async updateBatch(data, opts = {}) {
177
349
  for (const partialItem of data) {
178
350
  this.assertValidIpAccess(partialItem);
179
351
  }
180
352
  const primaryKeyField = this.schema.collections[this.collection].primary;
181
- const keys = data.map((item) => item[primaryKeyField]);
182
- const setsToNoAdmin = data.some((item) => item['admin_access'] === false);
353
+ if (!opts.mutationTracker) {
354
+ opts.mutationTracker = this.createMutationTracker();
355
+ }
356
+ const keys = [];
183
357
  try {
184
- if (setsToNoAdmin) {
185
- await this.checkForOtherAdminRoles(keys);
186
- }
358
+ await transaction(this.knex, async (trx) => {
359
+ const service = new RolesService({
360
+ accountability: this.accountability,
361
+ knex: trx,
362
+ schema: this.schema,
363
+ });
364
+ for (const item of data) {
365
+ const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
366
+ keys.push(await service.updateOne(item[primaryKeyField], omit(item, primaryKeyField), combinedOpts));
367
+ }
368
+ });
187
369
  }
188
- catch (err) {
189
- (opts || (opts = {})).preMutationError = err;
370
+ finally {
371
+ if (shouldClearCache(this.cache, opts, this.collection)) {
372
+ await this.cache.clear();
373
+ }
190
374
  }
191
- return super.updateBatch(data, opts);
375
+ return keys;
192
376
  }
193
377
  async updateMany(keys, data, opts) {
194
378
  this.assertValidIpAccess(data);
@@ -196,6 +380,21 @@ export class RolesService extends ItemsService {
196
380
  if ('admin_access' in data && data['admin_access'] === false) {
197
381
  await this.checkForOtherAdminRoles(keys);
198
382
  }
383
+ if (shouldCheckUserLimits() && ('admin_access' in data || 'app_access' in data)) {
384
+ const existingCounts = await getUserCountsByRoles(this.knex, keys);
385
+ const increasedCounts = {
386
+ admin: 0,
387
+ app: 0,
388
+ api: 0,
389
+ };
390
+ const type = this.getRoleAccessType(data);
391
+ for (const [existingType, existingCount] of Object.entries(existingCounts)) {
392
+ if (existingType === type)
393
+ continue;
394
+ increasedCounts[type] += existingCount;
395
+ }
396
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
397
+ }
199
398
  }
200
399
  catch (err) {
201
400
  (opts || (opts = {})).preMutationError = err;
@@ -1,13 +1,18 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { ForbiddenError, InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
3
- import { getSimpleHash, toArray, validatePayload } from '@directus/utils';
3
+ import { getSimpleHash, toArray, toBoolean, validatePayload } from '@directus/utils';
4
4
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
5
5
  import Joi from 'joi';
6
6
  import jwt from 'jsonwebtoken';
7
- import { cloneDeep, isEmpty } from 'lodash-es';
7
+ import { cloneDeep, isEmpty, mergeWith } from 'lodash-es';
8
8
  import { performance } from 'perf_hooks';
9
9
  import getDatabase from '../database/index.js';
10
10
  import { useLogger } from '../logger.js';
11
+ import { checkIncreasedUserLimits } from '../telemetry/utils/check-increased-user-limits.js';
12
+ import { getRoleCountsByRoles } from '../telemetry/utils/get-role-counts-by-roles.js';
13
+ import { getRoleCountsByUsers } from '../telemetry/utils/get-role-counts-by-users.js';
14
+ import {} from '../telemetry/utils/get-user-count.js';
15
+ import { shouldCheckUserLimits } from '../telemetry/utils/should-check-user-limits.js';
11
16
  import { getSecret } from '../utils/get-secret.js';
12
17
  import isUrlAllowed from '../utils/is-url-allowed.js';
13
18
  import { verifyJWT } from '../utils/jwt.js';
@@ -164,6 +169,7 @@ export class UsersService extends ItemsService {
164
169
  async createMany(data, opts) {
165
170
  const emails = data['map']((payload) => payload['email']).filter((email) => email);
166
171
  const passwords = data['map']((payload) => payload['password']).filter((password) => password);
172
+ const roles = data['map']((payload) => payload['role']).filter((role) => role);
167
173
  try {
168
174
  if (emails.length) {
169
175
  this.validateEmail(emails);
@@ -172,6 +178,33 @@ export class UsersService extends ItemsService {
172
178
  if (passwords.length) {
173
179
  await this.checkPasswordPolicy(passwords);
174
180
  }
181
+ if (shouldCheckUserLimits() && roles.length) {
182
+ const increasedCounts = {
183
+ admin: 0,
184
+ app: 0,
185
+ api: 0,
186
+ };
187
+ const existingRoles = [];
188
+ for (const role of roles) {
189
+ if (typeof role === 'object') {
190
+ if ('admin_access' in role && role['admin_access'] === true) {
191
+ increasedCounts.admin++;
192
+ }
193
+ else if ('app_access' in role && role['app_access'] === true) {
194
+ increasedCounts.app++;
195
+ }
196
+ else {
197
+ increasedCounts.api++;
198
+ }
199
+ }
200
+ else {
201
+ existingRoles.push(role);
202
+ }
203
+ }
204
+ const existingRoleCounts = await getRoleCountsByRoles(this.knex, existingRoles);
205
+ mergeWith(increasedCounts, existingRoleCounts, (x, y) => x + y);
206
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
207
+ }
175
208
  }
176
209
  catch (err) {
177
210
  (opts || (opts = {})).preMutationError = err;
@@ -216,6 +249,7 @@ export class UsersService extends ItemsService {
216
249
  */
217
250
  async updateMany(keys, data, opts) {
218
251
  try {
252
+ const needsUserLimitCheck = shouldCheckUserLimits();
219
253
  if (data['role']) {
220
254
  /*
221
255
  * data['role'] has the following cases:
@@ -226,7 +260,11 @@ export class UsersService extends ItemsService {
226
260
  const role = data['role']?.id ?? data['role'];
227
261
  let newRole;
228
262
  if (typeof role === 'string') {
229
- newRole = await this.knex.select('admin_access').from('directus_roles').where('id', role).first();
263
+ newRole = await this.knex
264
+ .select('admin_access', 'app_access')
265
+ .from('directus_roles')
266
+ .where('id', role)
267
+ .first();
230
268
  }
231
269
  else {
232
270
  newRole = role;
@@ -234,10 +272,35 @@ export class UsersService extends ItemsService {
234
272
  if (!newRole?.admin_access) {
235
273
  await this.checkRemainingAdminExistence(keys);
236
274
  }
275
+ if (needsUserLimitCheck && newRole) {
276
+ const existingCounts = await getRoleCountsByUsers(this.knex, keys);
277
+ const increasedCounts = {
278
+ admin: 0,
279
+ app: 0,
280
+ api: 0,
281
+ };
282
+ if (toBoolean(newRole.admin_access)) {
283
+ increasedCounts.admin = keys.length - existingCounts.admin;
284
+ }
285
+ else if (toBoolean(newRole.app_access)) {
286
+ increasedCounts.app = keys.length - existingCounts.app;
287
+ }
288
+ else {
289
+ increasedCounts.api = keys.length - existingCounts.api;
290
+ }
291
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
292
+ }
293
+ }
294
+ if (needsUserLimitCheck && data['role'] === null) {
295
+ await checkIncreasedUserLimits(this.knex, { admin: 0, app: 0, api: 1 });
237
296
  }
238
297
  if (data['status'] !== undefined && data['status'] !== 'active') {
239
298
  await this.checkRemainingActiveAdmin(keys);
240
299
  }
300
+ if (needsUserLimitCheck && data['status'] === 'active') {
301
+ const increasedCounts = await getRoleCountsByUsers(this.knex, keys, { inactiveUsers: true });
302
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
303
+ }
241
304
  if (data['email']) {
242
305
  if (keys.length > 1) {
243
306
  throw new RecordNotUniqueError({
@@ -1,16 +1,21 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { version } from 'directus/version';
3
+ import { getHelpers } from '../../database/helpers/index.js';
3
4
  import { getDatabase, getDatabaseClient } from '../../database/index.js';
5
+ import { getExtensionCount } from '../utils/get-extension-count.js';
6
+ import { getFieldCount } from '../utils/get-field-count.js';
4
7
  import { getItemCount } from '../utils/get-item-count.js';
5
8
  import { getUserCount } from '../utils/get-user-count.js';
6
9
  import { getUserItemCount } from '../utils/get-user-item-count.js';
7
- const basicCountCollections = [
8
- 'directus_dashboards',
9
- 'directus_extensions',
10
- 'directus_files',
11
- 'directus_flows',
12
- 'directus_roles',
13
- 'directus_shares',
10
+ const basicCountTasks = [
11
+ { collection: 'directus_dashboards' },
12
+ { collection: 'directus_files' },
13
+ {
14
+ collection: 'directus_flows',
15
+ where: ['status', '=', 'active'],
16
+ },
17
+ { collection: 'directus_roles' },
18
+ { collection: 'directus_shares' },
14
19
  ];
15
20
  /**
16
21
  * Create a telemetry report about the anonymous usage of the current installation
@@ -18,17 +23,20 @@ const basicCountCollections = [
18
23
  export const getReport = async () => {
19
24
  const db = getDatabase();
20
25
  const env = useEnv();
21
- const [basicCounts, userCounts, userItemCount] = await Promise.all([
22
- getItemCount(db, basicCountCollections),
26
+ const helpers = getHelpers(db);
27
+ const [basicCounts, userCounts, userItemCount, fieldsCounts, extensionsCounts, databaseSize] = await Promise.all([
28
+ getItemCount(db, basicCountTasks),
23
29
  getUserCount(db),
24
30
  getUserItemCount(db),
31
+ getFieldCount(db),
32
+ getExtensionCount(db),
33
+ helpers.schema.getDatabaseSize(),
25
34
  ]);
26
35
  return {
27
36
  url: env['PUBLIC_URL'],
28
37
  version: version,
29
38
  database: getDatabaseClient(),
30
39
  dashboards: basicCounts.directus_dashboards,
31
- extensions: basicCounts.directus_extensions,
32
40
  files: basicCounts.directus_files,
33
41
  flows: basicCounts.directus_flows,
34
42
  roles: basicCounts.directus_roles,
@@ -38,5 +46,9 @@ export const getReport = async () => {
38
46
  api_users: userCounts.api,
39
47
  collections: userItemCount.collections,
40
48
  items: userItemCount.items,
49
+ fields_max: fieldsCounts.max,
50
+ fields_total: fieldsCounts.total,
51
+ extensions: extensionsCounts.totalEnabled,
52
+ database_size: databaseSize ?? 0,
41
53
  };
42
54
  };
@@ -55,4 +55,16 @@ export interface TelemetryReport {
55
55
  * Number of shares in the system
56
56
  */
57
57
  shares: number;
58
+ /**
59
+ * Maximum number of fields in a collection
60
+ */
61
+ fields_max: number;
62
+ /**
63
+ * Number of fields in the system
64
+ */
65
+ fields_total: number;
66
+ /**
67
+ * Size of the database in bytes
68
+ */
69
+ database_size: number;
58
70
  }
@@ -0,0 +1,7 @@
1
+ import type { Knex } from 'knex';
2
+ import { type AccessTypeCount } from './get-user-count.js';
3
+ import type { PrimaryKey } from '@directus/types';
4
+ /**
5
+ * Ensure that user limits are not reached
6
+ */
7
+ export declare function checkIncreasedUserLimits(db: Knex, increasedUserCounts: AccessTypeCount, ignoreIds?: PrimaryKey[]): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { LimitExceededError } from '@directus/errors';
3
+ import { getUserCount } from './get-user-count.js';
4
+ const env = useEnv();
5
+ /**
6
+ * Ensure that user limits are not reached
7
+ */
8
+ export async function checkIncreasedUserLimits(db, increasedUserCounts, ignoreIds = []) {
9
+ if (!increasedUserCounts.admin && !increasedUserCounts.app && !increasedUserCounts.api)
10
+ return;
11
+ const userCounts = await getUserCount(db, ignoreIds);
12
+ if (increasedUserCounts.admin > 0 &&
13
+ increasedUserCounts.admin + userCounts.admin > Number(env['USERS_ADMIN_ACCESS_LIMIT'])) {
14
+ throw new LimitExceededError({ category: 'Active Admin users' });
15
+ }
16
+ if (increasedUserCounts.app > 0 && increasedUserCounts.app + userCounts.app > Number(env['USERS_APP_ACCESS_LIMIT'])) {
17
+ throw new LimitExceededError({ category: 'Active App users' });
18
+ }
19
+ if (increasedUserCounts.api > 0 && increasedUserCounts.api + userCounts.api > Number(env['USERS_API_ACCESS_LIMIT'])) {
20
+ throw new LimitExceededError({ category: 'Active API users' });
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ import { type Knex } from 'knex';
2
+ export interface ExtensionCount {
3
+ /**
4
+ * Total count of enabled extensions excluding Bundle-Parents,
5
+ * meaning a Bundle extensions with one extension inside of it counts as one.
6
+ */
7
+ totalEnabled: number;
8
+ }
9
+ export declare const getExtensionCount: (db: Knex) => Promise<ExtensionCount>;
@@ -0,0 +1,19 @@
1
+ import {} from 'knex';
2
+ import { ExtensionsService } from '../../services/extensions.js';
3
+ import { getSchema } from '../../utils/get-schema.js';
4
+ export const getExtensionCount = async (db) => {
5
+ const extensionsService = new ExtensionsService({
6
+ knex: db,
7
+ schema: await getSchema({ database: db }),
8
+ });
9
+ const extensions = await extensionsService.readAll();
10
+ let totalEnabled = 0;
11
+ for (const extension of extensions) {
12
+ if (extension.meta.enabled && extension.schema && extension.schema.type !== 'bundle') {
13
+ totalEnabled++;
14
+ }
15
+ }
16
+ return {
17
+ totalEnabled,
18
+ };
19
+ };
@@ -0,0 +1,6 @@
1
+ import { type Knex } from 'knex';
2
+ export interface FieldCount {
3
+ max: number;
4
+ total: number;
5
+ }
6
+ export declare const getFieldCount: (db: Knex) => Promise<FieldCount>;
@@ -0,0 +1,12 @@
1
+ import {} from 'knex';
2
+ export const getFieldCount = async (db) => {
3
+ const query = (await db
4
+ .max({ max: 'field_count' })
5
+ .sum({ total: 'field_count' })
6
+ .from(db.select('collection').count('* as field_count').from('directus_fields').groupBy('collection').as('inner'))
7
+ .first());
8
+ return {
9
+ max: query?.max ? Number(query.max) : 0,
10
+ total: query?.total ? Number(query.total) : 0,
11
+ };
12
+ };
@@ -3,13 +3,17 @@ export interface CollectionCount {
3
3
  collection: string;
4
4
  count: number;
5
5
  }
6
+ export interface CollectionCountTask {
7
+ collection: string;
8
+ where?: readonly [string, string, string | boolean | number];
9
+ }
6
10
  /**
7
- * Get the item count of the given collection in the given database
11
+ * Get the item count of the given task in the given database
8
12
  * @param db Knex instance to count against
9
- * @param collection Table to count rows in
13
+ * @param task Task to count rows for
10
14
  * @returns Collection name and count
11
15
  */
12
- export declare const countCollection: (db: Knex, collection: string) => Promise<CollectionCount>;
16
+ export declare const countCollection: (db: Knex, task: CollectionCountTask) => Promise<CollectionCount>;
13
17
  /**
14
18
  * Merge the given collection count in the object accumulator
15
19
  * Intended for use with .reduce()
@@ -19,8 +23,8 @@ export declare const countCollection: (db: Knex, collection: string) => Promise<
19
23
  */
20
24
  export declare const mergeResults: (acc: Record<string, number>, value: CollectionCount) => Record<string, number>;
21
25
  /**
22
- * Get an object of item counts for the given collections
26
+ * Get an object of item counts for the given tasks
23
27
  * @param db Database instance to get counts in
24
- * @param collections Array of table names to get count from
28
+ * @param tasks Array of tasks to get count for
25
29
  */
26
- export declare const getItemCount: <T extends readonly string[]>(db: Knex, collections: T) => Promise<Record<T[number], number>>;
30
+ export declare const getItemCount: <T extends readonly CollectionCountTask[]>(db: Knex, tasks: T) => Promise<Record<T[number]["collection"], number>>;
@@ -1,14 +1,18 @@
1
1
  import {} from 'knex';
2
2
  import pLimit from 'p-limit';
3
3
  /**
4
- * Get the item count of the given collection in the given database
4
+ * Get the item count of the given task in the given database
5
5
  * @param db Knex instance to count against
6
- * @param collection Table to count rows in
6
+ * @param task Task to count rows for
7
7
  * @returns Collection name and count
8
8
  */
9
- export const countCollection = async (db, collection) => {
10
- const count = await db.count('*', { as: 'count' }).from(collection).first();
11
- return { collection, count: Number(count?.['count'] ?? 0) };
9
+ export const countCollection = async (db, task) => {
10
+ const query = db.count('*', { as: 'count' }).from(task.collection);
11
+ if (task.where) {
12
+ query.where(...task.where);
13
+ }
14
+ const count = await query.first();
15
+ return { collection: task.collection, count: Number(count?.['count'] ?? 0) };
12
16
  };
13
17
  /**
14
18
  * Merge the given collection count in the object accumulator
@@ -22,15 +26,15 @@ export const mergeResults = (acc, value) => {
22
26
  return acc;
23
27
  };
24
28
  /**
25
- * Get an object of item counts for the given collections
29
+ * Get an object of item counts for the given tasks
26
30
  * @param db Database instance to get counts in
27
- * @param collections Array of table names to get count from
31
+ * @param tasks Array of tasks to get count for
28
32
  */
29
- export const getItemCount = async (db, collections) => {
33
+ export const getItemCount = async (db, tasks) => {
30
34
  // Counts can be a little heavy if the table is very large, so we'll only ever execute 3 of these
31
35
  // queries simultaneously to not overload the database
32
36
  const limit = pLimit(3);
33
- const calls = collections.map((collection) => limit(countCollection, db, collection));
37
+ const calls = tasks.map((task) => limit(countCollection, db, task));
34
38
  const results = await Promise.all(calls);
35
39
  return results.reduce(mergeResults, {});
36
40
  };
@@ -0,0 +1,6 @@
1
+ import type { Knex } from 'knex';
2
+ import { type AccessTypeCount } from './get-user-count.js';
3
+ /**
4
+ * Get the role type counts by role IDs
5
+ */
6
+ export declare function getRoleCountsByRoles(db: Knex, roles: string[]): Promise<AccessTypeCount>;
@@ -0,0 +1,27 @@
1
+ import { toBoolean } from '@directus/utils';
2
+ import {} from './get-user-count.js';
3
+ /**
4
+ * Get the role type counts by role IDs
5
+ */
6
+ export async function getRoleCountsByRoles(db, roles) {
7
+ const counts = {
8
+ admin: 0,
9
+ app: 0,
10
+ api: 0,
11
+ };
12
+ const result = (await db.select('id', 'admin_access', 'app_access').from('directus_roles').whereIn('id', roles));
13
+ for (const role of result) {
14
+ const adminAccess = toBoolean(role.admin_access);
15
+ const appAccess = toBoolean(role.app_access);
16
+ if (adminAccess) {
17
+ counts.admin++;
18
+ }
19
+ else if (appAccess) {
20
+ counts.app++;
21
+ }
22
+ else {
23
+ counts.api++;
24
+ }
25
+ }
26
+ return counts;
27
+ }
@@ -0,0 +1,11 @@
1
+ import type { PrimaryKey } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { AccessTypeCount } from './get-user-count.js';
4
+ type CountOptions = {
5
+ inactiveUsers?: boolean;
6
+ };
7
+ /**
8
+ * Get the role type counts by user IDs
9
+ */
10
+ export declare function getRoleCountsByUsers(db: Knex, userIds: PrimaryKey[], options?: CountOptions): Promise<AccessTypeCount>;
11
+ export {};
@@ -0,0 +1,34 @@
1
+ import { toBoolean } from '@directus/utils';
2
+ /**
3
+ * Get the role type counts by user IDs
4
+ */
5
+ export async function getRoleCountsByUsers(db, userIds, options = {}) {
6
+ const counts = {
7
+ admin: 0,
8
+ app: 0,
9
+ api: 0,
10
+ };
11
+ const result = await db
12
+ .count('directus_users.id', { as: 'count' })
13
+ .select('directus_roles.admin_access', 'directus_roles.app_access')
14
+ .from('directus_users')
15
+ .whereIn('directus_users.id', userIds)
16
+ .andWhere('directus_users.status', options.inactiveUsers ? '!=' : '=', 'active')
17
+ .leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
18
+ .groupBy('directus_roles.admin_access', 'directus_roles.app_access');
19
+ for (const record of result) {
20
+ const adminAccess = toBoolean(record.admin_access);
21
+ const appAccess = toBoolean(record.app_access);
22
+ const count = Number(record.count);
23
+ if (adminAccess) {
24
+ counts.admin += count;
25
+ }
26
+ else if (appAccess) {
27
+ counts.app += count;
28
+ }
29
+ else {
30
+ counts.api += count;
31
+ }
32
+ }
33
+ return counts;
34
+ }
@@ -1,7 +1,8 @@
1
+ import type { PrimaryKey } from '@directus/types';
1
2
  import { type Knex } from 'knex';
2
- export interface UserCount {
3
+ export interface AccessTypeCount {
3
4
  admin: number;
4
5
  app: number;
5
6
  api: number;
6
7
  }
7
- export declare const getUserCount: (db: Knex) => Promise<UserCount>;
8
+ export declare const getUserCount: (db: Knex, ignoreIds?: PrimaryKey[]) => Promise<AccessTypeCount>;
@@ -1,6 +1,6 @@
1
1
  import { toBoolean } from '@directus/utils';
2
2
  import {} from 'knex';
3
- export const getUserCount = async (db) => {
3
+ export const getUserCount = async (db, ignoreIds = []) => {
4
4
  const counts = {
5
5
  admin: 0,
6
6
  app: 0,
@@ -10,20 +10,23 @@ export const getUserCount = async (db) => {
10
10
  .count('directus_users.id', { as: 'count' })
11
11
  .select('directus_roles.admin_access', 'directus_roles.app_access')
12
12
  .from('directus_users')
13
+ .whereNotIn('directus_users.id', ignoreIds)
14
+ .andWhere('directus_users.status', 'active')
13
15
  .leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
16
+ .where('directus_users.status', '=', 'active')
14
17
  .groupBy('directus_roles.admin_access', 'directus_roles.app_access'));
15
18
  for (const record of result) {
16
19
  const adminAccess = toBoolean(record.admin_access);
17
20
  const appAccess = toBoolean(record.app_access);
18
21
  const count = Number(record.count);
19
22
  if (adminAccess) {
20
- counts.admin = count;
23
+ counts.admin += count;
21
24
  }
22
25
  else if (appAccess) {
23
- counts.app = count;
26
+ counts.app += count;
24
27
  }
25
28
  else {
26
- counts.api = count;
29
+ counts.api += count;
27
30
  }
28
31
  }
29
32
  return counts;
@@ -0,0 +1,7 @@
1
+ import type { PrimaryKey } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import { type AccessTypeCount } from './get-user-count.js';
4
+ /**
5
+ * Get the user type counts by role IDs
6
+ */
7
+ export declare function getUserCountsByRoles(db: Knex, roleIds: PrimaryKey[]): Promise<AccessTypeCount>;
@@ -0,0 +1,35 @@
1
+ import { toBoolean } from '@directus/utils';
2
+ import {} from './get-user-count.js';
3
+ /**
4
+ * Get the user type counts by role IDs
5
+ */
6
+ export async function getUserCountsByRoles(db, roleIds) {
7
+ const counts = {
8
+ admin: 0,
9
+ app: 0,
10
+ api: 0,
11
+ };
12
+ const result = (await db
13
+ .count('directus_users.id', { as: 'count' })
14
+ .select('directus_roles.admin_access', 'directus_roles.app_access')
15
+ .from('directus_users')
16
+ .whereIn('directus_roles.id', roleIds)
17
+ .andWhere('directus_users.status', '=', 'active')
18
+ .leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
19
+ .groupBy('directus_roles.admin_access', 'directus_roles.app_access'));
20
+ for (const record of result) {
21
+ const adminAccess = toBoolean(record.admin_access);
22
+ const appAccess = toBoolean(record.app_access);
23
+ const count = Number(record.count);
24
+ if (adminAccess) {
25
+ counts.admin += count;
26
+ }
27
+ else if (appAccess) {
28
+ counts.app += count;
29
+ }
30
+ else {
31
+ counts.api += count;
32
+ }
33
+ }
34
+ return counts;
35
+ }
@@ -1,7 +1,7 @@
1
+ import { isSystemCollection } from '@directus/system-data';
1
2
  import {} from 'knex';
2
3
  import { getSchema } from '../../utils/get-schema.js';
3
4
  import { getItemCount } from './get-item-count.js';
4
- import { isSystemCollection } from '@directus/system-data';
5
5
  /**
6
6
  * Sum all passed values together. Meant to be used with .reduce()
7
7
  */
@@ -11,7 +11,9 @@ export const sum = (acc, val) => (acc += val);
11
11
  */
12
12
  export const getUserItemCount = async (db) => {
13
13
  const schema = await getSchema({ database: db });
14
- const userCollections = Object.keys(schema.collections).filter((collection) => isSystemCollection(collection) === false);
14
+ const userCollections = Object.keys(schema.collections)
15
+ .filter((collection) => isSystemCollection(collection) === false)
16
+ .map((collection) => ({ collection }));
15
17
  const counts = await getItemCount(db, userCollections);
16
18
  const collections = userCollections.length;
17
19
  const items = Object.values(counts).reduce(sum, 0);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Confirm whether user limits needs to be checked
3
+ */
4
+ export declare function shouldCheckUserLimits(): boolean;
@@ -0,0 +1,13 @@
1
+ import { useEnv } from '@directus/env';
2
+ /**
3
+ * Confirm whether user limits needs to be checked
4
+ */
5
+ export function shouldCheckUserLimits() {
6
+ const env = useEnv();
7
+ if (Number(env['USERS_ADMIN_ACCESS_LIMIT']) !== Infinity ||
8
+ Number(env['USERS_APP_ACCESS_LIMIT']) !== Infinity ||
9
+ Number(env['USERS_API_ACCESS_LIMIT']) !== Infinity) {
10
+ return true;
11
+ }
12
+ return false;
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "19.2.0",
3
+ "version": "19.3.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -65,7 +65,7 @@
65
65
  "@rollup/plugin-node-resolve": "15.2.3",
66
66
  "@rollup/plugin-virtual": "3.0.2",
67
67
  "@types/cookie": "0.6.0",
68
- "argon2": "0.40.1",
68
+ "argon2": "0.40.3",
69
69
  "async": "3.2.5",
70
70
  "axios": "1.7.2",
71
71
  "busboy": "1.6.0",
@@ -96,7 +96,7 @@
96
96
  "graphql-ws": "5.16.0",
97
97
  "helmet": "7.1.0",
98
98
  "icc": "3.0.0",
99
- "inquirer": "9.2.22",
99
+ "inquirer": "9.2.23",
100
100
  "ioredis": "5.4.1",
101
101
  "ip-matching": "2.1.2",
102
102
  "isolated-vm": "4.7.2",
@@ -108,7 +108,7 @@
108
108
  "keyv": "4.5.4",
109
109
  "knex": "3.1.0",
110
110
  "ldapjs": "2.3.3",
111
- "liquidjs": "10.12.0",
111
+ "liquidjs": "10.13.1",
112
112
  "lodash-es": "4.17.21",
113
113
  "marked": "12.0.2",
114
114
  "micromustache": "8.0.3",
@@ -121,14 +121,14 @@
121
121
  "node-schedule": "2.1.1",
122
122
  "nodemailer": "6.9.13",
123
123
  "object-hash": "3.0.0",
124
- "openapi3-ts": "4.3.1",
124
+ "openapi3-ts": "4.3.2",
125
125
  "openid-client": "5.6.5",
126
126
  "ora": "8.0.1",
127
127
  "otplib": "12.0.1",
128
128
  "p-limit": "5.0.0",
129
129
  "p-queue": "8.0.1",
130
130
  "papaparse": "5.4.1",
131
- "pino": "9.0.0",
131
+ "pino": "9.1.0",
132
132
  "pino-http": "9.0.0",
133
133
  "pino-http-print": "3.1.0",
134
134
  "pino-pretty": "11.0.0",
@@ -140,35 +140,35 @@
140
140
  "sharp": "0.33.3",
141
141
  "snappy": "7.2.2",
142
142
  "stream-json": "1.8.0",
143
- "tar": "7.1.0",
144
- "tsx": "4.9.3",
143
+ "tar": "7.2.0",
144
+ "tsx": "4.12.0",
145
145
  "wellknown": "0.5.0",
146
146
  "ws": "8.17.0",
147
147
  "zod": "3.23.8",
148
148
  "zod-validation-error": "3.2.0",
149
- "@directus/app": "12.1.2",
150
149
  "@directus/constants": "11.0.4",
151
- "@directus/env": "1.1.5",
152
- "@directus/errors": "0.3.1",
153
- "@directus/extensions": "1.0.6",
154
- "@directus/extensions-registry": "1.0.6",
155
- "@directus/extensions-sdk": "11.0.6",
156
- "@directus/memory": "1.0.8",
157
- "@directus/pressure": "1.0.19",
158
- "@directus/schema": "11.0.2",
159
- "@directus/specs": "10.2.9",
150
+ "@directus/app": "12.1.4",
151
+ "@directus/env": "1.1.6",
152
+ "@directus/extensions": "1.0.8",
153
+ "@directus/errors": "0.3.2",
154
+ "@directus/extensions-registry": "1.0.8",
155
+ "@directus/extensions-sdk": "11.0.8",
160
156
  "@directus/format-title": "10.1.2",
157
+ "@directus/pressure": "1.0.20",
158
+ "@directus/memory": "1.0.9",
159
+ "@directus/schema": "11.0.3",
160
+ "@directus/specs": "10.2.10",
161
161
  "@directus/storage": "10.0.13",
162
- "@directus/storage-driver-cloudinary": "10.0.21",
163
- "@directus/storage-driver-azure": "10.0.21",
164
- "@directus/storage-driver-gcs": "10.0.22",
162
+ "@directus/storage-driver-azure": "10.0.22",
163
+ "@directus/storage-driver-gcs": "10.0.23",
165
164
  "@directus/storage-driver-local": "10.0.20",
166
- "@directus/storage-driver-supabase": "1.0.13",
167
- "@directus/system-data": "1.0.3",
168
- "@directus/storage-driver-s3": "10.0.22",
169
- "@directus/utils": "11.0.8",
170
- "directus": "10.11.2",
171
- "@directus/validation": "0.0.16"
165
+ "@directus/storage-driver-cloudinary": "10.0.22",
166
+ "@directus/storage-driver-s3": "10.0.23",
167
+ "@directus/storage-driver-supabase": "1.0.14",
168
+ "@directus/system-data": "1.0.4",
169
+ "@directus/utils": "11.0.9",
170
+ "@directus/validation": "0.0.17",
171
+ "directus": "10.12.1"
172
172
  },
173
173
  "devDependencies": {
174
174
  "@ngneat/falso": "7.2.0",
@@ -182,7 +182,7 @@
182
182
  "@types/destroy": "1.0.3",
183
183
  "@types/encodeurl": "1.0.2",
184
184
  "@types/express": "4.17.21",
185
- "@types/express-serve-static-core": "4.19.0",
185
+ "@types/express-serve-static-core": "4.19.3",
186
186
  "@types/fs-extra": "11.0.4",
187
187
  "@types/glob-to-regexp": "0.4.4",
188
188
  "@types/inquirer": "9.0.7",
@@ -210,8 +210,8 @@
210
210
  "typescript": "5.4.5",
211
211
  "vitest": "1.5.3",
212
212
  "@directus/random": "0.2.8",
213
- "@directus/types": "11.1.2",
214
- "@directus/tsconfig": "1.0.1"
213
+ "@directus/tsconfig": "1.0.1",
214
+ "@directus/types": "11.1.3"
215
215
  },
216
216
  "optionalDependencies": {
217
217
  "@keyv/redis": "2.8.4",