@directus/api 30.0.0 → 32.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 (197) hide show
  1. package/dist/app.js +7 -0
  2. package/dist/auth/auth.d.ts +2 -1
  3. package/dist/auth/auth.js +7 -2
  4. package/dist/auth/drivers/ldap.d.ts +0 -2
  5. package/dist/auth/drivers/ldap.js +9 -7
  6. package/dist/auth/drivers/oauth2.d.ts +0 -2
  7. package/dist/auth/drivers/oauth2.js +28 -11
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +28 -11
  10. package/dist/auth/drivers/saml.d.ts +0 -2
  11. package/dist/auth/drivers/saml.js +5 -5
  12. package/dist/auth.js +1 -2
  13. package/dist/cli/commands/bootstrap/index.js +12 -33
  14. package/dist/cli/commands/init/index.js +1 -1
  15. package/dist/cli/commands/schema/apply.d.ts +4 -0
  16. package/dist/cli/commands/schema/apply.js +26 -3
  17. package/dist/controllers/collections.js +7 -2
  18. package/dist/controllers/fields.js +31 -8
  19. package/dist/controllers/mcp.d.ts +2 -0
  20. package/dist/controllers/mcp.js +33 -0
  21. package/dist/controllers/server.js +26 -1
  22. package/dist/controllers/settings.js +9 -2
  23. package/dist/controllers/users.js +17 -7
  24. package/dist/controllers/versions.js +3 -2
  25. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  26. package/dist/database/errors/dialects/mssql.js +18 -10
  27. package/dist/database/helpers/fn/types.js +3 -3
  28. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  29. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  30. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  31. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  32. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  33. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  34. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  35. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  36. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  37. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  38. package/dist/database/helpers/schema/types.d.ts +5 -0
  39. package/dist/database/helpers/schema/types.js +6 -0
  40. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  41. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  42. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  43. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  44. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  45. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  46. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  47. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  48. package/dist/database/run-ast/README.md +46 -0
  49. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  50. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  51. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  52. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  53. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  54. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  55. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  56. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  57. package/dist/flows.js +1 -0
  58. package/dist/mcp/define.d.ts +2 -0
  59. package/dist/mcp/define.js +3 -0
  60. package/dist/mcp/index.d.ts +1 -0
  61. package/dist/mcp/index.js +1 -0
  62. package/dist/mcp/schema.d.ts +485 -0
  63. package/dist/mcp/schema.js +219 -0
  64. package/dist/mcp/server.d.ts +103 -0
  65. package/dist/mcp/server.js +310 -0
  66. package/dist/mcp/tools/assets.d.ts +3 -0
  67. package/dist/mcp/tools/assets.js +54 -0
  68. package/dist/mcp/tools/collections.d.ts +84 -0
  69. package/dist/mcp/tools/collections.js +90 -0
  70. package/dist/mcp/tools/fields.d.ts +101 -0
  71. package/dist/mcp/tools/fields.js +157 -0
  72. package/dist/mcp/tools/files.d.ts +235 -0
  73. package/dist/mcp/tools/files.js +103 -0
  74. package/dist/mcp/tools/flows.d.ts +323 -0
  75. package/dist/mcp/tools/flows.js +85 -0
  76. package/dist/mcp/tools/folders.d.ts +95 -0
  77. package/dist/mcp/tools/folders.js +96 -0
  78. package/dist/mcp/tools/index.d.ts +15 -0
  79. package/dist/mcp/tools/index.js +29 -0
  80. package/dist/mcp/tools/items.d.ts +87 -0
  81. package/dist/mcp/tools/items.js +141 -0
  82. package/dist/mcp/tools/operations.d.ts +171 -0
  83. package/dist/mcp/tools/operations.js +77 -0
  84. package/dist/mcp/tools/prompts/assets.md +8 -0
  85. package/dist/mcp/tools/prompts/collections.md +336 -0
  86. package/dist/mcp/tools/prompts/fields.md +521 -0
  87. package/dist/mcp/tools/prompts/files.md +180 -0
  88. package/dist/mcp/tools/prompts/flows.md +495 -0
  89. package/dist/mcp/tools/prompts/folders.md +34 -0
  90. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  91. package/dist/mcp/tools/prompts/index.js +19 -0
  92. package/dist/mcp/tools/prompts/items.md +317 -0
  93. package/dist/mcp/tools/prompts/operations.md +721 -0
  94. package/dist/mcp/tools/prompts/relations.md +386 -0
  95. package/dist/mcp/tools/prompts/schema.md +130 -0
  96. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  97. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  98. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  99. package/dist/mcp/tools/relations.d.ts +73 -0
  100. package/dist/mcp/tools/relations.js +93 -0
  101. package/dist/mcp/tools/schema.d.ts +54 -0
  102. package/dist/mcp/tools/schema.js +317 -0
  103. package/dist/mcp/tools/system.d.ts +3 -0
  104. package/dist/mcp/tools/system.js +22 -0
  105. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  106. package/dist/mcp/tools/trigger-flow.js +48 -0
  107. package/dist/mcp/transport.d.ts +13 -0
  108. package/dist/mcp/transport.js +18 -0
  109. package/dist/mcp/types.d.ts +56 -0
  110. package/dist/mcp/types.js +1 -0
  111. package/dist/metrics/lib/create-metrics.js +16 -25
  112. package/dist/middleware/collection-exists.js +2 -2
  113. package/dist/operations/mail/index.js +3 -1
  114. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  115. package/dist/operations/mail/rate-limiter.js +29 -0
  116. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  117. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  118. package/dist/schedules/metrics.js +6 -2
  119. package/dist/schedules/project.d.ts +4 -0
  120. package/dist/schedules/project.js +27 -0
  121. package/dist/services/authentication.js +36 -0
  122. package/dist/services/collections.d.ts +3 -3
  123. package/dist/services/collections.js +16 -1
  124. package/dist/services/fields.d.ts +21 -5
  125. package/dist/services/fields.js +109 -32
  126. package/dist/services/graphql/resolvers/query.js +1 -1
  127. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  128. package/dist/services/graphql/schema/parse-query.js +8 -8
  129. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  130. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  131. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  132. package/dist/services/import-export.d.ts +9 -1
  133. package/dist/services/import-export.js +287 -101
  134. package/dist/services/items.d.ts +1 -1
  135. package/dist/services/items.js +50 -24
  136. package/dist/services/mail/index.js +2 -0
  137. package/dist/services/mail/rate-limiter.d.ts +1 -0
  138. package/dist/services/mail/rate-limiter.js +29 -0
  139. package/dist/services/meta.js +28 -24
  140. package/dist/services/payload.d.ts +7 -3
  141. package/dist/services/payload.js +26 -12
  142. package/dist/services/schema.js +4 -1
  143. package/dist/services/server.d.ts +1 -0
  144. package/dist/services/server.js +15 -18
  145. package/dist/services/settings.d.ts +2 -1
  146. package/dist/services/settings.js +15 -0
  147. package/dist/services/tfa.d.ts +1 -1
  148. package/dist/services/tfa.js +20 -5
  149. package/dist/services/tus/server.js +14 -9
  150. package/dist/services/versions.d.ts +6 -4
  151. package/dist/services/versions.js +84 -25
  152. package/dist/telemetry/lib/get-report.js +4 -4
  153. package/dist/telemetry/lib/send-report.d.ts +6 -1
  154. package/dist/telemetry/lib/send-report.js +3 -1
  155. package/dist/telemetry/types/report.d.ts +17 -1
  156. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  157. package/dist/telemetry/utils/get-settings.js +14 -0
  158. package/dist/test-utils/README.md +760 -0
  159. package/dist/test-utils/cache.d.ts +51 -0
  160. package/dist/test-utils/cache.js +59 -0
  161. package/dist/test-utils/database.d.ts +48 -0
  162. package/dist/test-utils/database.js +52 -0
  163. package/dist/test-utils/emitter.d.ts +35 -0
  164. package/dist/test-utils/emitter.js +38 -0
  165. package/dist/test-utils/fields-service.d.ts +28 -0
  166. package/dist/test-utils/fields-service.js +36 -0
  167. package/dist/test-utils/items-service.d.ts +23 -0
  168. package/dist/test-utils/items-service.js +37 -0
  169. package/dist/test-utils/knex.d.ts +164 -0
  170. package/dist/test-utils/knex.js +268 -0
  171. package/dist/test-utils/schema.d.ts +26 -0
  172. package/dist/test-utils/schema.js +35 -0
  173. package/dist/types/auth.d.ts +2 -3
  174. package/dist/utils/apply-diff.js +15 -0
  175. package/dist/utils/create-admin.d.ts +11 -0
  176. package/dist/utils/create-admin.js +50 -0
  177. package/dist/utils/get-schema.js +5 -3
  178. package/dist/utils/get-snapshot-diff.js +49 -5
  179. package/dist/utils/get-snapshot.js +13 -7
  180. package/dist/utils/sanitize-schema.d.ts +11 -4
  181. package/dist/utils/sanitize-schema.js +9 -6
  182. package/dist/utils/schedule.js +15 -19
  183. package/dist/utils/validate-diff.js +31 -0
  184. package/dist/utils/validate-snapshot.js +7 -0
  185. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  186. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  187. package/dist/utils/versioning/handle-version.d.ts +2 -2
  188. package/dist/utils/versioning/handle-version.js +47 -43
  189. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  190. package/dist/utils/versioning/split-recursive.js +27 -0
  191. package/dist/websocket/controllers/hooks.js +12 -20
  192. package/dist/websocket/messages.d.ts +3 -3
  193. package/package.json +65 -66
  194. package/dist/cli/utils/defaults.d.ts +0 -4
  195. package/dist/cli/utils/defaults.js +0 -17
  196. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  197. package/dist/telemetry/utils/get-project-id.js +0 -4
@@ -8,7 +8,7 @@ import { getCache } from '../cache.js';
8
8
  import { translateDatabaseError } from '../database/errors/translate.js';
9
9
  import { getAstFromQuery } from '../database/get-ast-from-query/get-ast-from-query.js';
10
10
  import { getHelpers } from '../database/helpers/index.js';
11
- import getDatabase from '../database/index.js';
11
+ import getDatabase, { getDatabaseClient } from '../database/index.js';
12
12
  import { runAst } from '../database/run-ast/run-ast.js';
13
13
  import emitter from '../emitter.js';
14
14
  import { processAst } from '../permissions/modules/process-ast/process-ast.js';
@@ -148,6 +148,7 @@ export class ItemsService {
148
148
  knex: trx,
149
149
  schema: this.schema,
150
150
  nested: this.nested,
151
+ overwriteDefaults: opts.overwriteDefaults,
151
152
  });
152
153
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
153
154
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
@@ -172,10 +173,15 @@ export class ItemsService {
172
173
  autoIncrementSequenceNeedsToBeReset = true;
173
174
  }
174
175
  try {
176
+ let returningOptions = undefined;
177
+ // Support MSSQL tables that have triggers.
178
+ if (getDatabaseClient(trx) === 'mssql') {
179
+ returningOptions = { includeTriggerModifications: true };
180
+ }
175
181
  const result = await trx
176
182
  .insert(payloadWithoutAliases)
177
183
  .into(this.collection)
178
- .returning(primaryKeyField)
184
+ .returning(primaryKeyField, returningOptions)
179
185
  .then((result) => result[0]);
180
186
  const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
181
187
  if (pkField.type === 'uuid') {
@@ -338,6 +344,7 @@ export class ItemsService {
338
344
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
339
345
  bypassEmitAction: (params) => nestedActionEvents.push(params),
340
346
  mutationTracker: opts.mutationTracker,
347
+ overwriteDefaults: opts.overwriteDefaults?.[index],
341
348
  bypassAutoIncrementSequenceReset,
342
349
  });
343
350
  primaryKeys.push(primaryKey);
@@ -434,8 +441,8 @@ export class ItemsService {
434
441
  const filterWithKey = assign({}, query.filter, { [primaryKeyField]: { _eq: key } });
435
442
  const queryWithKey = assign({}, query, { filter: filterWithKey });
436
443
  let results = [];
437
- if (query.version) {
438
- results = (await handleVersion(this, key, queryWithKey, opts));
444
+ if (query.version && query.version !== 'main') {
445
+ results = [await handleVersion(this, key, queryWithKey, opts)];
439
446
  }
440
447
  else {
441
448
  results = await this.readByQuery(queryWithKey, opts);
@@ -497,13 +504,15 @@ export class ItemsService {
497
504
  await transaction(this.knex, async (knex) => {
498
505
  const service = this.fork({ knex });
499
506
  let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None;
500
- for (const item of data) {
507
+ for (const index in data) {
508
+ const item = data[index];
501
509
  const primaryKey = item[primaryKeyField];
502
510
  if (!primaryKey)
503
511
  throw new InvalidPayloadError({ reason: `Item in update misses primary key` });
504
512
  const combinedOpts = {
505
513
  autoPurgeCache: false,
506
514
  ...opts,
515
+ overwriteDefaults: opts.overwriteDefaults?.[index],
507
516
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
508
517
  };
509
518
  keys.push(await service.updateOne(primaryKey, omit(item, primaryKeyField), combinedOpts));
@@ -591,6 +600,7 @@ export class ItemsService {
591
600
  knex: trx,
592
601
  schema: this.schema,
593
602
  nested: this.nested,
603
+ overwriteDefaults: opts.overwriteDefaults,
594
604
  });
595
605
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
596
606
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
@@ -752,8 +762,13 @@ export class ItemsService {
752
762
  const primaryKeys = await transaction(this.knex, async (knex) => {
753
763
  const service = this.fork({ knex });
754
764
  const primaryKeys = [];
755
- for (const payload of payloads) {
756
- const primaryKey = await service.upsertOne(payload, { ...(opts || {}), autoPurgeCache: false });
765
+ for (const index in payloads) {
766
+ const payload = payloads[index];
767
+ const primaryKey = await service.upsertOne(payload, {
768
+ ...(opts || {}),
769
+ overwriteDefaults: opts.overwriteDefaults?.[index],
770
+ autoPurgeCache: false,
771
+ });
757
772
  primaryKeys.push(primaryKey);
758
773
  }
759
774
  return primaryKeys;
@@ -796,12 +811,23 @@ export class ItemsService {
796
811
  }
797
812
  const primaryKeyField = this.schema.collections[this.collection].primary;
798
813
  validateKeys(this.schema, this.collection, primaryKeyField, keys);
814
+ const keysAfterHooks = opts.emitEvents !== false
815
+ ? await emitter.emitFilter(this.eventScope === 'items'
816
+ ? ['items.delete', `${this.collection}.items.delete`]
817
+ : `${this.eventScope}.delete`, keys, {
818
+ collection: this.collection,
819
+ }, {
820
+ database: this.knex,
821
+ schema: this.schema,
822
+ accountability: this.accountability,
823
+ })
824
+ : keys;
799
825
  if (this.accountability) {
800
826
  await validateAccess({
801
827
  accountability: this.accountability,
802
828
  action: 'delete',
803
829
  collection: this.collection,
804
- primaryKeys: keys,
830
+ primaryKeys: keysAfterHooks,
805
831
  }, {
806
832
  knex: this.knex,
807
833
  schema: this.schema,
@@ -810,17 +836,8 @@ export class ItemsService {
810
836
  if (opts.preMutationError) {
811
837
  throw opts.preMutationError;
812
838
  }
813
- if (opts.emitEvents !== false) {
814
- await emitter.emitFilter(this.eventScope === 'items' ? ['items.delete', `${this.collection}.items.delete`] : `${this.eventScope}.delete`, keys, {
815
- collection: this.collection,
816
- }, {
817
- database: this.knex,
818
- schema: this.schema,
819
- accountability: this.accountability,
820
- });
821
- }
822
839
  await transaction(this.knex, async (trx) => {
823
- await trx(this.collection).whereIn(primaryKeyField, keys).delete();
840
+ await trx(this.collection).whereIn(primaryKeyField, keysAfterHooks).delete();
824
841
  if (opts.userIntegrityCheckFlags) {
825
842
  if (opts.onRequireUserIntegrityCheck) {
826
843
  opts.onRequireUserIntegrityCheck(opts.userIntegrityCheckFlags);
@@ -837,7 +854,7 @@ export class ItemsService {
837
854
  knex: trx,
838
855
  schema: this.schema,
839
856
  });
840
- await activityService.createMany(keys.map((key) => ({
857
+ await activityService.createMany(keysAfterHooks.map((key) => ({
841
858
  action: Action.DELETE,
842
859
  user: this.accountability.user,
843
860
  collection: this.collection,
@@ -857,8 +874,8 @@ export class ItemsService {
857
874
  ? ['items.delete', `${this.collection}.items.delete`]
858
875
  : `${this.eventScope}.delete`,
859
876
  meta: {
860
- payload: keys,
861
- keys: keys,
877
+ payload: keysAfterHooks,
878
+ keys: keysAfterHooks,
862
879
  collection: this.collection,
863
880
  },
864
881
  context: {
@@ -874,7 +891,7 @@ export class ItemsService {
874
891
  emitter.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
875
892
  }
876
893
  }
877
- return keys;
894
+ return keysAfterHooks;
878
895
  }
879
896
  /**
880
897
  * Read/treat collection as singleton.
@@ -882,8 +899,17 @@ export class ItemsService {
882
899
  async readSingleton(query, opts) {
883
900
  query = clone(query);
884
901
  query.limit = 1;
885
- const records = await this.readByQuery(query, opts);
886
- const record = records[0];
902
+ let record;
903
+ if (query.version && query.version !== 'main') {
904
+ const primaryKeyField = this.schema.collections[this.collection].primary;
905
+ const key = (await this.knex.select(primaryKeyField).from(this.collection).first())?.[primaryKeyField];
906
+ if (key) {
907
+ record = await handleVersion(this, key, query, opts);
908
+ }
909
+ }
910
+ else {
911
+ record = (await this.readByQuery(query, opts))[0];
912
+ }
887
913
  if (!record) {
888
914
  let fields = Object.entries(this.schema.collections[this.collection].fields);
889
915
  const defaults = {};
@@ -10,6 +10,7 @@ import emitter from '../../emitter.js';
10
10
  import { useLogger } from '../../logger/index.js';
11
11
  import getMailer from '../../mailer.js';
12
12
  import { Url } from '../../utils/url.js';
13
+ import { useEmailRateLimiterQueue } from './rate-limiter.js';
13
14
  const env = useEnv();
14
15
  const logger = useLogger();
15
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -37,6 +38,7 @@ export class MailService {
37
38
  }
38
39
  }
39
40
  async send(options) {
41
+ await useEmailRateLimiterQueue();
40
42
  const payload = await emitter.emitFilter(`email.send`, options, {});
41
43
  if (!payload)
42
44
  return null;
@@ -0,0 +1 @@
1
+ export declare function useEmailRateLimiterQueue(): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { RateLimiterQueue } from 'rate-limiter-flexible';
3
+ import { createRateLimiter } from '../../rate-limiter.js';
4
+ import { toBoolean } from '@directus/utils';
5
+ import { EmailLimitExceededError } from '@directus/errors';
6
+ let emailRateLimiterQueue;
7
+ const env = useEnv();
8
+ if (toBoolean(env['RATE_LIMITER_EMAIL_ENABLED']) === true) {
9
+ emailRateLimiterQueue = new RateLimiterQueue(createRateLimiter('RATE_LIMITER_EMAIL'), {
10
+ maxQueueSize: Number(env['RATE_LIMITER_EMAIL_QUEUE_SIZE']),
11
+ });
12
+ }
13
+ export async function useEmailRateLimiterQueue() {
14
+ if (!emailRateLimiterQueue)
15
+ return;
16
+ try {
17
+ await emailRateLimiterQueue.removeTokens(1);
18
+ }
19
+ catch (err) {
20
+ if (err instanceof Error) {
21
+ throw new EmailLimitExceededError({
22
+ points: 'RATE_LIMITER_EMAIL_POINTS' in env ? Number(env['RATE_LIMITER_EMAIL_POINTS']) : undefined,
23
+ duration: 'RATE_LIMITER_EMAIL_DURATION' in env ? Number(env['RATE_LIMITER_EMAIL_DURATION']) : undefined,
24
+ message: 'RATE_LIMITER_EMAIL_ERROR_MESSAGE' in env ? String(env['RATE_LIMITER_EMAIL_ERROR_MESSAGE']) : undefined,
25
+ });
26
+ }
27
+ throw err;
28
+ }
29
+ }
@@ -1,8 +1,10 @@
1
1
  import { isArray } from 'lodash-es';
2
- import { getAstFromQuery } from '../database/get-ast-from-query/get-ast-from-query.js';
3
2
  import getDatabase from '../database/index.js';
4
- import { runAst } from '../database/run-ast/run-ast.js';
5
- import { processAst } from '../permissions/modules/process-ast/process-ast.js';
3
+ import applyQuery from '../database/run-ast/lib/apply-query/index.js';
4
+ import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
5
+ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
6
+ import { getCases } from '../permissions/modules/process-ast/lib/get-cases.js';
7
+ import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
6
8
  export class MetaService {
7
9
  knex;
8
10
  accountability;
@@ -33,27 +35,29 @@ export class MetaService {
33
35
  return this.filterCount(collection, {});
34
36
  }
35
37
  async filterCount(collection, query) {
36
- const primaryKeyName = this.schema.collections[collection].primary;
37
- const aggregateQuery = {
38
- aggregate: {
39
- countDistinct: [primaryKeyName],
40
- },
41
- search: query.search ?? null,
38
+ let permissions = [];
39
+ if (this.accountability && this.accountability.admin !== true) {
40
+ const context = { knex: this.knex, schema: this.schema };
41
+ await validateAccess({
42
+ accountability: this.accountability,
43
+ action: 'read',
44
+ collection,
45
+ }, context);
46
+ const policies = await fetchPolicies(this.accountability, context);
47
+ permissions = await fetchPermissions({ action: 'read', accountability: this.accountability, policies }, context);
48
+ }
49
+ const { cases } = getCases(collection, permissions, []);
50
+ const { query: dbQuery, hasJoins } = applyQuery(this.knex, collection, this.knex(collection), {
42
51
  filter: query.filter ?? null,
43
- };
44
- let ast = await getAstFromQuery({
45
- collection,
46
- query: aggregateQuery,
47
- accountability: this.accountability,
48
- }, {
49
- schema: this.schema,
50
- knex: this.knex,
51
- });
52
- ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
53
- const records = await runAst(ast, this.schema, this.accountability, {
54
- knex: this.knex,
55
- });
56
- return Number((isArray(records) ? records[0]?.['countDistinct'][primaryKeyName] : records?.['countDistinct'][primaryKeyName]) ??
57
- 0);
52
+ search: query.search ?? null,
53
+ }, this.schema, cases, permissions);
54
+ if (hasJoins) {
55
+ dbQuery.countDistinct({ count: [`${collection}.${this.schema.collections[collection].primary}`] });
56
+ }
57
+ else {
58
+ dbQuery.count('*', { as: 'count' });
59
+ }
60
+ const records = await dbQuery;
61
+ return Number((isArray(records) ? records[0]?.['count'] : records?.['count']) ?? 0);
58
62
  }
59
63
  }
@@ -1,4 +1,4 @@
1
- import type { AbstractServiceOptions, Accountability, Aggregate, FieldOverview, Item, MutationOptions, PayloadAction, PayloadServiceProcessRelationResult, PrimaryKey, SchemaOverview } from '@directus/types';
1
+ import type { AbstractServiceOptions, Accountability, Aggregate, DefaultOverwrite, FieldOverview, Item, MutationOptions, PayloadAction, PayloadServiceProcessRelationResult, PrimaryKey, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { Helpers } from '../database/helpers/index.js';
4
4
  type Transformers = {
@@ -9,6 +9,7 @@ type Transformers = {
9
9
  accountability: Accountability | null;
10
10
  specials: string[];
11
11
  helpers: Helpers;
12
+ overwriteDefaults: DefaultOverwrite | undefined;
12
13
  }) => Promise<any>;
13
14
  };
14
15
  /**
@@ -22,7 +23,10 @@ export declare class PayloadService {
22
23
  collection: string;
23
24
  schema: SchemaOverview;
24
25
  nested: string[];
25
- constructor(collection: string, options: AbstractServiceOptions);
26
+ overwriteDefaults: DefaultOverwrite | undefined;
27
+ constructor(collection: string, options: AbstractServiceOptions & {
28
+ overwriteDefaults?: DefaultOverwrite | undefined;
29
+ });
26
30
  transformers: Transformers;
27
31
  processValues(action: PayloadAction, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
28
32
  processValues(action: PayloadAction, payload: Partial<Item>): Promise<Partial<Item>>;
@@ -62,6 +66,6 @@ export declare class PayloadService {
62
66
  * Transforms the input partial payload to match the output structure, to have consistency
63
67
  * between delta and data
64
68
  */
65
- prepareDelta(data: Partial<Item>): Promise<string | null>;
69
+ prepareDelta(delta: Partial<Item>): Promise<string | null>;
66
70
  }
67
71
  export {};
@@ -21,6 +21,7 @@ export class PayloadService {
21
21
  collection;
22
22
  schema;
23
23
  nested;
24
+ overwriteDefaults;
24
25
  constructor(collection, options) {
25
26
  this.accountability = options.accountability || null;
26
27
  this.knex = options.knex || getDatabase();
@@ -28,6 +29,7 @@ export class PayloadService {
28
29
  this.collection = collection;
29
30
  this.schema = options.schema;
30
31
  this.nested = options.nested ?? [];
32
+ this.overwriteDefaults = options.overwriteDefaults;
31
33
  return this;
32
34
  }
33
35
  transformers = {
@@ -77,14 +79,14 @@ export class PayloadService {
77
79
  return value ? '**********' : null;
78
80
  return value;
79
81
  },
80
- async 'user-created'({ action, value, accountability }) {
82
+ async 'user-created'({ action, value, accountability, overwriteDefaults }) {
81
83
  if (action === 'create')
82
- return accountability?.user || null;
84
+ return (overwriteDefaults ? overwriteDefaults._user : accountability?.user) ?? null;
83
85
  return value;
84
86
  },
85
- async 'user-updated'({ action, value, accountability }) {
87
+ async 'user-updated'({ action, value, accountability, overwriteDefaults }) {
86
88
  if (action === 'update')
87
- return accountability?.user || null;
89
+ return (overwriteDefaults ? overwriteDefaults._user : accountability?.user) ?? null;
88
90
  return value;
89
91
  },
90
92
  async 'role-created'({ action, value, accountability }) {
@@ -97,14 +99,14 @@ export class PayloadService {
97
99
  return accountability?.role || null;
98
100
  return value;
99
101
  },
100
- async 'date-created'({ action, value, helpers }) {
102
+ async 'date-created'({ action, value, helpers, overwriteDefaults }) {
101
103
  if (action === 'create')
102
- return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
104
+ return new Date(overwriteDefaults ? overwriteDefaults._date : helpers.date.writeTimestamp(new Date().toISOString()));
103
105
  return value;
104
106
  },
105
- async 'date-updated'({ action, value, helpers }) {
107
+ async 'date-updated'({ action, value, helpers, overwriteDefaults }) {
106
108
  if (action === 'update')
107
- return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
109
+ return new Date(overwriteDefaults ? overwriteDefaults._date : helpers.date.writeTimestamp(new Date().toISOString()));
108
110
  return value;
109
111
  },
110
112
  async 'cast-csv'({ action, value }) {
@@ -212,6 +214,7 @@ export class PayloadService {
212
214
  accountability,
213
215
  specials: fieldSpecials,
214
216
  helpers: this.helpers,
217
+ overwriteDefaults: this.overwriteDefaults,
215
218
  });
216
219
  }
217
220
  }
@@ -405,6 +408,7 @@ export class PayloadService {
405
408
  autoPurgeCache: opts?.autoPurgeCache,
406
409
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
407
410
  skipTracking: opts?.skipTracking,
411
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
408
412
  onItemCreate: opts?.onItemCreate,
409
413
  mutationTracker: opts?.mutationTracker,
410
414
  });
@@ -419,6 +423,7 @@ export class PayloadService {
419
423
  autoPurgeCache: opts?.autoPurgeCache,
420
424
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
421
425
  skipTracking: opts?.skipTracking,
426
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
422
427
  onItemCreate: opts?.onItemCreate,
423
428
  mutationTracker: opts?.mutationTracker,
424
429
  });
@@ -479,6 +484,7 @@ export class PayloadService {
479
484
  autoPurgeCache: opts?.autoPurgeCache,
480
485
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
481
486
  skipTracking: opts?.skipTracking,
487
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
482
488
  onItemCreate: opts?.onItemCreate,
483
489
  mutationTracker: opts?.mutationTracker,
484
490
  });
@@ -493,6 +499,7 @@ export class PayloadService {
493
499
  autoPurgeCache: opts?.autoPurgeCache,
494
500
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
495
501
  skipTracking: opts?.skipTracking,
502
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.field],
496
503
  onItemCreate: opts?.onItemCreate,
497
504
  mutationTracker: opts?.mutationTracker,
498
505
  });
@@ -589,6 +596,7 @@ export class PayloadService {
589
596
  autoPurgeCache: opts?.autoPurgeCache,
590
597
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
591
598
  skipTracking: opts?.skipTracking,
599
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
592
600
  onItemCreate: opts?.onItemCreate,
593
601
  mutationTracker: opts?.mutationTracker,
594
602
  })));
@@ -619,6 +627,7 @@ export class PayloadService {
619
627
  autoPurgeCache: opts?.autoPurgeCache,
620
628
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
621
629
  skipTracking: opts?.skipTracking,
630
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
622
631
  onItemCreate: opts?.onItemCreate,
623
632
  mutationTracker: opts?.mutationTracker,
624
633
  });
@@ -632,6 +641,7 @@ export class PayloadService {
632
641
  autoPurgeCache: opts?.autoPurgeCache,
633
642
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
634
643
  skipTracking: opts?.skipTracking,
644
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field],
635
645
  onItemCreate: opts?.onItemCreate,
636
646
  mutationTracker: opts?.mutationTracker,
637
647
  });
@@ -679,13 +689,14 @@ export class PayloadService {
679
689
  autoPurgeCache: opts?.autoPurgeCache,
680
690
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
681
691
  skipTracking: opts?.skipTracking,
692
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['create'],
682
693
  onItemCreate: opts?.onItemCreate,
683
694
  mutationTracker: opts?.mutationTracker,
684
695
  });
685
696
  }
686
697
  if (alterations.update) {
687
- for (const item of alterations.update) {
688
- const { [relatedPrimaryKeyField]: key, ...record } = item;
698
+ for (const index in alterations.update) {
699
+ const { [relatedPrimaryKeyField]: key, ...record } = alterations.update[index];
689
700
  const existingRecord = await this.knex
690
701
  .select(relatedPrimaryKeyField, relation.field)
691
702
  .from(relation.collection)
@@ -702,6 +713,7 @@ export class PayloadService {
702
713
  autoPurgeCache: opts?.autoPurgeCache,
703
714
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
704
715
  skipTracking: opts?.skipTracking,
716
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['update'][index],
705
717
  onItemCreate: opts?.onItemCreate,
706
718
  mutationTracker: opts?.mutationTracker,
707
719
  });
@@ -733,6 +745,7 @@ export class PayloadService {
733
745
  autoPurgeCache: opts?.autoPurgeCache,
734
746
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
735
747
  skipTracking: opts?.skipTracking,
748
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['delete'],
736
749
  onItemCreate: opts?.onItemCreate,
737
750
  mutationTracker: opts?.mutationTracker,
738
751
  });
@@ -746,6 +759,7 @@ export class PayloadService {
746
759
  autoPurgeCache: opts?.autoPurgeCache,
747
760
  autoPurgeSystemCache: opts?.autoPurgeSystemCache,
748
761
  skipTracking: opts?.skipTracking,
762
+ overwriteDefaults: opts?.overwriteDefaults?.[relation.meta.one_field]?.['delete'],
749
763
  onItemCreate: opts?.onItemCreate,
750
764
  mutationTracker: opts?.mutationTracker,
751
765
  });
@@ -759,8 +773,8 @@ export class PayloadService {
759
773
  * Transforms the input partial payload to match the output structure, to have consistency
760
774
  * between delta and data
761
775
  */
762
- async prepareDelta(data) {
763
- let payload = cloneDeep(data);
776
+ async prepareDelta(delta) {
777
+ let payload = cloneDeep(delta);
764
778
  for (const key in payload) {
765
779
  if (payload[key]?.isRawInstance) {
766
780
  payload[key] = payload[key].bindings[0];
@@ -34,7 +34,10 @@ export class SchemaService {
34
34
  validateSnapshot(snapshot, options?.force);
35
35
  const currentSnapshot = options?.currentSnapshot ?? (await getSnapshot({ database: this.knex }));
36
36
  const diff = getSnapshotDiff(currentSnapshot, snapshot);
37
- if (diff.collections.length === 0 && diff.fields.length === 0 && diff.relations.length === 0) {
37
+ if (diff.collections.length === 0 &&
38
+ diff.fields.length === 0 &&
39
+ diff.relations.length === 0 &&
40
+ (!diff.systemFields || diff.systemFields.length === 0)) {
38
41
  return null;
39
42
  }
40
43
  return diff;
@@ -7,6 +7,7 @@ export declare class ServerService {
7
7
  settingsService: SettingsService;
8
8
  schema: SchemaOverview;
9
9
  constructor(options: AbstractServiceOptions);
10
+ isSetupCompleted(): Promise<boolean>;
10
11
  serverInfo(): Promise<Record<string, any>>;
11
12
  health(): Promise<Record<string, any>>;
12
13
  }
@@ -28,8 +28,12 @@ export class ServerService {
28
28
  this.schema = options.schema;
29
29
  this.settingsService = new SettingsService({ knex: this.knex, schema: this.schema });
30
30
  }
31
+ async isSetupCompleted() {
32
+ return Boolean(await this.knex('directus_users').first());
33
+ }
31
34
  async serverInfo() {
32
35
  const info = {};
36
+ const setupComplete = await this.isSetupCompleted();
33
37
  const projectInfo = await this.settingsService.readSingleton({
34
38
  fields: [
35
39
  'project_name',
@@ -53,6 +57,8 @@ export class ServerService {
53
57
  ],
54
58
  });
55
59
  info['project'] = projectInfo;
60
+ info['mcp_enabled'] = toBoolean(env['MCP_ENABLED'] ?? true);
61
+ info['setupCompleted'] = setupComplete;
56
62
  if (this.accountability?.user) {
57
63
  if (env['RATE_LIMITER_ENABLED']) {
58
64
  info['rateLimit'] = {
@@ -111,8 +117,9 @@ export class ServerService {
111
117
  chunkSize: RESUMABLE_UPLOADS.CHUNK_SIZE,
112
118
  };
113
119
  }
114
- info['version'] = version;
115
120
  }
121
+ if (this.accountability?.user || !setupComplete)
122
+ info['version'] = version;
116
123
  return info;
117
124
  }
118
125
  async health() {
@@ -221,8 +228,8 @@ export class ServerService {
221
228
  };
222
229
  const startTime = performance.now();
223
230
  try {
224
- await cache.set(`health-${checkID}`, true, 5);
225
- await cache.delete(`health-${checkID}`);
231
+ await cache.set(`directus-health-${checkID}`, true, 5);
232
+ await cache.delete(`directus-health-${checkID}`);
226
233
  }
227
234
  catch (err) {
228
235
  checks['cache:responseTime'][0].status = 'error';
@@ -255,8 +262,8 @@ export class ServerService {
255
262
  };
256
263
  const startTime = performance.now();
257
264
  try {
258
- await rateLimiter.consume(`health-${checkID}`, 1);
259
- await rateLimiter.delete(`health-${checkID}`);
265
+ await rateLimiter.consume(`directus-health-${checkID}`, 1);
266
+ await rateLimiter.delete(`directus-health-${checkID}`);
260
267
  }
261
268
  catch (err) {
262
269
  checks['rateLimiter:responseTime'][0].status = 'error';
@@ -291,8 +298,8 @@ export class ServerService {
291
298
  };
292
299
  const startTime = performance.now();
293
300
  try {
294
- await rateLimiterGlobal.consume(`health-${checkID}`, 1);
295
- await rateLimiterGlobal.delete(`health-${checkID}`);
301
+ await rateLimiterGlobal.consume(`directus-health-${checkID}`, 1);
302
+ await rateLimiterGlobal.delete(`directus-health-${checkID}`);
296
303
  }
297
304
  catch (err) {
298
305
  checks['rateLimiterGlobal:responseTime'][0].status = 'error';
@@ -326,17 +333,7 @@ export class ServerService {
326
333
  ];
327
334
  const startTime = performance.now();
328
335
  try {
329
- await disk.write(`health-${checkID}`, Readable.from(['check']));
330
- const fileStream = await disk.read(`health-${checkID}`);
331
- fileStream.on('data', async () => {
332
- try {
333
- fileStream.destroy();
334
- await disk.delete(`health-${checkID}`);
335
- }
336
- catch (error) {
337
- logger.error(error);
338
- }
339
- });
336
+ await disk.write('directus-health-file', Readable.from([checkID]));
340
337
  }
341
338
  catch (err) {
342
339
  checks[`storage:${location}:responseTime`][0].status = 'error';
@@ -1,5 +1,6 @@
1
- import type { AbstractServiceOptions } from '@directus/types';
1
+ import type { AbstractServiceOptions, OwnerInformation } from '@directus/types';
2
2
  import { ItemsService } from './items.js';
3
3
  export declare class SettingsService extends ItemsService {
4
4
  constructor(options: AbstractServiceOptions);
5
+ setOwner(data: OwnerInformation): Promise<import("@directus/types").PrimaryKey>;
5
6
  }
@@ -1,6 +1,21 @@
1
1
  import { ItemsService } from './items.js';
2
+ import { sendReport } from '../telemetry/index.js';
3
+ import { version } from 'directus/version';
2
4
  export class SettingsService extends ItemsService {
3
5
  constructor(options) {
4
6
  super('directus_settings', options);
5
7
  }
8
+ async setOwner(data) {
9
+ const { project_id } = await this.knex.select('project_id').from('directus_settings').first();
10
+ sendReport({ ...data, project_id, version }).catch(async () => {
11
+ await this.knex.update('project_status', 'pending').from('directus_settings');
12
+ });
13
+ return await this.upsertSingleton({
14
+ project_owner: data.project_owner,
15
+ project_usage: data.project_usage,
16
+ org_name: data.org_name,
17
+ product_updates: data.product_updates,
18
+ project_status: null,
19
+ });
20
+ }
6
21
  }
@@ -6,7 +6,7 @@ export declare class TFAService {
6
6
  itemsService: ItemsService;
7
7
  constructor(options: AbstractServiceOptions);
8
8
  verifyOTP(key: PrimaryKey, otp: string, secret?: string): Promise<boolean>;
9
- generateTFA(key: PrimaryKey): Promise<Record<string, string>>;
9
+ generateTFA(key: PrimaryKey, requiresPassword?: boolean): Promise<Record<string, string>>;
10
10
  enableTFA(key: PrimaryKey, otp: string, secret: string): Promise<void>;
11
11
  disableTFA(key: PrimaryKey): Promise<void>;
12
12
  }