@directus/api 14.1.2 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/dist/app.js +8 -6
  2. package/dist/auth/drivers/ldap.js +7 -4
  3. package/dist/auth/drivers/local.js +3 -2
  4. package/dist/auth/drivers/oauth2.js +11 -5
  5. package/dist/auth/drivers/openid.js +11 -5
  6. package/dist/auth/drivers/saml.js +6 -4
  7. package/dist/auth.js +7 -4
  8. package/dist/bus/index.d.ts +1 -0
  9. package/dist/bus/index.js +1 -0
  10. package/dist/bus/lib/use-bus.d.ts +9 -0
  11. package/dist/bus/lib/use-bus.js +21 -0
  12. package/dist/cache.js +9 -9
  13. package/dist/cli/commands/bootstrap/index.js +6 -2
  14. package/dist/cli/commands/count/index.js +2 -1
  15. package/dist/cli/commands/database/install.js +2 -1
  16. package/dist/cli/commands/database/migrate.js +2 -1
  17. package/dist/cli/commands/roles/create.js +2 -1
  18. package/dist/cli/commands/schema/apply.js +46 -34
  19. package/dist/cli/commands/schema/snapshot.js +6 -5
  20. package/dist/cli/commands/users/create.js +4 -3
  21. package/dist/cli/commands/users/passwd.js +5 -4
  22. package/dist/cli/index.js +2 -2
  23. package/dist/cli/load-extensions.js +4 -2
  24. package/dist/cli/utils/create-env/env-stub.liquid +1 -1
  25. package/dist/constants.d.ts +1 -1
  26. package/dist/constants.js +4 -1
  27. package/dist/controllers/assets.js +5 -3
  28. package/dist/controllers/auth.js +5 -4
  29. package/dist/controllers/extensions.js +18 -6
  30. package/dist/controllers/files.js +3 -3
  31. package/dist/controllers/schema.js +3 -2
  32. package/dist/controllers/shares.js +3 -3
  33. package/dist/database/helpers/index.d.ts +1 -1
  34. package/dist/database/index.d.ts +2 -1
  35. package/dist/database/index.js +11 -3
  36. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +3 -1
  37. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -1
  38. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  39. package/dist/database/migrations/20230721A-require-shares-fields.js +2 -1
  40. package/dist/database/migrations/20231215A-add-focalpoints.d.ts +3 -0
  41. package/dist/database/migrations/20231215A-add-focalpoints.js +12 -0
  42. package/dist/database/migrations/run.js +2 -1
  43. package/dist/database/run-ast.js +5 -2
  44. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -7
  45. package/dist/database/system-data/fields/files.yaml +16 -0
  46. package/dist/database/system-data/relations/relations.yaml +4 -0
  47. package/dist/emitter.d.ts +1 -0
  48. package/dist/emitter.js +4 -1
  49. package/dist/extensions/lib/get-extensions-path.d.ts +1 -1
  50. package/dist/extensions/lib/get-extensions-path.js +2 -1
  51. package/dist/extensions/lib/get-extensions.d.ts +1 -1
  52. package/dist/extensions/lib/get-extensions.js +32 -8
  53. package/dist/extensions/lib/get-shared-deps-mapping.js +7 -5
  54. package/dist/extensions/lib/sandbox/register/call-reference.js +4 -2
  55. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -0
  56. package/dist/extensions/lib/sandbox/sdk/generators/log.js +2 -1
  57. package/dist/extensions/lib/sync-extensions.js +6 -4
  58. package/dist/extensions/manager.d.ts +5 -0
  59. package/dist/extensions/manager.js +84 -34
  60. package/dist/flows.js +13 -7
  61. package/dist/logger.d.ts +7 -6
  62. package/dist/logger.js +116 -91
  63. package/dist/mailer.js +4 -2
  64. package/dist/middleware/cache.js +4 -2
  65. package/dist/middleware/check-ip.js +25 -6
  66. package/dist/middleware/cors.js +2 -1
  67. package/dist/middleware/error-handler.js +5 -5
  68. package/dist/middleware/rate-limiter-global.js +4 -2
  69. package/dist/middleware/rate-limiter-ip.js +16 -12
  70. package/dist/middleware/respond.js +4 -2
  71. package/dist/operations/log/index.js +2 -1
  72. package/dist/rate-limiter.d.ts +2 -1
  73. package/dist/rate-limiter.js +5 -2
  74. package/dist/redis/index.d.ts +3 -0
  75. package/dist/redis/index.js +3 -0
  76. package/dist/redis/lib/create-redis.d.ts +7 -0
  77. package/dist/redis/lib/create-redis.js +12 -0
  78. package/dist/redis/lib/use-redis.d.ts +16 -0
  79. package/dist/redis/lib/use-redis.js +22 -0
  80. package/dist/redis/utils/redis-config-available.d.ts +4 -0
  81. package/dist/redis/utils/redis-config-available.js +8 -0
  82. package/dist/request/request-interceptor.js +7 -5
  83. package/dist/request/response-interceptor.js +2 -2
  84. package/dist/request/validate-ip.d.ts +1 -1
  85. package/dist/request/validate-ip.js +23 -7
  86. package/dist/server.d.ts +2 -0
  87. package/dist/server.js +11 -7
  88. package/dist/services/activity.js +5 -4
  89. package/dist/services/assets.d.ts +2 -0
  90. package/dist/services/assets.js +9 -4
  91. package/dist/services/authentication.js +17 -9
  92. package/dist/services/collections.js +5 -4
  93. package/dist/services/extensions.d.ts +15 -9
  94. package/dist/services/extensions.js +75 -40
  95. package/dist/services/fields.js +9 -4
  96. package/dist/services/files.d.ts +2 -2
  97. package/dist/services/files.js +22 -14
  98. package/dist/services/graphql/index.js +96 -18
  99. package/dist/services/graphql/subscription.js +2 -2
  100. package/dist/services/graphql/types/bigint.js +16 -5
  101. package/dist/services/graphql/utils/process-error.d.ts +4 -1
  102. package/dist/services/graphql/utils/process-error.js +10 -8
  103. package/dist/services/import-export/index.js +5 -3
  104. package/dist/services/items.js +12 -8
  105. package/dist/services/mail/index.js +4 -2
  106. package/dist/services/notifications.js +7 -3
  107. package/dist/services/payload.js +3 -3
  108. package/dist/services/relations.js +19 -10
  109. package/dist/services/server.js +7 -7
  110. package/dist/services/shares.js +3 -2
  111. package/dist/services/specifications.js +5 -4
  112. package/dist/services/users.js +24 -13
  113. package/dist/services/versions.js +6 -5
  114. package/dist/services/webhooks.d.ts +2 -2
  115. package/dist/services/webhooks.js +2 -2
  116. package/dist/services/websocket.d.ts +1 -1
  117. package/dist/services/websocket.js +4 -3
  118. package/dist/storage/register-drivers.js +2 -1
  119. package/dist/storage/register-locations.js +2 -1
  120. package/dist/synchronization.js +3 -1
  121. package/dist/telemetry/index.d.ts +4 -0
  122. package/dist/telemetry/index.js +4 -0
  123. package/dist/telemetry/lib/get-report.d.ts +5 -0
  124. package/dist/telemetry/lib/get-report.js +42 -0
  125. package/dist/telemetry/lib/init-telemetry.d.ts +11 -0
  126. package/dist/telemetry/lib/init-telemetry.js +30 -0
  127. package/dist/telemetry/lib/send-report.d.ts +5 -0
  128. package/dist/telemetry/lib/send-report.js +23 -0
  129. package/dist/telemetry/lib/track.d.ts +10 -0
  130. package/dist/telemetry/lib/track.js +30 -0
  131. package/dist/telemetry/types/report.d.ts +58 -0
  132. package/dist/telemetry/types/report.js +1 -0
  133. package/dist/telemetry/utils/get-item-count.d.ts +26 -0
  134. package/dist/telemetry/utils/get-item-count.js +36 -0
  135. package/dist/telemetry/utils/get-random-wait-time.d.ts +5 -0
  136. package/dist/telemetry/utils/get-random-wait-time.js +5 -0
  137. package/dist/telemetry/utils/get-user-count.d.ts +7 -0
  138. package/dist/telemetry/utils/get-user-count.js +30 -0
  139. package/dist/telemetry/utils/get-user-item-count.d.ts +13 -0
  140. package/dist/telemetry/utils/get-user-item-count.js +18 -0
  141. package/dist/types/assets.d.ts +2 -0
  142. package/dist/utils/apply-diff.js +2 -1
  143. package/dist/utils/apply-query.js +2 -2
  144. package/dist/utils/delete-from-require-cache.js +2 -1
  145. package/dist/utils/get-accountability-for-token.js +3 -2
  146. package/dist/utils/get-auth-providers.js +2 -1
  147. package/dist/utils/get-cache-headers.js +5 -2
  148. package/dist/utils/get-cache-key.js +1 -1
  149. package/dist/utils/get-config-from-env.js +2 -1
  150. package/dist/utils/get-default-value.js +4 -3
  151. package/dist/utils/get-ip-from-req.d.ts +1 -1
  152. package/dist/utils/get-ip-from-req.js +5 -3
  153. package/dist/utils/get-permissions.js +5 -3
  154. package/dist/utils/get-schema.js +5 -2
  155. package/dist/utils/get-snapshot-diff.js +7 -9
  156. package/dist/utils/get-snapshot.js +5 -5
  157. package/dist/utils/get-versioned-hash.js +1 -1
  158. package/dist/utils/ip-in-networks.d.ts +6 -0
  159. package/dist/utils/ip-in-networks.js +13 -0
  160. package/dist/utils/is-url-allowed.js +2 -1
  161. package/dist/utils/job-queue.d.ts +1 -0
  162. package/dist/utils/job-queue.js +3 -0
  163. package/dist/utils/md.d.ts +1 -1
  164. package/dist/utils/md.js +3 -2
  165. package/dist/utils/sanitize-query.js +7 -2
  166. package/dist/utils/sanitize-schema.d.ts +1 -1
  167. package/dist/utils/should-clear-cache.js +2 -1
  168. package/dist/utils/should-skip-cache.js +2 -1
  169. package/dist/utils/transformations.js +95 -12
  170. package/dist/utils/validate-env.js +4 -2
  171. package/dist/utils/validate-query.js +8 -3
  172. package/dist/utils/validate-snapshot.js +3 -3
  173. package/dist/utils/validate-storage.js +4 -2
  174. package/dist/webhooks.js +4 -3
  175. package/dist/websocket/controllers/base.d.ts +2 -0
  176. package/dist/websocket/controllers/base.js +12 -6
  177. package/dist/websocket/controllers/graphql.d.ts +2 -0
  178. package/dist/websocket/controllers/graphql.js +5 -3
  179. package/dist/websocket/controllers/hooks.js +3 -2
  180. package/dist/websocket/controllers/index.d.ts +2 -0
  181. package/dist/websocket/controllers/index.js +4 -2
  182. package/dist/websocket/controllers/rest.d.ts +2 -0
  183. package/dist/websocket/controllers/rest.js +4 -2
  184. package/dist/websocket/errors.js +2 -1
  185. package/dist/websocket/handlers/heartbeat.js +4 -3
  186. package/dist/websocket/handlers/subscribe.d.ts +2 -2
  187. package/dist/websocket/handlers/subscribe.js +5 -4
  188. package/dist/websocket/types.d.ts +3 -1
  189. package/package.json +114 -115
  190. package/dist/__utils__/items-utils.d.ts +0 -2
  191. package/dist/__utils__/items-utils.js +0 -31
  192. package/dist/__utils__/mock-env.d.ts +0 -18
  193. package/dist/__utils__/mock-env.js +0 -41
  194. package/dist/__utils__/schemas.d.ts +0 -13
  195. package/dist/__utils__/schemas.js +0 -301
  196. package/dist/__utils__/snapshots.d.ts +0 -5
  197. package/dist/__utils__/snapshots.js +0 -903
  198. package/dist/env.d.ts +0 -13
  199. package/dist/env.js +0 -505
  200. package/dist/messenger.d.ts +0 -24
  201. package/dist/messenger.js +0 -64
  202. package/dist/utils/package.d.ts +0 -2
  203. package/dist/utils/package.js +0 -6
  204. package/dist/utils/telemetry.d.ts +0 -1
  205. package/dist/utils/telemetry.js +0 -23
  206. package/dist/utils/to-boolean.d.ts +0 -4
  207. package/dist/utils/to-boolean.js +0 -6
@@ -1,13 +1,14 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import formatTitle from '@directus/format-title';
2
3
  import { spec } from '@directus/specs';
4
+ import { version } from 'directus/version';
3
5
  import { cloneDeep, mergeWith } from 'lodash-es';
4
6
  import { OAS_REQUIRED_SCHEMAS } from '../constants.js';
5
7
  import getDatabase from '../database/index.js';
6
- import env from '../env.js';
7
8
  import { getRelationType } from '../utils/get-relation-type.js';
8
- import { version } from '../utils/package.js';
9
- import { GraphQLService } from './graphql/index.js';
10
9
  import { reduceSchema } from '../utils/reduce-schema.js';
10
+ import { GraphQLService } from './graphql/index.js';
11
+ const env = useEnv();
11
12
  export class SpecificationService {
12
13
  accountability;
13
14
  knex;
@@ -19,7 +20,7 @@ export class SpecificationService {
19
20
  this.knex = knex || getDatabase();
20
21
  this.schema = schema;
21
22
  this.oas = new OASSpecsService({ knex, schema, accountability });
22
- this.graphql = new GraphQLSpecsService({ knex, schema });
23
+ this.graphql = new GraphQLSpecsService({ knex, schema, accountability });
23
24
  }
24
25
  }
25
26
  class OASSpecsService {
@@ -1,3 +1,5 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
1
3
  import { getSimpleHash, toArray } from '@directus/utils';
2
4
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
5
  import Joi from 'joi';
@@ -5,9 +7,6 @@ import jwt from 'jsonwebtoken';
5
7
  import { cloneDeep, isEmpty } from 'lodash-es';
6
8
  import { performance } from 'perf_hooks';
7
9
  import getDatabase from '../database/index.js';
8
- import env from '../env.js';
9
- import { ForbiddenError } from '@directus/errors';
10
- import { InvalidPayloadError, RecordNotUniqueError, UnprocessableContentError } from '@directus/errors';
11
10
  import isUrlAllowed from '../utils/is-url-allowed.js';
12
11
  import { verifyJWT } from '../utils/jwt.js';
13
12
  import { stall } from '../utils/stall.js';
@@ -15,6 +14,7 @@ import { Url } from '../utils/url.js';
15
14
  import { ItemsService } from './items.js';
16
15
  import { MailService } from './mail/index.js';
17
16
  import { SettingsService } from './settings.js';
17
+ const env = useEnv();
18
18
  export class UsersService extends ItemsService {
19
19
  constructor(options) {
20
20
  super('directus_users', options);
@@ -116,7 +116,7 @@ export class UsersService extends ItemsService {
116
116
  */
117
117
  async getUserByEmail(email) {
118
118
  return await this.knex
119
- .select('id', 'role', 'status', 'password')
119
+ .select('id', 'role', 'status', 'password', 'email')
120
120
  .from('directus_users')
121
121
  .whereRaw(`LOWER(??) = ?`, ['email', email.toLowerCase()])
122
122
  .first();
@@ -213,9 +213,20 @@ export class UsersService extends ItemsService {
213
213
  async updateMany(keys, data, opts) {
214
214
  try {
215
215
  if (data['role']) {
216
- // data['role'] will be an object with id with GraphQL mutations
217
- const roleId = data['role']?.id ?? data['role'];
218
- const newRole = await this.knex.select('admin_access').from('directus_roles').where('id', roleId).first();
216
+ /*
217
+ * data['role'] has the following cases:
218
+ * - a string with existing role id
219
+ * - an object with existing role id for GraphQL mutations
220
+ * - an object with data for new role
221
+ */
222
+ const role = data['role']?.id ?? data['role'];
223
+ let newRole;
224
+ if (typeof role === 'string') {
225
+ newRole = await this.knex.select('admin_access').from('directus_roles').where('id', role).first();
226
+ }
227
+ else {
228
+ newRole = role;
229
+ }
219
230
  if (!newRole?.admin_access) {
220
231
  await this.checkRemainingAdminExistence(keys);
221
232
  }
@@ -325,13 +336,13 @@ export class UsersService extends ItemsService {
325
336
  if (isEmpty(user) || user.status === 'invited') {
326
337
  const subjectLine = subject ?? "You've been invited";
327
338
  await mailService.send({
328
- to: email,
339
+ to: user?.email ?? email,
329
340
  subject: subjectLine,
330
341
  template: {
331
342
  name: 'user-invitation',
332
343
  data: {
333
- url: this.inviteUrl(email, url),
334
- email,
344
+ url: this.inviteUrl(user?.email ?? email, url),
345
+ email: user?.email ?? email,
335
346
  },
336
347
  },
337
348
  });
@@ -369,20 +380,20 @@ export class UsersService extends ItemsService {
369
380
  knex: this.knex,
370
381
  accountability: this.accountability,
371
382
  });
372
- const payload = { email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
383
+ const payload = { email: user.email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
373
384
  const token = jwt.sign(payload, env['SECRET'], { expiresIn: '1d', issuer: 'directus' });
374
385
  const acceptURL = url
375
386
  ? new Url(url).setQuery('token', token).toString()
376
387
  : new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
377
388
  const subjectLine = subject ? subject : 'Password Reset Request';
378
389
  await mailService.send({
379
- to: email,
390
+ to: user.email,
380
391
  subject: subjectLine,
381
392
  template: {
382
393
  name: 'password-reset',
383
394
  data: {
384
395
  url: acceptURL,
385
- email,
396
+ email: user.email,
386
397
  },
387
398
  },
388
399
  });
@@ -172,26 +172,27 @@ export class VersionsService extends ItemsService {
172
172
  knex: this.knex,
173
173
  schema: this.schema,
174
174
  });
175
+ const { item, collection } = version;
175
176
  const activity = await activityService.createOne({
176
177
  action: Action.VERSION_SAVE,
177
178
  user: this.accountability?.user ?? null,
178
- collection: version['collection'],
179
+ collection,
179
180
  ip: this.accountability?.ip ?? null,
180
181
  user_agent: this.accountability?.userAgent ?? null,
181
182
  origin: this.accountability?.origin ?? null,
182
- item: version['item'],
183
+ item,
183
184
  });
184
185
  const revisionDelta = await payloadService.prepareDelta(data);
185
186
  await revisionsService.createOne({
186
187
  activity,
187
188
  version: key,
188
- collection: version['collection'],
189
- item: version['item'],
189
+ collection,
190
+ item,
190
191
  data: revisionDelta,
191
192
  delta: revisionDelta,
192
193
  });
193
194
  const { cache } = getCache();
194
- if (shouldClearCache(cache, undefined, version['collection'])) {
195
+ if (shouldClearCache(cache, undefined, collection)) {
195
196
  cache.clear();
196
197
  }
197
198
  return data;
@@ -1,8 +1,8 @@
1
- import type { Messenger } from '../messenger.js';
1
+ import type { Bus } from '@directus/memory';
2
2
  import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey, Webhook } from '../types/index.js';
3
3
  import { ItemsService } from './items.js';
4
4
  export declare class WebhooksService extends ItemsService<Webhook> {
5
- messenger: Messenger;
5
+ messenger: Bus;
6
6
  constructor(options: AbstractServiceOptions);
7
7
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
8
8
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
@@ -1,10 +1,10 @@
1
- import { getMessenger } from '../messenger.js';
1
+ import { useBus } from '../bus/index.js';
2
2
  import { ItemsService } from './items.js';
3
3
  export class WebhooksService extends ItemsService {
4
4
  messenger;
5
5
  constructor(options) {
6
6
  super('directus_webhooks', options);
7
- this.messenger = getMessenger();
7
+ this.messenger = useBus();
8
8
  }
9
9
  async createOne(data, opts) {
10
10
  const result = await super.createOne(data, opts);
@@ -1,6 +1,6 @@
1
1
  import type { ActionHandler } from '@directus/types';
2
- import type { WebSocketClient } from '../websocket/types.js';
3
2
  import type { WebSocketMessage } from '../websocket/messages.js';
3
+ import type { WebSocketClient } from '../websocket/types.js';
4
4
  export declare class WebSocketService {
5
5
  private controller;
6
6
  constructor();
@@ -1,8 +1,9 @@
1
- import { getWebSocketController } from '../websocket/controllers/index.js';
1
+ import { useEnv } from '@directus/env';
2
2
  import { ServiceUnavailableError } from '@directus/errors';
3
- import { toBoolean } from '../utils/to-boolean.js';
3
+ import { toBoolean } from '@directus/utils';
4
4
  import emitter from '../emitter.js';
5
- import env from '../env.js';
5
+ import { getWebSocketController } from '../websocket/controllers/index.js';
6
+ const env = useEnv();
6
7
  export class WebSocketService {
7
8
  controller;
8
9
  constructor() {
@@ -1,6 +1,7 @@
1
- import env from '../env.js';
1
+ import { useEnv } from '@directus/env';
2
2
  import { getStorageDriver } from './get-storage-driver.js';
3
3
  export const registerDrivers = async (storage) => {
4
+ const env = useEnv();
4
5
  const usedDrivers = [];
5
6
  for (const [key, value] of Object.entries(env)) {
6
7
  if ((key.startsWith('STORAGE_') && key.endsWith('_DRIVER')) === false)
@@ -1,7 +1,8 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { toArray } from '@directus/utils';
2
- import env from '../env.js';
3
3
  import { getConfigFromEnv } from '../utils/get-config-from-env.js';
4
4
  export const registerLocations = async (storage) => {
5
+ const env = useEnv();
5
6
  const locations = toArray(env['STORAGE_LOCATIONS']);
6
7
  locations.forEach((location) => {
7
8
  location = location.trim();
@@ -1,10 +1,11 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { Redis } from 'ioredis';
2
- import env from './env.js';
3
3
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
4
4
  let synchronizationManager;
5
5
  function getSynchronizationManager() {
6
6
  if (synchronizationManager)
7
7
  return synchronizationManager;
8
+ const env = useEnv();
8
9
  if (env['SYNCHRONIZATION_STORE'] === 'redis') {
9
10
  synchronizationManager = new SynchronizationManagerRedis();
10
11
  }
@@ -73,6 +74,7 @@ class SynchronizationManagerRedis {
73
74
  namespace;
74
75
  client;
75
76
  constructor() {
77
+ const env = useEnv();
76
78
  const config = getConfigFromEnv('REDIS');
77
79
  this.client = new Redis(env['REDIS'] ?? config);
78
80
  this.namespace = env['SYNCHRONIZATION_NAMESPACE'] ?? 'directus-sync';
@@ -0,0 +1,4 @@
1
+ export * from './lib/get-report.js';
2
+ export * from './lib/init-telemetry.js';
3
+ export * from './lib/send-report.js';
4
+ export * from './lib/track.js';
@@ -0,0 +1,4 @@
1
+ export * from './lib/get-report.js';
2
+ export * from './lib/init-telemetry.js';
3
+ export * from './lib/send-report.js';
4
+ export * from './lib/track.js';
@@ -0,0 +1,5 @@
1
+ import type { TelemetryReport } from '../types/report.js';
2
+ /**
3
+ * Create a telemetry report about the anonymous usage of the current installation
4
+ */
5
+ export declare const getReport: () => Promise<TelemetryReport>;
@@ -0,0 +1,42 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { version } from 'directus/version';
3
+ import { getDatabase, getDatabaseClient } from '../../database/index.js';
4
+ import { getItemCount } from '../utils/get-item-count.js';
5
+ import { getUserCount } from '../utils/get-user-count.js';
6
+ 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',
14
+ ];
15
+ /**
16
+ * Create a telemetry report about the anonymous usage of the current installation
17
+ */
18
+ export const getReport = async () => {
19
+ const db = getDatabase();
20
+ const env = useEnv();
21
+ const [basicCounts, userCounts, userItemCount] = await Promise.all([
22
+ getItemCount(db, basicCountCollections),
23
+ getUserCount(db),
24
+ getUserItemCount(db),
25
+ ]);
26
+ return {
27
+ url: env['PUBLIC_URL'],
28
+ version: version,
29
+ database: getDatabaseClient(),
30
+ dashboards: basicCounts.directus_dashboards,
31
+ extensions: basicCounts.directus_extensions,
32
+ files: basicCounts.directus_files,
33
+ flows: basicCounts.directus_flows,
34
+ roles: basicCounts.directus_roles,
35
+ shares: basicCounts.directus_shares,
36
+ admin_users: userCounts.admin,
37
+ app_users: userCounts.app,
38
+ api_users: userCounts.api,
39
+ collections: userItemCount.collections,
40
+ items: userItemCount.items,
41
+ };
42
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Exported to be able to test the anonymous callback function
3
+ */
4
+ export declare const jobCallback: () => void;
5
+ /**
6
+ * Initialize the telemetry tracking. Will generate a report on start, and set a schedule to report
7
+ * every 6 hours
8
+ *
9
+ * @returns Whether or not telemetry has been initialized
10
+ */
11
+ export declare const initTelemetry: () => Promise<boolean>;
@@ -0,0 +1,30 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
3
+ import { getCache } from '../../cache.js';
4
+ import { scheduleSynchronizedJob } from '../../utils/schedule.js';
5
+ import { track } from './track.js';
6
+ /**
7
+ * Exported to be able to test the anonymous callback function
8
+ */
9
+ export const jobCallback = () => {
10
+ track();
11
+ };
12
+ /**
13
+ * Initialize the telemetry tracking. Will generate a report on start, and set a schedule to report
14
+ * every 6 hours
15
+ *
16
+ * @returns Whether or not telemetry has been initialized
17
+ */
18
+ export const initTelemetry = async () => {
19
+ const env = useEnv();
20
+ if (toBoolean(env['TELEMETRY']) === false)
21
+ return false;
22
+ scheduleSynchronizedJob('telemetry', '0 */6 * * *', jobCallback);
23
+ const { lockCache } = getCache();
24
+ if (!(await lockCache.get('telemetry-lock'))) {
25
+ await lockCache.set('telemetry-lock', true, 30000);
26
+ track({ wait: false });
27
+ // Don't flush the lock. We want to debounce these calls across containers on startup
28
+ }
29
+ return true;
30
+ };
@@ -0,0 +1,5 @@
1
+ import type { TelemetryReport } from '../types/report.js';
2
+ /**
3
+ * Post an anonymous usage report to the centralized intake server
4
+ */
5
+ export declare const sendReport: (report: TelemetryReport) => Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { URL } from 'node:url';
3
+ /**
4
+ * Post an anonymous usage report to the centralized intake server
5
+ */
6
+ export const sendReport = async (report) => {
7
+ const env = useEnv();
8
+ const url = new URL('/v1/metrics', env['TELEMETRY_URL']);
9
+ const headers = {
10
+ 'Content-Type': 'application/json',
11
+ };
12
+ if (env['TELEMETRY_AUTHORIZATION']) {
13
+ headers['Authorization'] = env['TELEMETRY_AUTHORIZATION'];
14
+ }
15
+ const res = await fetch(url, {
16
+ method: 'POST',
17
+ body: JSON.stringify(report),
18
+ headers,
19
+ });
20
+ if (!res.ok) {
21
+ throw new Error(`[${res.status}] ${await res.text()}`);
22
+ }
23
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Generate and send a report. Will log on error, but not throw. No need to be awaited
3
+ *
4
+ * @param opts Options for the tracking
5
+ * @param opts.wait Whether or not to wait a random amount of time between 0 and 30 minutes
6
+ * @returns whether or not the tracking was successful
7
+ */
8
+ export declare const track: (opts?: {
9
+ wait: boolean;
10
+ }) => Promise<boolean>;
@@ -0,0 +1,30 @@
1
+ import { getNodeEnv } from '@directus/utils/node';
2
+ import { setTimeout } from 'timers/promises';
3
+ import { useLogger } from '../../logger.js';
4
+ import { getRandomWaitTime } from '../utils/get-random-wait-time.js';
5
+ import { getReport } from './get-report.js';
6
+ import { sendReport } from './send-report.js';
7
+ /**
8
+ * Generate and send a report. Will log on error, but not throw. No need to be awaited
9
+ *
10
+ * @param opts Options for the tracking
11
+ * @param opts.wait Whether or not to wait a random amount of time between 0 and 30 minutes
12
+ * @returns whether or not the tracking was successful
13
+ */
14
+ export const track = async (opts = { wait: true }) => {
15
+ const logger = useLogger();
16
+ if (opts.wait) {
17
+ await setTimeout(getRandomWaitTime());
18
+ }
19
+ try {
20
+ const report = await getReport();
21
+ await sendReport(report);
22
+ return true;
23
+ }
24
+ catch (err) {
25
+ if (getNodeEnv() === 'development') {
26
+ logger.error(err);
27
+ }
28
+ return false;
29
+ }
30
+ };
@@ -0,0 +1,58 @@
1
+ export interface TelemetryReport {
2
+ /**
3
+ * The project's web-facing public URL
4
+ */
5
+ url: string;
6
+ /**
7
+ * Current Directus version in use
8
+ */
9
+ version: string;
10
+ /**
11
+ * Database client in use
12
+ */
13
+ database: string;
14
+ /**
15
+ * Number of users in the system that have admin access to the system
16
+ */
17
+ admin_users: number;
18
+ /**
19
+ * Number of users that can access the app, but don't have admin access
20
+ */
21
+ app_users: number;
22
+ /**
23
+ * Number of users that can only access the API
24
+ */
25
+ api_users: number;
26
+ /**
27
+ * Number of unique roles in the system
28
+ */
29
+ roles: number;
30
+ /**
31
+ * Number of unique flows in the system
32
+ */
33
+ flows: number;
34
+ /**
35
+ * Number of unique dashboards in the system
36
+ */
37
+ dashboards: number;
38
+ /**
39
+ * Number of installed extensions in the system. Does not differentiate between enabled/disabled
40
+ */
41
+ extensions: number;
42
+ /**
43
+ * Number of Directus-managed collections
44
+ */
45
+ collections: number;
46
+ /**
47
+ * Total number of items in the non-system tables
48
+ */
49
+ items: number;
50
+ /**
51
+ * Number of files in the system
52
+ */
53
+ files: number;
54
+ /**
55
+ * Number of shares in the system
56
+ */
57
+ shares: number;
58
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { type Knex } from 'knex';
2
+ export interface CollectionCount {
3
+ collection: string;
4
+ count: number;
5
+ }
6
+ /**
7
+ * Get the item count of the given collection in the given database
8
+ * @param db Knex instance to count against
9
+ * @param collection Table to count rows in
10
+ * @returns Collection name and count
11
+ */
12
+ export declare const countCollection: (db: Knex, collection: string) => Promise<CollectionCount>;
13
+ /**
14
+ * Merge the given collection count in the object accumulator
15
+ * Intended for use with .reduce()
16
+ * @param acc Accumulator
17
+ * @param value Current collection count object in array
18
+ * @returns Updated accumulator
19
+ */
20
+ export declare const mergeResults: (acc: Record<string, number>, value: CollectionCount) => Record<string, number>;
21
+ /**
22
+ * Get an object of item counts for the given collections
23
+ * @param db Database instance to get counts in
24
+ * @param collections Array of table names to get count from
25
+ */
26
+ export declare const getItemCount: <T extends readonly string[]>(db: Knex, collections: T) => Promise<Record<T[number], number>>;
@@ -0,0 +1,36 @@
1
+ import {} from 'knex';
2
+ import pLimit from 'p-limit';
3
+ /**
4
+ * Get the item count of the given collection in the given database
5
+ * @param db Knex instance to count against
6
+ * @param collection Table to count rows in
7
+ * @returns Collection name and count
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) };
12
+ };
13
+ /**
14
+ * Merge the given collection count in the object accumulator
15
+ * Intended for use with .reduce()
16
+ * @param acc Accumulator
17
+ * @param value Current collection count object in array
18
+ * @returns Updated accumulator
19
+ */
20
+ export const mergeResults = (acc, value) => {
21
+ acc[value.collection] = value.count;
22
+ return acc;
23
+ };
24
+ /**
25
+ * Get an object of item counts for the given collections
26
+ * @param db Database instance to get counts in
27
+ * @param collections Array of table names to get count from
28
+ */
29
+ export const getItemCount = async (db, collections) => {
30
+ // Counts can be a little heavy if the table is very large, so we'll only ever execute 3 of these
31
+ // queries simultaneously to not overload the database
32
+ const limit = pLimit(3);
33
+ const calls = collections.map((collection) => limit(countCollection, db, collection));
34
+ const results = await Promise.all(calls);
35
+ return results.reduce(mergeResults, {});
36
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Returns randomized value between 0 and 1.8e+6 (30min in ms) intended to be used as the randomized wait for
3
+ * telemetry tracking
4
+ */
5
+ export declare const getRandomWaitTime: () => number;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Returns randomized value between 0 and 1.8e+6 (30min in ms) intended to be used as the randomized wait for
3
+ * telemetry tracking
4
+ */
5
+ export const getRandomWaitTime = () => Math.floor(Math.random() * 1.8e6);
@@ -0,0 +1,7 @@
1
+ import { type Knex } from 'knex';
2
+ export interface UserCount {
3
+ admin: number;
4
+ app: number;
5
+ api: number;
6
+ }
7
+ export declare const getUserCount: (db: Knex) => Promise<UserCount>;
@@ -0,0 +1,30 @@
1
+ import { toBoolean } from '@directus/utils';
2
+ import {} from 'knex';
3
+ export const getUserCount = async (db) => {
4
+ const counts = {
5
+ admin: 0,
6
+ app: 0,
7
+ api: 0,
8
+ };
9
+ const result = (await db
10
+ .count('directus_users.id', { as: 'count' })
11
+ .select('directus_roles.admin_access', 'directus_roles.app_access')
12
+ .from('directus_users')
13
+ .leftJoin('directus_roles', 'directus_users.role', '=', 'directus_roles.id')
14
+ .groupBy('directus_roles.admin_access', 'directus_roles.app_access'));
15
+ for (const record of result) {
16
+ const adminAccess = toBoolean(record.admin_access);
17
+ const appAccess = toBoolean(record.app_access);
18
+ const count = Number(record.count);
19
+ if (adminAccess) {
20
+ counts.admin = count;
21
+ }
22
+ else if (appAccess) {
23
+ counts.app = count;
24
+ }
25
+ else {
26
+ counts.api = count;
27
+ }
28
+ }
29
+ return counts;
30
+ };
@@ -0,0 +1,13 @@
1
+ import { type Knex } from 'knex';
2
+ export interface UserItemCount {
3
+ collections: number;
4
+ items: number;
5
+ }
6
+ /**
7
+ * Sum all passed values together. Meant to be used with .reduce()
8
+ */
9
+ export declare const sum: (acc: number, val: number) => number;
10
+ /**
11
+ * Count all the items in the non-system tables
12
+ */
13
+ export declare const getUserItemCount: (db: Knex) => Promise<UserItemCount>;
@@ -0,0 +1,18 @@
1
+ import {} from 'knex';
2
+ import { getSchema } from '../../utils/get-schema.js';
3
+ import { getItemCount } from './get-item-count.js';
4
+ /**
5
+ * Sum all passed values together. Meant to be used with .reduce()
6
+ */
7
+ export const sum = (acc, val) => (acc += val);
8
+ /**
9
+ * Count all the items in the non-system tables
10
+ */
11
+ export const getUserItemCount = async (db) => {
12
+ const schema = await getSchema({ database: db });
13
+ const userCollections = Object.keys(schema.collections).filter((collection) => collection.startsWith('directus_') === false);
14
+ const counts = await getItemCount(db, userCollections);
15
+ const collections = userCollections.length;
16
+ const items = Object.values(counts).reduce(sum, 0);
17
+ return { collections, items };
18
+ };
@@ -12,6 +12,8 @@ export type TransformationParams = {
12
12
  transforms?: Transformation[];
13
13
  format?: TransformationFormat | 'auto';
14
14
  quality?: number;
15
+ focal_point_x?: number;
16
+ focal_point_y?: number;
15
17
  } & TransformationResize;
16
18
  export type TransformationSet = {
17
19
  transformationParams: TransformationParams;