@directus/api 15.0.0 → 17.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 (185) hide show
  1. package/dist/app.js +6 -4
  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 +9 -2
  5. package/dist/auth/drivers/openid.js +9 -2
  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 +2 -1
  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/load-extensions.js +4 -2
  23. package/dist/cli/utils/create-env/env-stub.liquid +1 -1
  24. package/dist/constants.d.ts +1 -1
  25. package/dist/constants.js +4 -1
  26. package/dist/controllers/assets.js +5 -3
  27. package/dist/controllers/auth.js +5 -4
  28. package/dist/controllers/extensions.js +18 -6
  29. package/dist/controllers/files.js +3 -3
  30. package/dist/controllers/permissions.js +11 -2
  31. package/dist/controllers/schema.js +3 -2
  32. package/dist/controllers/shares.js +3 -3
  33. package/dist/controllers/utils.js +13 -32
  34. package/dist/database/helpers/index.d.ts +1 -1
  35. package/dist/database/index.js +9 -2
  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/emitter.js +3 -1
  47. package/dist/extensions/lib/get-extensions-path.d.ts +1 -1
  48. package/dist/extensions/lib/get-extensions-path.js +2 -1
  49. package/dist/extensions/lib/get-extensions.d.ts +1 -1
  50. package/dist/extensions/lib/get-extensions.js +32 -8
  51. package/dist/extensions/lib/get-shared-deps-mapping.js +6 -4
  52. package/dist/extensions/lib/sandbox/register/call-reference.js +4 -2
  53. package/dist/extensions/lib/sandbox/sdk/generators/log.js +2 -1
  54. package/dist/extensions/lib/sync-extensions.js +6 -4
  55. package/dist/extensions/manager.js +43 -19
  56. package/dist/flows.js +13 -7
  57. package/dist/logger.d.ts +7 -7
  58. package/dist/logger.js +116 -92
  59. package/dist/mailer.js +4 -2
  60. package/dist/middleware/cache.js +4 -2
  61. package/dist/middleware/check-ip.js +25 -6
  62. package/dist/middleware/cors.js +2 -1
  63. package/dist/middleware/error-handler.js +5 -5
  64. package/dist/middleware/rate-limiter-global.js +4 -2
  65. package/dist/middleware/rate-limiter-ip.js +2 -1
  66. package/dist/middleware/respond.js +5 -3
  67. package/dist/operations/log/index.js +2 -1
  68. package/dist/rate-limiter.d.ts +2 -1
  69. package/dist/rate-limiter.js +5 -2
  70. package/dist/redis/index.d.ts +3 -2
  71. package/dist/redis/index.js +3 -2
  72. package/dist/redis/{create-redis.js → lib/create-redis.js} +2 -2
  73. package/dist/redis/utils/redis-config-available.d.ts +4 -0
  74. package/dist/redis/utils/redis-config-available.js +8 -0
  75. package/dist/request/request-interceptor.js +7 -5
  76. package/dist/request/response-interceptor.js +2 -2
  77. package/dist/request/validate-ip.d.ts +1 -1
  78. package/dist/request/validate-ip.js +23 -7
  79. package/dist/server.js +11 -7
  80. package/dist/services/activity.js +5 -4
  81. package/dist/services/assets.d.ts +2 -0
  82. package/dist/services/assets.js +9 -6
  83. package/dist/services/authentication.js +17 -9
  84. package/dist/services/authorization.d.ts +1 -1
  85. package/dist/services/authorization.js +15 -3
  86. package/dist/services/collections.js +5 -4
  87. package/dist/services/extensions.d.ts +15 -9
  88. package/dist/services/extensions.js +74 -39
  89. package/dist/services/fields.js +9 -4
  90. package/dist/services/files.d.ts +2 -2
  91. package/dist/services/files.js +22 -14
  92. package/dist/services/graphql/index.js +46 -3
  93. package/dist/services/graphql/subscription.js +2 -2
  94. package/dist/services/graphql/types/bigint.js +16 -5
  95. package/dist/services/graphql/utils/process-error.d.ts +4 -1
  96. package/dist/services/graphql/utils/process-error.js +10 -8
  97. package/dist/services/{import-export/index.d.ts → import-export.d.ts} +1 -1
  98. package/dist/services/{import-export/index.js → import-export.js} +14 -12
  99. package/dist/services/index.d.ts +1 -1
  100. package/dist/services/index.js +1 -1
  101. package/dist/services/items.js +12 -8
  102. package/dist/services/mail/index.js +4 -2
  103. package/dist/services/notifications.js +7 -3
  104. package/dist/services/permissions.d.ts +3 -2
  105. package/dist/services/permissions.js +76 -1
  106. package/dist/services/relations.js +19 -10
  107. package/dist/services/roles.js +83 -15
  108. package/dist/services/server.js +7 -5
  109. package/dist/services/shares.js +3 -2
  110. package/dist/services/specifications.js +2 -1
  111. package/dist/services/users.js +20 -9
  112. package/dist/services/versions.js +6 -5
  113. package/dist/services/webhooks.d.ts +2 -2
  114. package/dist/services/webhooks.js +2 -2
  115. package/dist/services/websocket.d.ts +1 -1
  116. package/dist/services/websocket.js +4 -3
  117. package/dist/storage/register-drivers.js +2 -1
  118. package/dist/storage/register-locations.js +2 -1
  119. package/dist/synchronization.js +3 -1
  120. package/dist/telemetry/lib/get-report.js +1 -1
  121. package/dist/telemetry/lib/init-telemetry.js +2 -2
  122. package/dist/telemetry/lib/send-report.js +1 -1
  123. package/dist/telemetry/lib/track.js +2 -3
  124. package/dist/telemetry/utils/get-user-count.js +1 -1
  125. package/dist/types/assets.d.ts +2 -0
  126. package/dist/types/items.d.ts +4 -12
  127. package/dist/types/items.js +0 -4
  128. package/dist/utils/apply-diff.js +2 -1
  129. package/dist/utils/apply-query.js +0 -11
  130. package/dist/utils/delete-from-require-cache.js +2 -1
  131. package/dist/utils/get-accountability-for-token.js +3 -2
  132. package/dist/utils/get-auth-providers.js +2 -1
  133. package/dist/utils/get-cache-headers.js +5 -2
  134. package/dist/utils/get-config-from-env.js +2 -1
  135. package/dist/utils/get-default-value.js +4 -3
  136. package/dist/utils/get-ip-from-req.js +4 -2
  137. package/dist/utils/get-permissions.js +5 -3
  138. package/dist/utils/get-schema.js +5 -2
  139. package/dist/utils/get-snapshot-diff.js +7 -9
  140. package/dist/utils/get-snapshot.js +4 -4
  141. package/dist/utils/ip-in-networks.d.ts +6 -0
  142. package/dist/utils/ip-in-networks.js +13 -0
  143. package/dist/utils/is-url-allowed.js +2 -1
  144. package/dist/utils/job-queue.d.ts +1 -0
  145. package/dist/utils/job-queue.js +3 -0
  146. package/dist/utils/sanitize-query.js +7 -2
  147. package/dist/utils/sanitize-schema.d.ts +1 -1
  148. package/dist/utils/should-clear-cache.js +2 -1
  149. package/dist/utils/should-skip-cache.js +2 -1
  150. package/dist/utils/transformations.js +95 -12
  151. package/dist/utils/validate-env.js +4 -2
  152. package/dist/utils/validate-query.js +7 -3
  153. package/dist/utils/validate-storage.js +4 -2
  154. package/dist/webhooks.js +4 -3
  155. package/dist/websocket/controllers/base.js +12 -6
  156. package/dist/websocket/controllers/graphql.js +4 -2
  157. package/dist/websocket/controllers/hooks.js +3 -2
  158. package/dist/websocket/controllers/index.js +4 -2
  159. package/dist/websocket/controllers/rest.js +4 -2
  160. package/dist/websocket/errors.js +2 -1
  161. package/dist/websocket/handlers/heartbeat.js +4 -3
  162. package/dist/websocket/handlers/subscribe.d.ts +2 -2
  163. package/dist/websocket/handlers/subscribe.js +5 -4
  164. package/package.json +57 -57
  165. package/dist/__utils__/items-utils.d.ts +0 -2
  166. package/dist/__utils__/items-utils.js +0 -31
  167. package/dist/__utils__/mock-env.d.ts +0 -18
  168. package/dist/__utils__/mock-env.js +0 -41
  169. package/dist/__utils__/schemas.d.ts +0 -13
  170. package/dist/__utils__/schemas.js +0 -301
  171. package/dist/__utils__/snapshots.d.ts +0 -5
  172. package/dist/__utils__/snapshots.js +0 -903
  173. package/dist/env.d.ts +0 -14
  174. package/dist/env.js +0 -511
  175. package/dist/messenger.d.ts +0 -24
  176. package/dist/messenger.js +0 -64
  177. package/dist/services/import-export/import-worker.d.ts +0 -9
  178. package/dist/services/import-export/import-worker.js +0 -9
  179. package/dist/utils/to-boolean.d.ts +0 -4
  180. package/dist/utils/to-boolean.js +0 -6
  181. package/dist/worker-pool.d.ts +0 -2
  182. package/dist/worker-pool.js +0 -19
  183. /package/dist/redis/{create-redis.d.ts → lib/create-redis.d.ts} +0 -0
  184. /package/dist/redis/{use-redis.d.ts → lib/use-redis.d.ts} +0 -0
  185. /package/dist/redis/{use-redis.js → lib/use-redis.js} +0 -0
@@ -1,50 +1,39 @@
1
- import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
- import { createInspector } from '@directus/schema';
3
- import Joi from 'joi';
1
+ import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
2
+ import { isObject } from '@directus/utils';
4
3
  import { omit, pick } from 'lodash-es';
5
- import { getCache } from '../cache.js';
6
- import { getHelpers } from '../database/helpers/index.js';
7
- import getDatabase, { getSchemaInspector } from '../database/index.js';
4
+ import getDatabase from '../database/index.js';
8
5
  import { getExtensionManager } from '../extensions/index.js';
9
6
  import { ItemsService } from './items.js';
10
- import { PermissionsService } from './permissions.js';
7
+ export class ExtensionReadError extends Error {
8
+ originalError;
9
+ constructor(originalError) {
10
+ super();
11
+ this.originalError = originalError;
12
+ }
13
+ }
11
14
  export class ExtensionsService {
12
15
  knex;
13
- permissionsService;
14
- schemaInspector;
15
16
  accountability;
16
17
  schema;
17
18
  extensionsItemService;
18
- systemCache;
19
- helpers;
20
19
  extensionsManager;
21
20
  constructor(options) {
22
21
  this.knex = options.knex || getDatabase();
23
- this.permissionsService = new PermissionsService(options);
24
- this.schemaInspector = options.knex ? createInspector(options.knex) : getSchemaInspector();
25
22
  this.schema = options.schema;
26
23
  this.accountability = options.accountability || null;
27
24
  this.extensionsManager = getExtensionManager();
28
25
  this.extensionsItemService = new ItemsService('directus_extensions', {
29
26
  knex: this.knex,
30
27
  schema: this.schema,
31
- // No accountability here, as every other method is hardcoded to be admin only
28
+ accountability: this.accountability,
32
29
  });
33
- this.systemCache = getCache().systemCache;
34
- this.helpers = getHelpers(this.knex);
35
30
  }
36
31
  async readAll() {
37
- if (this.accountability?.admin !== true) {
38
- throw new ForbiddenError();
39
- }
40
32
  const installedExtensions = this.extensionsManager.getExtensions();
41
33
  const configuredExtensions = await this.extensionsItemService.readByQuery({ limit: -1 });
42
34
  return this.stitch(installedExtensions, configuredExtensions);
43
35
  }
44
36
  async readOne(bundle, name) {
45
- if (this.accountability?.admin !== true) {
46
- throw new ForbiddenError();
47
- }
48
37
  const key = this.getKey(bundle, name);
49
38
  const schema = this.extensionsManager.getExtensions().find((extension) => extension.name === (bundle ?? name));
50
39
  const meta = await this.extensionsItemService.readOne(key);
@@ -54,27 +43,73 @@ export class ExtensionsService {
54
43
  throw new ForbiddenError();
55
44
  }
56
45
  async updateOne(bundle, name, data) {
57
- if (this.accountability?.admin !== true) {
58
- throw new ForbiddenError();
59
- }
60
- const key = this.getKey(bundle, name);
61
- const updateExtensionSchema = Joi.object({
62
- meta: Joi.object({
63
- enabled: Joi.boolean(),
64
- }),
46
+ const result = await this.knex.transaction(async (trx) => {
47
+ if (!isObject(data.meta)) {
48
+ throw new InvalidPayloadError({ reason: `"meta" is required` });
49
+ }
50
+ const service = new ExtensionsService({
51
+ knex: trx,
52
+ accountability: this.accountability,
53
+ schema: this.schema,
54
+ });
55
+ const key = this.getKey(bundle, name);
56
+ await service.extensionsItemService.updateOne(key, data.meta);
57
+ let extension;
58
+ try {
59
+ extension = await service.readOne(bundle, name);
60
+ }
61
+ catch (error) {
62
+ throw new ExtensionReadError(error);
63
+ }
64
+ if ('enabled' in data.meta) {
65
+ await service.checkBundleAndSyncStatus(trx, extension);
66
+ }
67
+ return extension;
65
68
  });
66
- const { error } = updateExtensionSchema.validate(data);
67
- if (error) {
68
- throw new InvalidPayloadError({ reason: error.message });
69
- }
70
- if ('meta' in data && 'enabled' in data.meta) {
71
- await this.knex('directus_extensions').update({ enabled: data.meta.enabled }).where({ name: key });
72
- this.extensionsManager.reload();
73
- }
69
+ this.extensionsManager.reload();
70
+ return result;
74
71
  }
75
72
  getKey(bundle, name) {
76
73
  return bundle ? `${bundle}/${name}` : name;
77
74
  }
75
+ /**
76
+ * Sync a bundles enabled status
77
+ * - If the extension or extensions parent is not a bundle changes are skipped
78
+ * - If a bundles status is toggled, all children are set to that status
79
+ * - If an entries status is toggled, then if the:
80
+ * - Parent bundle is non-partial throws UnprocessableContentError
81
+ * - Entry status change resulted in all children being disabled then the parent bundle is disabled
82
+ * - Entry status change resulted in at least one child being enabled then the parent bundle is enabled
83
+ */
84
+ async checkBundleAndSyncStatus(trx, extension) {
85
+ if (extension.bundle === null) {
86
+ if (extension.schema?.type === 'bundle') {
87
+ await trx('directus_extensions')
88
+ .update({ enabled: extension.meta.enabled })
89
+ .where('name', 'LIKE', this.getKey(extension.name, '%'));
90
+ }
91
+ return;
92
+ }
93
+ const parent = await this.readOne(null, extension.bundle);
94
+ if (parent.schema?.type !== 'bundle') {
95
+ return;
96
+ }
97
+ if (parent.schema.partial === false) {
98
+ throw new UnprocessableContentError({
99
+ reason: 'Unable to toggle status of an entry for a bundle marked as non partial',
100
+ });
101
+ }
102
+ const child = await trx('directus_extensions')
103
+ .where('name', 'LIKE', this.getKey(extension.bundle, '%'))
104
+ .where({ enabled: true })
105
+ .first();
106
+ if (!child && parent.meta.enabled) {
107
+ await trx('directus_extensions').update({ enabled: false }).where({ name: parent.name });
108
+ }
109
+ else if (child && !parent.meta.enabled) {
110
+ await trx('directus_extensions').update({ enabled: true }).where({ name: parent.name });
111
+ }
112
+ }
78
113
  /**
79
114
  * Combine the settings stored in the database with the information available from the installed
80
115
  * extensions into the standardized extensions api output
@@ -128,7 +163,7 @@ export class ExtensionsService {
128
163
  return {
129
164
  name,
130
165
  bundle: bundleName,
131
- schema: schema ? pick(schema, 'type', 'local', 'version') : null,
166
+ schema: schema ? pick(schema, 'type', 'local', 'version', 'partial') : null,
132
167
  meta: omit(meta, 'name'),
133
168
  };
134
169
  });
@@ -319,14 +319,19 @@ export class FieldsService {
319
319
  }
320
320
  if (hookAdjustedField.schema) {
321
321
  const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field);
322
+ if (hookAdjustedField.schema?.is_nullable === true && existingColumn.is_primary_key) {
323
+ throw new InvalidPayloadError({ reason: 'Primary key cannot be null' });
324
+ }
322
325
  // Sanitize column only when applying snapshot diff as opts is only passed from /utils/apply-diff.ts
323
326
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
324
327
  if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
325
328
  try {
326
- await this.knex.schema.alterTable(collection, (table) => {
327
- if (!hookAdjustedField.schema)
328
- return;
329
- this.addColumnToTable(table, field, existingColumn);
329
+ await this.knex.transaction(async (trx) => {
330
+ await trx.schema.alterTable(collection, async (table) => {
331
+ if (!hookAdjustedField.schema)
332
+ return;
333
+ this.addColumnToTable(table, field, existingColumn);
334
+ });
330
335
  });
331
336
  }
332
337
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- import type { File, BusboyFileStream } from '@directus/types';
2
+ import type { BusboyFileStream, File } from '@directus/types';
3
3
  import type { Readable } from 'node:stream';
4
4
  import type { AbstractServiceOptions, MutationOptions, PrimaryKey } from '../types/index.js';
5
5
  import { ItemsService } from './items.js';
@@ -15,7 +15,7 @@ export declare class FilesService extends ItemsService {
15
15
  /**
16
16
  * Extract metadata from a buffer's content
17
17
  */
18
- getMetadata(stream: Readable, allowList?: any): Promise<Metadata>;
18
+ getMetadata(stream: Readable, allowList?: string | string[]): Promise<Metadata>;
19
19
  /**
20
20
  * Import a single file from an external URL
21
21
  */
@@ -1,7 +1,9 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ContentTooLargeError, ForbiddenError, InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
1
3
  import formatTitle from '@directus/format-title';
2
4
  import { toArray } from '@directus/utils';
3
5
  import encodeURL from 'encodeurl';
4
- import exif from 'exif-reader';
6
+ import exif, {} from 'exif-reader';
5
7
  import { parse as parseIcc } from 'icc';
6
8
  import { clone, pick } from 'lodash-es';
7
9
  import { extension } from 'mime-types';
@@ -13,13 +15,13 @@ import sharp from 'sharp';
13
15
  import url from 'url';
14
16
  import { SUPPORTED_IMAGE_METADATA_FORMATS } from '../constants.js';
15
17
  import emitter from '../emitter.js';
16
- import env from '../env.js';
17
- import { ContentTooLargeError, ForbiddenError, InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
18
- import logger from '../logger.js';
18
+ import { useLogger } from '../logger.js';
19
19
  import { getAxios } from '../request/index.js';
20
20
  import { getStorage } from '../storage/index.js';
21
21
  import { parseIptc, parseXmp } from '../utils/parse-image-metadata.js';
22
22
  import { ItemsService } from './items.js';
23
+ const env = useEnv();
24
+ const logger = useLogger();
23
25
  export class FilesService extends ItemsService {
24
26
  constructor(options) {
25
27
  super('directus_files', options);
@@ -138,7 +140,8 @@ export class FilesService extends ItemsService {
138
140
  if (!payload.metadata && metadata) {
139
141
  payload.metadata = metadata;
140
142
  }
141
- // Note that if this is a replace file upload, the below properities are fetched and included in the payload above in the `existingFile` variable...so this will ONLY set the values if they're not already set
143
+ // Note that if this is a replace file upload, the below properties are fetched and included in the payload above
144
+ // in the `existingFile` variable... so this will ONLY set the values if they're not already set
142
145
  if (!payload.description && description) {
143
146
  payload.description = description;
144
147
  }
@@ -192,20 +195,25 @@ export class FilesService extends ItemsService {
192
195
  const fullMetadata = {};
193
196
  if (sharpMetadata.exif) {
194
197
  try {
195
- const { image, thumbnail, interoperability, ...rest } = exif(sharpMetadata.exif);
196
- if (image) {
197
- fullMetadata.ifd0 = image;
198
+ const { Image, ThumbnailTags, Iop, GPSInfo, Photo } = exif(sharpMetadata.exif);
199
+ if (Image) {
200
+ fullMetadata.ifd0 = Image;
201
+ }
202
+ if (ThumbnailTags) {
203
+ fullMetadata.ifd1 = ThumbnailTags;
204
+ }
205
+ if (Iop) {
206
+ fullMetadata.interop = Iop;
198
207
  }
199
- if (thumbnail) {
200
- fullMetadata.ifd1 = thumbnail;
208
+ if (GPSInfo) {
209
+ fullMetadata.gps = GPSInfo;
201
210
  }
202
- if (interoperability) {
203
- fullMetadata.interop = interoperability;
211
+ if (Photo) {
212
+ fullMetadata.exif = Photo;
204
213
  }
205
- Object.assign(fullMetadata, rest);
206
214
  }
207
215
  catch (err) {
208
- logger.warn(`Couldn't extract EXIF metadata from file`);
216
+ logger.warn(`Couldn't extract Exif metadata from file`);
209
217
  logger.warn(err);
210
218
  }
211
219
  }
@@ -1,6 +1,7 @@
1
1
  import { Action, FUNCTIONS } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
2
3
  import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
- import { parseFilterFunctionPath } from '@directus/utils';
4
+ import { parseFilterFunctionPath, toBoolean } from '@directus/utils';
4
5
  import argon2 from 'argon2';
5
6
  import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
6
7
  import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
@@ -8,14 +9,12 @@ import { assign, flatten, get, mapKeys, merge, omit, pick, set, transform, uniq
8
9
  import { clearSystemCache, getCache } from '../../cache.js';
9
10
  import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
10
11
  import getDatabase from '../../database/index.js';
11
- import env from '../../env.js';
12
12
  import { generateHash } from '../../utils/generate-hash.js';
13
13
  import { getGraphQLType } from '../../utils/get-graphql-type.js';
14
14
  import { getMilliseconds } from '../../utils/get-milliseconds.js';
15
15
  import { getService } from '../../utils/get-service.js';
16
16
  import { reduceSchema } from '../../utils/reduce-schema.js';
17
17
  import { sanitizeQuery } from '../../utils/sanitize-query.js';
18
- import { toBoolean } from '../../utils/to-boolean.js';
19
18
  import { validateQuery } from '../../utils/validate-query.js';
20
19
  import { ActivityService } from '../activity.js';
21
20
  import { AuthenticationService } from '../authentication.js';
@@ -41,6 +40,7 @@ import { GraphQLStringOrFloat } from './types/string-or-float.js';
41
40
  import { GraphQLVoid } from './types/void.js';
42
41
  import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
43
42
  import processError from './utils/process-error.js';
43
+ const env = useEnv();
44
44
  const validationRules = Array.from(specifiedRules);
45
45
  if (env['GRAPHQL_INTROSPECTION'] === false) {
46
46
  validationRules.push(NoSchemaIntrospectionCustomRule);
@@ -595,6 +595,47 @@ export class GraphQLService {
595
595
  },
596
596
  },
597
597
  });
598
+ const BigIntFilterOperators = schemaComposer.createInputTC({
599
+ name: 'big_int_filter_operators',
600
+ fields: {
601
+ _eq: {
602
+ type: GraphQLBigInt,
603
+ },
604
+ _neq: {
605
+ type: GraphQLBigInt,
606
+ },
607
+ _in: {
608
+ type: new GraphQLList(GraphQLBigInt),
609
+ },
610
+ _nin: {
611
+ type: new GraphQLList(GraphQLBigInt),
612
+ },
613
+ _gt: {
614
+ type: GraphQLBigInt,
615
+ },
616
+ _gte: {
617
+ type: GraphQLBigInt,
618
+ },
619
+ _lt: {
620
+ type: GraphQLBigInt,
621
+ },
622
+ _lte: {
623
+ type: GraphQLBigInt,
624
+ },
625
+ _null: {
626
+ type: GraphQLBoolean,
627
+ },
628
+ _nnull: {
629
+ type: GraphQLBoolean,
630
+ },
631
+ _between: {
632
+ type: new GraphQLList(GraphQLBigInt),
633
+ },
634
+ _nbetween: {
635
+ type: new GraphQLList(GraphQLBigInt),
636
+ },
637
+ },
638
+ });
598
639
  const GeometryFilterOperators = schemaComposer.createInputTC({
599
640
  name: 'geometry_filter_operators',
600
641
  fields: {
@@ -705,6 +746,8 @@ export class GraphQLService {
705
746
  filterOperatorType = BooleanFilterOperators;
706
747
  break;
707
748
  case GraphQLBigInt:
749
+ filterOperatorType = BigIntFilterOperators;
750
+ break;
708
751
  case GraphQLInt:
709
752
  case GraphQLFloat:
710
753
  filterOperatorType = NumberFilterOperators;
@@ -1,11 +1,11 @@
1
1
  import { EventEmitter, on } from 'events';
2
- import { getMessenger } from '../../messenger.js';
2
+ import { useBus } from '../../bus/index.js';
3
3
  import { getSchema } from '../../utils/get-schema.js';
4
4
  import { refreshAccountability } from '../../websocket/authenticate.js';
5
5
  import { getPayload } from '../../websocket/utils/items.js';
6
6
  const messages = createPubSub(new EventEmitter());
7
7
  export function bindPubSub() {
8
- const messenger = getMessenger();
8
+ const messenger = useBus();
9
9
  messenger.subscribe('websocket.event', (message) => {
10
10
  messages.publish(`${message['collection']}_mutated`, message);
11
11
  });
@@ -1,4 +1,7 @@
1
1
  import { GraphQLScalarType, Kind } from 'graphql';
2
+ // minimum and maximum int64 values database vendors use for big integer
3
+ const MIN_BIG_INT = -9223372036854775808n;
4
+ const MAX_BIG_INT = 9223372036854775807n;
2
5
  export const GraphQLBigInt = new GraphQLScalarType({
3
6
  name: 'GraphQLBigInt',
4
7
  description: 'BigInt value',
@@ -26,11 +29,19 @@ export const GraphQLBigInt = new GraphQLScalarType({
26
29
  },
27
30
  });
28
31
  function parseNumberValue(input) {
29
- if (!/[+-]?([0-9]+[.])?[0-9]+/.test(input))
30
- return input;
31
- const value = parseInt(input);
32
- if (isNaN(value) || value < Number.MIN_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {
32
+ // Attempt to parse the input as a regular integer
33
+ const intValue = Number(input);
34
+ if (isNaN(intValue)) {
33
35
  throw new Error('Invalid GraphQLBigInt');
34
36
  }
35
- return value;
37
+ if (!Number.isSafeInteger(intValue)) {
38
+ // If the input is not a safe integer, its a big int, so return it as string,
39
+ // because currently string is the best way to handle big int due to knex limitations and JSON.stringify not able to serialise bigInt
40
+ const bigIntInput = BigInt(input);
41
+ if (bigIntInput < MIN_BIG_INT || bigIntInput > MAX_BIG_INT) {
42
+ throw new Error('Invalid GraphQLBigInt');
43
+ }
44
+ return input;
45
+ }
46
+ return intValue;
36
47
  }
@@ -1,4 +1,7 @@
1
+ import { type DirectusError } from '@directus/errors';
1
2
  import type { Accountability } from '@directus/types';
2
3
  import type { GraphQLError, GraphQLFormattedError } from 'graphql';
3
- declare const processError: (accountability: Accountability | null, error: Readonly<GraphQLError>) => GraphQLFormattedError;
4
+ declare const processError: (accountability: Accountability | null, error: Readonly<GraphQLError & {
5
+ originalError: GraphQLError | DirectusError | Error | undefined;
6
+ }>) => GraphQLFormattedError;
4
7
  export default processError;
@@ -1,8 +1,12 @@
1
1
  import { isDirectusError } from '@directus/errors';
2
- import logger from '../../../logger.js';
2
+ import { useLogger } from '../../../logger.js';
3
3
  const processError = (accountability, error) => {
4
+ const logger = useLogger();
4
5
  logger.error(error);
5
- const { originalError } = error;
6
+ let originalError = error.originalError;
7
+ if (originalError && 'originalError' in originalError) {
8
+ originalError = originalError.originalError;
9
+ }
6
10
  if (isDirectusError(originalError)) {
7
11
  return {
8
12
  message: originalError.message,
@@ -10,6 +14,8 @@ const processError = (accountability, error) => {
10
14
  code: originalError.code,
11
15
  ...(originalError.extensions ?? {}),
12
16
  },
17
+ ...(error.locations && { locations: error.locations }),
18
+ ...(error.path && { path: error.path }),
13
19
  };
14
20
  }
15
21
  else {
@@ -19,13 +25,9 @@ const processError = (accountability, error) => {
19
25
  extensions: {
20
26
  code: 'INTERNAL_SERVER_ERROR',
21
27
  },
28
+ ...(error.locations && { locations: error.locations }),
29
+ ...(error.path && { path: error.path }),
22
30
  };
23
- if (error.locations) {
24
- graphqlFormattedError.locations = error.locations;
25
- }
26
- if (error.path) {
27
- graphqlFormattedError.path = error.path;
28
- }
29
31
  return graphqlFormattedError;
30
32
  }
31
33
  else {
@@ -2,7 +2,7 @@
2
2
  import type { Accountability, File, Query, SchemaOverview } from '@directus/types';
3
3
  import type { Knex } from 'knex';
4
4
  import type { Readable } from 'node:stream';
5
- import type { AbstractServiceOptions } from '../../types/index.js';
5
+ import type { AbstractServiceOptions } from '../types/index.js';
6
6
  type ExportFormat = 'csv' | 'json' | 'xml' | 'yaml';
7
7
  export declare class ImportService {
8
8
  knex: Knex;
@@ -1,3 +1,5 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
1
3
  import { parseJSON, toArray } from '@directus/utils';
2
4
  import { queue } from 'async';
3
5
  import destroyStream from 'destroy';
@@ -8,18 +10,18 @@ import { createReadStream } from 'node:fs';
8
10
  import { appendFile } from 'node:fs/promises';
9
11
  import Papa from 'papaparse';
10
12
  import StreamArray from 'stream-json/streamers/StreamArray.js';
11
- import getDatabase from '../../database/index.js';
12
- import emitter from '../../emitter.js';
13
- import env from '../../env.js';
14
- import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
15
- import logger from '../../logger.js';
16
- import { getDateFormatted } from '../../utils/get-date-formatted.js';
17
- import { Url } from '../../utils/url.js';
18
- import { userName } from '../../utils/user-name.js';
19
- import { FilesService } from '../files.js';
20
- import { ItemsService } from '../items.js';
21
- import { NotificationsService } from '../notifications.js';
22
- import { UsersService } from '../users.js';
13
+ import getDatabase from '../database/index.js';
14
+ import emitter from '../emitter.js';
15
+ import { useLogger } from '../logger.js';
16
+ import { getDateFormatted } from '../utils/get-date-formatted.js';
17
+ import { Url } from '../utils/url.js';
18
+ import { userName } from '../utils/user-name.js';
19
+ import { FilesService } from './files.js';
20
+ import { ItemsService } from './items.js';
21
+ import { NotificationsService } from './notifications.js';
22
+ import { UsersService } from './users.js';
23
+ const env = useEnv();
24
+ const logger = useLogger();
23
25
  export class ImportService {
24
26
  knex;
25
27
  accountability;
@@ -10,7 +10,7 @@ export * from './files.js';
10
10
  export * from './flows.js';
11
11
  export * from './folders.js';
12
12
  export * from './graphql/index.js';
13
- export * from './import-export/index.js';
13
+ export * from './import-export.js';
14
14
  export * from './items.js';
15
15
  export * from './mail/index.js';
16
16
  export * from './meta.js';
@@ -10,7 +10,7 @@ export * from './files.js';
10
10
  export * from './flows.js';
11
11
  export * from './folders.js';
12
12
  export * from './graphql/index.js';
13
- export * from './import-export/index.js';
13
+ export * from './import-export.js';
14
14
  export * from './items.js';
15
15
  export * from './mail/index.js';
16
16
  export * from './meta.js';
@@ -1,19 +1,19 @@
1
1
  import { Action } from '@directus/constants';
2
+ import { useEnv } from '@directus/env';
3
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
4
  import { assign, clone, cloneDeep, omit, pick, without } from 'lodash-es';
3
5
  import { getCache } from '../cache.js';
6
+ import { translateDatabaseError } from '../database/errors/translate.js';
4
7
  import { getHelpers } from '../database/helpers/index.js';
5
8
  import getDatabase from '../database/index.js';
6
9
  import runAST from '../database/run-ast.js';
7
10
  import emitter from '../emitter.js';
8
- import env from '../env.js';
9
- import { ForbiddenError } from '@directus/errors';
10
- import { translateDatabaseError } from '../database/errors/translate.js';
11
- import { InvalidPayloadError } from '@directus/errors';
12
11
  import getASTFromQuery from '../utils/get-ast-from-query.js';
13
12
  import { shouldClearCache } from '../utils/should-clear-cache.js';
14
13
  import { validateKeys } from '../utils/validate-keys.js';
15
14
  import { AuthorizationService } from './authorization.js';
16
15
  import { PayloadService } from './payload.js';
16
+ const env = useEnv();
17
17
  export class ItemsService {
18
18
  collection;
19
19
  knex;
@@ -118,15 +118,19 @@ export class ItemsService {
118
118
  const payloadWithTypeCasting = await payloadService.processValues('create', payloadWithoutAliases);
119
119
  // The primary key can already exist in the payload.
120
120
  // In case of manual string / UUID primary keys it's always provided at this point.
121
- // In case of an integer primary key, it might be provided as the user can specify the value manually.
121
+ // In case of an (big) integer primary key, it might be provided as the user can specify the value manually.
122
122
  let primaryKey = payloadWithTypeCasting[primaryKeyField];
123
+ if (primaryKey) {
124
+ validateKeys(this.schema, this.collection, primaryKeyField, primaryKey);
125
+ }
123
126
  // If a PK of type number was provided, although the PK is set the auto_increment,
124
127
  // depending on the database, the sequence might need to be reset to protect future PK collisions.
125
128
  let autoIncrementSequenceNeedsToBeReset = false;
126
129
  const pkField = this.schema.collections[this.collection].fields[primaryKeyField];
127
130
  if (primaryKey &&
131
+ pkField &&
128
132
  !opts.bypassAutoIncrementSequenceReset &&
129
- pkField.type === 'integer' &&
133
+ ['integer', 'bigInteger'].includes(pkField.type) &&
130
134
  pkField.defaultValue === 'AUTO_INCREMENT') {
131
135
  autoIncrementSequenceNeedsToBeReset = true;
132
136
  }
@@ -505,7 +509,7 @@ export class ItemsService {
505
509
  nestedActionEvents.push(...nestedActionEventsM2O);
506
510
  nestedActionEvents.push(...nestedActionEventsA2O);
507
511
  for (const key of keys) {
508
- const { revisions, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payload, key, opts);
512
+ const { revisions, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payloadWithA2O, key, opts);
509
513
  childrenRevisions.push(...revisions);
510
514
  nestedActionEvents.push(...nestedActionEventsO2M);
511
515
  }
@@ -569,7 +573,7 @@ export class ItemsService {
569
573
  ? ['items.update', `${this.collection}.items.update`]
570
574
  : `${this.eventScope}.update`,
571
575
  meta: {
572
- payload,
576
+ payload: payloadWithPresets,
573
577
  keys,
574
578
  collection: this.collection,
575
579
  },
@@ -1,14 +1,16 @@
1
+ import { useEnv } from '@directus/env';
1
2
  import { InvalidPayloadError } from '@directus/errors';
2
3
  import fse from 'fs-extra';
3
4
  import { Liquid } from 'liquidjs';
4
5
  import path from 'path';
5
6
  import { fileURLToPath } from 'url';
6
7
  import getDatabase from '../../database/index.js';
7
- import env from '../../env.js';
8
8
  import { getExtensionsPath } from '../../extensions/lib/get-extensions-path.js';
9
- import logger from '../../logger.js';
9
+ import { useLogger } from '../../logger.js';
10
10
  import getMailer from '../../mailer.js';
11
11
  import { Url } from '../../utils/url.js';
12
+ const env = useEnv();
13
+ const logger = useLogger();
12
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
15
  const liquidEngine = new Liquid({
14
16
  root: [path.resolve(getExtensionsPath(), 'templates'), path.resolve(__dirname, 'templates')],
@@ -1,10 +1,12 @@
1
- import env from '../env.js';
2
- import logger from '../logger.js';
1
+ import { useEnv } from '@directus/env';
2
+ import { useLogger } from '../logger.js';
3
3
  import { md } from '../utils/md.js';
4
4
  import { Url } from '../utils/url.js';
5
5
  import { ItemsService } from './items.js';
6
6
  import { MailService } from './mail/index.js';
7
7
  import { UsersService } from './users.js';
8
+ const env = useEnv();
9
+ const logger = useLogger();
8
10
  export class NotificationsService extends ItemsService {
9
11
  usersService;
10
12
  mailService;
@@ -30,7 +32,9 @@ export class NotificationsService extends ItemsService {
30
32
  const user = await this.usersService.readOne(data.recipient, {
31
33
  fields: ['id', 'email', 'email_notifications', 'role.app_access'],
32
34
  });
33
- const manageUserAccountUrl = new Url(env['PUBLIC_URL']).addPath('admin', 'users', user['id']).toString();
35
+ const manageUserAccountUrl = new Url(env['PUBLIC_URL'])
36
+ .addPath('admin', 'users', user['id'])
37
+ .toString();
34
38
  const html = data.message ? md(data.message) : '';
35
39
  if (user['email'] && user['email_notifications'] === true) {
36
40
  try {