@directus/api 19.2.0 → 19.3.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.
Files changed (42) 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 +200 -11
  22. package/dist/services/users.js +64 -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/package.json +25 -25
@@ -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,10 +1,16 @@
1
- import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
1
+ import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
2
2
  import { getMatch } from 'ip-matching';
3
+ import { checkIncreasedUserLimits } from '../telemetry/utils/check-increased-user-limits.js';
4
+ import { getRoleCountsByUsers } from '../telemetry/utils/get-role-counts-by-users.js';
5
+ import {} from '../telemetry/utils/get-user-count.js';
6
+ import { getUserCountsByRoles } from '../telemetry/utils/get-user-counts-by-roles.js';
3
7
  import { transaction } from '../utils/transaction.js';
4
8
  import { ItemsService } from './items.js';
5
9
  import { PermissionsService } from './permissions/index.js';
6
10
  import { PresetsService } from './presets.js';
7
11
  import { UsersService } from './users.js';
12
+ import { shouldClearCache } from '../utils/should-clear-cache.js';
13
+ import { omit } from 'lodash-es';
8
14
  export class RolesService extends ItemsService {
9
15
  constructor(options) {
10
16
  super('directus_roles', options);
@@ -24,8 +30,9 @@ export class RolesService extends ItemsService {
24
30
  }
25
31
  async checkForOtherAdminUsers(key, users) {
26
32
  const role = await this.knex.select('admin_access').from('directus_roles').where('id', '=', key).first();
33
+ // No-op if role doesn't exist
27
34
  if (!role)
28
- throw new ForbiddenError();
35
+ return;
29
36
  const usersBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map((user) => user.id);
30
37
  const usersAdded = [];
31
38
  const usersUpdated = [];
@@ -151,44 +158,211 @@ export class RolesService extends ItemsService {
151
158
  });
152
159
  }
153
160
  }
161
+ getRoleAccessType(data) {
162
+ if ('admin_access' in data && data['admin_access'] === true) {
163
+ return 'admin';
164
+ }
165
+ else if (('app_access' in data && data['app_access'] === true) || 'app_access' in data === false) {
166
+ return 'app';
167
+ }
168
+ else {
169
+ return 'api';
170
+ }
171
+ }
154
172
  async createOne(data, opts) {
155
173
  this.assertValidIpAccess(data);
174
+ const increasedCounts = {
175
+ admin: 0,
176
+ app: 0,
177
+ api: 0,
178
+ };
179
+ const existingIds = [];
180
+ if ('users' in data) {
181
+ const type = this.getRoleAccessType(data);
182
+ increasedCounts[type] += data['users'].length;
183
+ for (const user of data['users']) {
184
+ if (typeof user === 'string') {
185
+ existingIds.push(user);
186
+ }
187
+ else if (typeof user === 'object' && 'id' in user) {
188
+ existingIds.push(user['id']);
189
+ }
190
+ }
191
+ }
192
+ await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
156
193
  return super.createOne(data, opts);
157
194
  }
158
195
  async createMany(data, opts) {
196
+ const increasedCounts = {
197
+ admin: 0,
198
+ app: 0,
199
+ api: 0,
200
+ };
201
+ const existingIds = [];
159
202
  for (const partialItem of data) {
160
203
  this.assertValidIpAccess(partialItem);
204
+ if ('users' in partialItem) {
205
+ const type = this.getRoleAccessType(partialItem);
206
+ increasedCounts[type] += partialItem['users'].length;
207
+ for (const user of partialItem['users']) {
208
+ if (typeof user === 'string') {
209
+ existingIds.push(user);
210
+ }
211
+ else if (typeof user === 'object' && 'id' in user) {
212
+ existingIds.push(user['id']);
213
+ }
214
+ }
215
+ }
161
216
  }
217
+ await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
162
218
  return super.createMany(data, opts);
163
219
  }
164
220
  async updateOne(key, data, opts) {
165
221
  this.assertValidIpAccess(data);
166
222
  try {
223
+ const increasedCounts = {
224
+ admin: 0,
225
+ app: 0,
226
+ api: 0,
227
+ };
228
+ let increasedUsers = 0;
229
+ const existingIds = [];
230
+ let existingRole = await this.knex
231
+ .count('directus_users.id', { as: 'count' })
232
+ .select('directus_roles.admin_access', 'directus_roles.app_access')
233
+ .from('directus_users')
234
+ .where('directus_roles.id', '=', key)
235
+ .andWhere('directus_users.status', '=', 'active')
236
+ .leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
237
+ .groupBy('directus_roles.admin_access', 'directus_roles.app_access')
238
+ .first();
239
+ if (!existingRole) {
240
+ try {
241
+ const role = (await this.knex
242
+ .select('admin_access', 'app_access')
243
+ .from('directus_roles')
244
+ .where('id', '=', key)
245
+ .first()) ?? { admin_access: null, app_access: null };
246
+ existingRole = { count: 0, ...role };
247
+ }
248
+ catch {
249
+ existingRole = { count: 0, admin_access: null, app_access: null };
250
+ }
251
+ }
167
252
  if ('users' in data) {
168
253
  await this.checkForOtherAdminUsers(key, data['users']);
254
+ const users = data['users'];
255
+ if (Array.isArray(users)) {
256
+ increasedUsers = users.length - Number(existingRole.count);
257
+ for (const user of users) {
258
+ if (typeof user === 'string') {
259
+ existingIds.push(user);
260
+ }
261
+ else if (typeof user === 'object' && 'id' in user) {
262
+ existingIds.push(user['id']);
263
+ }
264
+ }
265
+ }
266
+ else {
267
+ increasedUsers += users.create.length;
268
+ increasedUsers -= users.delete.length;
269
+ const userIds = [];
270
+ for (const user of users.update) {
271
+ if ('status' in user) {
272
+ // account for users being activated and deactivated
273
+ if (user['status'] === 'active') {
274
+ increasedUsers++;
275
+ }
276
+ else {
277
+ increasedUsers--;
278
+ }
279
+ }
280
+ userIds.push(user.id);
281
+ }
282
+ try {
283
+ const existingCounts = await getRoleCountsByUsers(this.knex, userIds);
284
+ if (existingRole.admin_access) {
285
+ increasedUsers += existingCounts.app + existingCounts.api;
286
+ }
287
+ else if (existingRole.app_access) {
288
+ increasedUsers += existingCounts.admin + existingCounts.api;
289
+ }
290
+ else {
291
+ increasedUsers += existingCounts.admin + existingCounts.app;
292
+ }
293
+ }
294
+ catch {
295
+ // ignore failed user call
296
+ }
297
+ }
298
+ }
299
+ let isAccessChanged = false;
300
+ let accessType = 'api';
301
+ if ('app_access' in data) {
302
+ if (data['app_access'] === true) {
303
+ accessType = 'app';
304
+ if (!existingRole.app_access)
305
+ isAccessChanged = true;
306
+ }
307
+ else if (existingRole.app_access) {
308
+ isAccessChanged = true;
309
+ }
310
+ }
311
+ else if (existingRole.app_access) {
312
+ accessType = 'app';
313
+ }
314
+ if ('admin_access' in data) {
315
+ if (data['admin_access'] === true) {
316
+ accessType = 'admin';
317
+ if (!existingRole.admin_access)
318
+ isAccessChanged = true;
319
+ }
320
+ else if (existingRole.admin_access) {
321
+ isAccessChanged = true;
322
+ }
323
+ }
324
+ else if (existingRole.admin_access) {
325
+ accessType = 'admin';
326
+ }
327
+ if (isAccessChanged) {
328
+ increasedCounts[accessType] += Number(existingRole.count);
169
329
  }
330
+ increasedCounts[accessType] += increasedUsers;
331
+ await checkIncreasedUserLimits(this.knex, increasedCounts, existingIds);
170
332
  }
171
333
  catch (err) {
172
334
  (opts || (opts = {})).preMutationError = err;
173
335
  }
174
336
  return super.updateOne(key, data, opts);
175
337
  }
176
- async updateBatch(data, opts) {
338
+ async updateBatch(data, opts = {}) {
177
339
  for (const partialItem of data) {
178
340
  this.assertValidIpAccess(partialItem);
179
341
  }
180
342
  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);
343
+ if (!opts.mutationTracker) {
344
+ opts.mutationTracker = this.createMutationTracker();
345
+ }
346
+ const keys = [];
183
347
  try {
184
- if (setsToNoAdmin) {
185
- await this.checkForOtherAdminRoles(keys);
186
- }
348
+ await transaction(this.knex, async (trx) => {
349
+ const service = new RolesService({
350
+ accountability: this.accountability,
351
+ knex: trx,
352
+ schema: this.schema,
353
+ });
354
+ for (const item of data) {
355
+ const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
356
+ keys.push(await service.updateOne(item[primaryKeyField], omit(item, primaryKeyField), combinedOpts));
357
+ }
358
+ });
187
359
  }
188
- catch (err) {
189
- (opts || (opts = {})).preMutationError = err;
360
+ finally {
361
+ if (shouldClearCache(this.cache, opts, this.collection)) {
362
+ await this.cache.clear();
363
+ }
190
364
  }
191
- return super.updateBatch(data, opts);
365
+ return keys;
192
366
  }
193
367
  async updateMany(keys, data, opts) {
194
368
  this.assertValidIpAccess(data);
@@ -196,6 +370,21 @@ export class RolesService extends ItemsService {
196
370
  if ('admin_access' in data && data['admin_access'] === false) {
197
371
  await this.checkForOtherAdminRoles(keys);
198
372
  }
373
+ if ('admin_access' in data || 'app_access' in data) {
374
+ const existingCounts = await getUserCountsByRoles(this.knex, keys);
375
+ const increasedCounts = {
376
+ admin: 0,
377
+ app: 0,
378
+ api: 0,
379
+ };
380
+ const type = this.getRoleAccessType(data);
381
+ for (const [existingType, existingCount] of Object.entries(existingCounts)) {
382
+ if (existingType === type)
383
+ continue;
384
+ increasedCounts[type] += existingCount;
385
+ }
386
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
387
+ }
199
388
  }
200
389
  catch (err) {
201
390
  (opts || (opts = {})).preMutationError = err;
@@ -1,13 +1,17 @@
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';
11
15
  import { getSecret } from '../utils/get-secret.js';
12
16
  import isUrlAllowed from '../utils/is-url-allowed.js';
13
17
  import { verifyJWT } from '../utils/jwt.js';
@@ -164,6 +168,7 @@ export class UsersService extends ItemsService {
164
168
  async createMany(data, opts) {
165
169
  const emails = data['map']((payload) => payload['email']).filter((email) => email);
166
170
  const passwords = data['map']((payload) => payload['password']).filter((password) => password);
171
+ const roles = data['map']((payload) => payload['role']).filter((role) => role);
167
172
  try {
168
173
  if (emails.length) {
169
174
  this.validateEmail(emails);
@@ -172,6 +177,33 @@ export class UsersService extends ItemsService {
172
177
  if (passwords.length) {
173
178
  await this.checkPasswordPolicy(passwords);
174
179
  }
180
+ if (roles.length) {
181
+ const increasedCounts = {
182
+ admin: 0,
183
+ app: 0,
184
+ api: 0,
185
+ };
186
+ const existingRoles = [];
187
+ for (const role of roles) {
188
+ if (typeof role === 'object') {
189
+ if ('admin_access' in role && role['admin_access'] === true) {
190
+ increasedCounts.admin++;
191
+ }
192
+ else if ('app_access' in role && role['app_access'] === true) {
193
+ increasedCounts.app++;
194
+ }
195
+ else {
196
+ increasedCounts.api++;
197
+ }
198
+ }
199
+ else {
200
+ existingRoles.push(role);
201
+ }
202
+ }
203
+ const existingRoleCounts = await getRoleCountsByRoles(this.knex, existingRoles);
204
+ mergeWith(increasedCounts, existingRoleCounts, (x, y) => x + y);
205
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
206
+ }
175
207
  }
176
208
  catch (err) {
177
209
  (opts || (opts = {})).preMutationError = err;
@@ -226,7 +258,11 @@ export class UsersService extends ItemsService {
226
258
  const role = data['role']?.id ?? data['role'];
227
259
  let newRole;
228
260
  if (typeof role === 'string') {
229
- newRole = await this.knex.select('admin_access').from('directus_roles').where('id', role).first();
261
+ newRole = await this.knex
262
+ .select('admin_access', 'app_access')
263
+ .from('directus_roles')
264
+ .where('id', role)
265
+ .first();
230
266
  }
231
267
  else {
232
268
  newRole = role;
@@ -234,10 +270,35 @@ export class UsersService extends ItemsService {
234
270
  if (!newRole?.admin_access) {
235
271
  await this.checkRemainingAdminExistence(keys);
236
272
  }
273
+ if (newRole) {
274
+ const existingCounts = await getRoleCountsByUsers(this.knex, keys);
275
+ const increasedCounts = {
276
+ admin: 0,
277
+ app: 0,
278
+ api: 0,
279
+ };
280
+ if (toBoolean(newRole.admin_access)) {
281
+ increasedCounts.admin = keys.length - existingCounts.admin;
282
+ }
283
+ else if (toBoolean(newRole.app_access)) {
284
+ increasedCounts.app = keys.length - existingCounts.app;
285
+ }
286
+ else {
287
+ increasedCounts.api = keys.length - existingCounts.api;
288
+ }
289
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
290
+ }
291
+ }
292
+ if (data['role'] === null) {
293
+ await checkIncreasedUserLimits(this.knex, { admin: 0, app: 0, api: 1 });
237
294
  }
238
295
  if (data['status'] !== undefined && data['status'] !== 'active') {
239
296
  await this.checkRemainingActiveAdmin(keys);
240
297
  }
298
+ if (data['status'] === 'active') {
299
+ const increasedCounts = await getRoleCountsByUsers(this.knex, keys, { inactiveUsers: true });
300
+ await checkIncreasedUserLimits(this.knex, increasedCounts);
301
+ }
241
302
  if (data['email']) {
242
303
  if (keys.length > 1) {
243
304
  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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "19.2.0",
3
+ "version": "19.3.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -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",
@@ -146,29 +146,29 @@
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",
149
+ "@directus/app": "12.1.3",
150
+ "@directus/env": "1.1.6",
150
151
  "@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",
152
+ "@directus/errors": "0.3.2",
153
+ "@directus/extensions": "1.0.7",
154
+ "@directus/extensions-registry": "1.0.7",
160
155
  "@directus/format-title": "10.1.2",
156
+ "@directus/extensions-sdk": "11.0.7",
157
+ "@directus/pressure": "1.0.20",
158
+ "@directus/memory": "1.0.9",
159
+ "@directus/schema": "11.0.2",
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-cloudinary": "10.0.22",
164
+ "@directus/storage-driver-gcs": "10.0.23",
165
165
  "@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"
166
+ "@directus/storage-driver-supabase": "1.0.14",
167
+ "@directus/storage-driver-s3": "10.0.23",
168
+ "@directus/system-data": "1.0.4",
169
+ "@directus/utils": "11.0.9",
170
+ "directus": "10.12.0",
171
+ "@directus/validation": "0.0.17"
172
172
  },
173
173
  "devDependencies": {
174
174
  "@ngneat/falso": "7.2.0",
@@ -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.2"
215
215
  },
216
216
  "optionalDependencies": {
217
217
  "@keyv/redis": "2.8.4",