@directus/api 15.0.0 → 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 (169) 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/schema.js +3 -2
  31. package/dist/controllers/shares.js +3 -3
  32. package/dist/database/helpers/index.d.ts +1 -1
  33. package/dist/database/index.js +9 -2
  34. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +3 -1
  35. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -1
  36. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  37. package/dist/database/migrations/20230721A-require-shares-fields.js +2 -1
  38. package/dist/database/migrations/20231215A-add-focalpoints.d.ts +3 -0
  39. package/dist/database/migrations/20231215A-add-focalpoints.js +12 -0
  40. package/dist/database/migrations/run.js +2 -1
  41. package/dist/database/run-ast.js +5 -2
  42. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -7
  43. package/dist/database/system-data/fields/files.yaml +16 -0
  44. package/dist/emitter.js +3 -1
  45. package/dist/extensions/lib/get-extensions-path.d.ts +1 -1
  46. package/dist/extensions/lib/get-extensions-path.js +2 -1
  47. package/dist/extensions/lib/get-extensions.d.ts +1 -1
  48. package/dist/extensions/lib/get-extensions.js +32 -8
  49. package/dist/extensions/lib/get-shared-deps-mapping.js +6 -4
  50. package/dist/extensions/lib/sandbox/register/call-reference.js +4 -2
  51. package/dist/extensions/lib/sandbox/sdk/generators/log.js +2 -1
  52. package/dist/extensions/lib/sync-extensions.js +6 -4
  53. package/dist/extensions/manager.js +43 -19
  54. package/dist/flows.js +13 -7
  55. package/dist/logger.d.ts +7 -7
  56. package/dist/logger.js +116 -92
  57. package/dist/mailer.js +4 -2
  58. package/dist/middleware/cache.js +4 -2
  59. package/dist/middleware/check-ip.js +25 -6
  60. package/dist/middleware/cors.js +2 -1
  61. package/dist/middleware/error-handler.js +5 -5
  62. package/dist/middleware/rate-limiter-global.js +4 -2
  63. package/dist/middleware/rate-limiter-ip.js +2 -1
  64. package/dist/middleware/respond.js +4 -2
  65. package/dist/operations/log/index.js +2 -1
  66. package/dist/rate-limiter.d.ts +2 -1
  67. package/dist/rate-limiter.js +5 -2
  68. package/dist/redis/index.d.ts +3 -2
  69. package/dist/redis/index.js +3 -2
  70. package/dist/redis/{create-redis.js → lib/create-redis.js} +2 -2
  71. package/dist/redis/utils/redis-config-available.d.ts +4 -0
  72. package/dist/redis/utils/redis-config-available.js +8 -0
  73. package/dist/request/request-interceptor.js +7 -5
  74. package/dist/request/response-interceptor.js +2 -2
  75. package/dist/request/validate-ip.d.ts +1 -1
  76. package/dist/request/validate-ip.js +23 -7
  77. package/dist/server.js +11 -7
  78. package/dist/services/activity.js +5 -4
  79. package/dist/services/assets.d.ts +2 -0
  80. package/dist/services/assets.js +9 -4
  81. package/dist/services/authentication.js +17 -9
  82. package/dist/services/collections.js +5 -4
  83. package/dist/services/extensions.d.ts +15 -9
  84. package/dist/services/extensions.js +74 -39
  85. package/dist/services/fields.js +9 -4
  86. package/dist/services/files.d.ts +2 -2
  87. package/dist/services/files.js +22 -14
  88. package/dist/services/graphql/index.js +46 -3
  89. package/dist/services/graphql/subscription.js +2 -2
  90. package/dist/services/graphql/types/bigint.js +16 -5
  91. package/dist/services/graphql/utils/process-error.d.ts +4 -1
  92. package/dist/services/graphql/utils/process-error.js +10 -8
  93. package/dist/services/import-export/index.js +5 -3
  94. package/dist/services/items.js +12 -8
  95. package/dist/services/mail/index.js +4 -2
  96. package/dist/services/notifications.js +7 -3
  97. package/dist/services/relations.js +19 -10
  98. package/dist/services/server.js +5 -4
  99. package/dist/services/shares.js +3 -2
  100. package/dist/services/specifications.js +2 -1
  101. package/dist/services/users.js +20 -9
  102. package/dist/services/versions.js +6 -5
  103. package/dist/services/webhooks.d.ts +2 -2
  104. package/dist/services/webhooks.js +2 -2
  105. package/dist/services/websocket.d.ts +1 -1
  106. package/dist/services/websocket.js +4 -3
  107. package/dist/storage/register-drivers.js +2 -1
  108. package/dist/storage/register-locations.js +2 -1
  109. package/dist/synchronization.js +3 -1
  110. package/dist/telemetry/lib/get-report.js +1 -1
  111. package/dist/telemetry/lib/init-telemetry.js +2 -2
  112. package/dist/telemetry/lib/send-report.js +1 -1
  113. package/dist/telemetry/lib/track.js +2 -3
  114. package/dist/telemetry/utils/get-user-count.js +1 -1
  115. package/dist/types/assets.d.ts +2 -0
  116. package/dist/utils/apply-diff.js +2 -1
  117. package/dist/utils/apply-query.js +0 -11
  118. package/dist/utils/delete-from-require-cache.js +2 -1
  119. package/dist/utils/get-accountability-for-token.js +3 -2
  120. package/dist/utils/get-auth-providers.js +2 -1
  121. package/dist/utils/get-cache-headers.js +5 -2
  122. package/dist/utils/get-config-from-env.js +2 -1
  123. package/dist/utils/get-default-value.js +4 -3
  124. package/dist/utils/get-ip-from-req.js +4 -2
  125. package/dist/utils/get-permissions.js +5 -3
  126. package/dist/utils/get-schema.js +5 -2
  127. package/dist/utils/get-snapshot-diff.js +7 -9
  128. package/dist/utils/get-snapshot.js +4 -4
  129. package/dist/utils/ip-in-networks.d.ts +6 -0
  130. package/dist/utils/ip-in-networks.js +13 -0
  131. package/dist/utils/is-url-allowed.js +2 -1
  132. package/dist/utils/job-queue.d.ts +1 -0
  133. package/dist/utils/job-queue.js +3 -0
  134. package/dist/utils/sanitize-query.js +7 -2
  135. package/dist/utils/sanitize-schema.d.ts +1 -1
  136. package/dist/utils/should-clear-cache.js +2 -1
  137. package/dist/utils/should-skip-cache.js +2 -1
  138. package/dist/utils/transformations.js +95 -12
  139. package/dist/utils/validate-env.js +4 -2
  140. package/dist/utils/validate-query.js +7 -3
  141. package/dist/utils/validate-storage.js +4 -2
  142. package/dist/webhooks.js +4 -3
  143. package/dist/websocket/controllers/base.js +12 -6
  144. package/dist/websocket/controllers/graphql.js +4 -2
  145. package/dist/websocket/controllers/hooks.js +3 -2
  146. package/dist/websocket/controllers/index.js +4 -2
  147. package/dist/websocket/controllers/rest.js +4 -2
  148. package/dist/websocket/errors.js +2 -1
  149. package/dist/websocket/handlers/heartbeat.js +4 -3
  150. package/dist/websocket/handlers/subscribe.d.ts +2 -2
  151. package/dist/websocket/handlers/subscribe.js +5 -4
  152. package/package.json +57 -57
  153. package/dist/__utils__/items-utils.d.ts +0 -2
  154. package/dist/__utils__/items-utils.js +0 -31
  155. package/dist/__utils__/mock-env.d.ts +0 -18
  156. package/dist/__utils__/mock-env.js +0 -41
  157. package/dist/__utils__/schemas.d.ts +0 -13
  158. package/dist/__utils__/schemas.js +0 -301
  159. package/dist/__utils__/snapshots.d.ts +0 -5
  160. package/dist/__utils__/snapshots.js +0 -903
  161. package/dist/env.d.ts +0 -14
  162. package/dist/env.js +0 -511
  163. package/dist/messenger.d.ts +0 -24
  164. package/dist/messenger.js +0 -64
  165. package/dist/utils/to-boolean.d.ts +0 -4
  166. package/dist/utils/to-boolean.js +0 -6
  167. /package/dist/redis/{create-redis.d.ts → lib/create-redis.d.ts} +0 -0
  168. /package/dist/redis/{use-redis.d.ts → lib/use-redis.d.ts} +0 -0
  169. /package/dist/redis/{use-redis.js → lib/use-redis.js} +0 -0
@@ -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 {
@@ -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';
@@ -10,9 +12,7 @@ import Papa from 'papaparse';
10
12
  import StreamArray from 'stream-json/streamers/StreamArray.js';
11
13
  import getDatabase from '../../database/index.js';
12
14
  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';
15
+ import { useLogger } from '../../logger.js';
16
16
  import { getDateFormatted } from '../../utils/get-date-formatted.js';
17
17
  import { Url } from '../../utils/url.js';
18
18
  import { userName } from '../../utils/user-name.js';
@@ -20,6 +20,8 @@ import { FilesService } from '../files.js';
20
20
  import { ItemsService } from '../items.js';
21
21
  import { NotificationsService } from '../notifications.js';
22
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;
@@ -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 {
@@ -1,3 +1,4 @@
1
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
1
2
  import { createInspector } from '@directus/schema';
2
3
  import { toArray } from '@directus/utils';
3
4
  import { clearSystemCache, getCache } from '../cache.js';
@@ -5,7 +6,6 @@ import { getHelpers } from '../database/helpers/index.js';
5
6
  import getDatabase, { getSchemaInspector } from '../database/index.js';
6
7
  import { systemRelationRows } from '../database/system-data/relations/index.js';
7
8
  import emitter from '../emitter.js';
8
- import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
9
9
  import { getDefaultIndexName } from '../utils/get-default-index-name.js';
10
10
  import { getSchema } from '../utils/get-schema.js';
11
11
  import { ItemsService } from './items.js';
@@ -115,16 +115,18 @@ export class RelationsService {
115
115
  if (!relation.field) {
116
116
  throw new InvalidPayloadError({ reason: '"field" is required' });
117
117
  }
118
- if (relation.collection in this.schema.collections === false) {
118
+ const collectionSchema = this.schema.collections[relation.collection];
119
+ if (!collectionSchema) {
119
120
  throw new InvalidPayloadError({ reason: `Collection "${relation.collection}" doesn't exist` });
120
121
  }
121
- if (relation.field in this.schema.collections[relation.collection].fields === false) {
122
+ const fieldSchema = collectionSchema.fields[relation.field];
123
+ if (!fieldSchema) {
122
124
  throw new InvalidPayloadError({
123
125
  reason: `Field "${relation.field}" doesn't exist in collection "${relation.collection}"`,
124
126
  });
125
127
  }
126
128
  // A primary key should not be a foreign key
127
- if (this.schema.collections[relation.collection].primary === relation.field) {
129
+ if (collectionSchema.primary === relation.field) {
128
130
  throw new InvalidPayloadError({
129
131
  reason: `Field "${relation.field}" in collection "${relation.collection}" is a primary key`,
130
132
  });
@@ -151,7 +153,7 @@ export class RelationsService {
151
153
  await this.knex.transaction(async (trx) => {
152
154
  if (relation.related_collection) {
153
155
  await trx.schema.alterTable(relation.collection, async (table) => {
154
- this.alterType(table, relation);
156
+ this.alterType(table, relation, fieldSchema.nullable);
155
157
  const constraintName = getDefaultIndexName('foreign', relation.collection, relation.field);
156
158
  const builder = table
157
159
  .foreign(relation.field, constraintName)
@@ -198,10 +200,12 @@ export class RelationsService {
198
200
  if (this.accountability && this.accountability.admin !== true) {
199
201
  throw new ForbiddenError();
200
202
  }
201
- if (collection in this.schema.collections === false) {
203
+ const collectionSchema = this.schema.collections[collection];
204
+ if (!collectionSchema) {
202
205
  throw new InvalidPayloadError({ reason: `Collection "${collection}" doesn't exist` });
203
206
  }
204
- if (field in this.schema.collections[collection].fields === false) {
207
+ const fieldSchema = collectionSchema.fields[field];
208
+ if (!fieldSchema) {
205
209
  throw new InvalidPayloadError({ reason: `Field "${field}" doesn't exist in collection "${collection}"` });
206
210
  }
207
211
  const existingRelation = this.schema.relations.find((existingRelation) => existingRelation.collection === collection && existingRelation.field === field);
@@ -225,7 +229,7 @@ export class RelationsService {
225
229
  constraintName = this.helpers.schema.constraintName(constraintName);
226
230
  existingRelation.schema.constraint_name = constraintName;
227
231
  }
228
- this.alterType(table, relation);
232
+ this.alterType(table, relation, fieldSchema.nullable);
229
233
  const builder = table
230
234
  .foreign(field, constraintName || undefined)
231
235
  .references(`${existingRelation.related_collection}.${this.schema.collections[existingRelation.related_collection].primary}`);
@@ -447,11 +451,16 @@ export class RelationsService {
447
451
  *
448
452
  * @TODO This is a bit of a hack, and might be better of abstracted elsewhere
449
453
  */
450
- alterType(table, relation) {
454
+ alterType(table, relation, nullable) {
451
455
  const m2oFieldDBType = this.schema.collections[relation.collection].fields[relation.field].dbType;
452
456
  const relatedFieldDBType = this.schema.collections[relation.related_collection].fields[this.schema.collections[relation.related_collection].primary].dbType;
453
457
  if (m2oFieldDBType !== relatedFieldDBType && m2oFieldDBType === 'int' && relatedFieldDBType === 'int unsigned') {
454
- table.specificType(relation.field, 'int unsigned').alter();
458
+ const alterField = table.specificType(relation.field, 'int unsigned');
459
+ // Maintains the non-nullable state
460
+ if (!nullable) {
461
+ alterField.notNullable();
462
+ }
463
+ alterField.alter();
455
464
  }
456
465
  }
457
466
  }
@@ -1,19 +1,20 @@
1
- import { toArray } from '@directus/utils';
1
+ import { useEnv } from '@directus/env';
2
+ import { toArray, toBoolean } from '@directus/utils';
2
3
  import { version } from 'directus/version';
3
4
  import { merge } from 'lodash-es';
4
5
  import { Readable } from 'node:stream';
5
6
  import { performance } from 'perf_hooks';
6
7
  import { getCache } from '../cache.js';
7
8
  import getDatabase, { hasDatabaseConnection } from '../database/index.js';
8
- import env from '../env.js';
9
- import logger from '../logger.js';
9
+ import { useLogger } from '../logger.js';
10
10
  import getMailer from '../mailer.js';
11
11
  import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
12
12
  import { rateLimiter } from '../middleware/rate-limiter-ip.js';
13
13
  import { SERVER_ONLINE } from '../server.js';
14
14
  import { getStorage } from '../storage/index.js';
15
- import { toBoolean } from '../utils/to-boolean.js';
16
15
  import { SettingsService } from './settings.js';
16
+ const env = useEnv();
17
+ const logger = useLogger();
17
18
  export class ServerService {
18
19
  knex;
19
20
  accountability;
@@ -1,7 +1,7 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
1
3
  import argon2 from 'argon2';
2
4
  import jwt from 'jsonwebtoken';
3
- import env from '../env.js';
4
- import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
5
5
  import { getMilliseconds } from '../utils/get-milliseconds.js';
6
6
  import { md } from '../utils/md.js';
7
7
  import { Url } from '../utils/url.js';
@@ -10,6 +10,7 @@ import { AuthorizationService } from './authorization.js';
10
10
  import { ItemsService } from './items.js';
11
11
  import { MailService } from './mail/index.js';
12
12
  import { UsersService } from './users.js';
13
+ const env = useEnv();
13
14
  export class SharesService extends ItemsService {
14
15
  authorizationService;
15
16
  constructor(options) {
@@ -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';
3
4
  import { version } from 'directus/version';
4
5
  import { cloneDeep, mergeWith } from 'lodash-es';
5
6
  import { OAS_REQUIRED_SCHEMAS } from '../constants.js';
6
7
  import getDatabase from '../database/index.js';
7
- import env from '../env.js';
8
8
  import { getRelationType } from '../utils/get-relation-type.js';
9
9
  import { reduceSchema } from '../utils/reduce-schema.js';
10
10
  import { GraphQLService } from './graphql/index.js';
11
+ const env = useEnv();
11
12
  export class SpecificationService {
12
13
  accountability;
13
14
  knex;
@@ -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);
@@ -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: user.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: user.email,
344
+ url: this.inviteUrl(user?.email ?? email, url),
345
+ email: user?.email ?? email,
335
346
  },
336
347
  },
337
348
  });