@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
@@ -11,7 +11,7 @@ import { isNestedMetaUpdate } from '../../../utils/apply-diff.js';
11
11
  import { applySnapshot } from '../../../utils/apply-snapshot.js';
12
12
  import { getSnapshotDiff } from '../../../utils/get-snapshot-diff.js';
13
13
  import { getSnapshot } from '../../../utils/get-snapshot.js';
14
- function filterSnapshotDiff(snapshot, filters) {
14
+ export function filterSnapshotDiff(snapshot, filters) {
15
15
  const filterSet = new Set(filters);
16
16
  function shouldKeep(item) {
17
17
  if (filterSet.has(item.collection))
@@ -23,6 +23,7 @@ function filterSnapshotDiff(snapshot, filters) {
23
23
  const filteredDiff = {
24
24
  collections: snapshot.collections.filter((item) => shouldKeep(item)),
25
25
  fields: snapshot.fields.filter((item) => shouldKeep(item)),
26
+ systemFields: snapshot.systemFields.filter((item) => shouldKeep(item)),
26
27
  relations: snapshot.relations.filter((item) => shouldKeep(item)),
27
28
  };
28
29
  return filteredDiff;
@@ -53,6 +54,7 @@ export async function apply(snapshotPath, options) {
53
54
  }
54
55
  if (snapshotDiff.collections.length === 0 &&
55
56
  snapshotDiff.fields.length === 0 &&
57
+ snapshotDiff.systemFields.length === 0 &&
56
58
  snapshotDiff.relations.length === 0) {
57
59
  logger.info('No changes to apply.');
58
60
  database.destroy();
@@ -116,6 +118,27 @@ export async function apply(snapshotPath, options) {
116
118
  }
117
119
  sections.push(lines.join('\n'));
118
120
  }
121
+ if (snapshotDiff.systemFields.length > 0) {
122
+ const lines = [chalk.underline.bold('System Fields:')];
123
+ for (const { collection, field, diff } of snapshotDiff.systemFields) {
124
+ if (diff[0]?.kind === DiffKind.EDIT) {
125
+ lines.push(` - ${chalk.magenta('Update')} ${collection}.${field}`);
126
+ for (const change of diff) {
127
+ const path = formatPath(change.path);
128
+ if (change.kind === DiffKind.EDIT) {
129
+ lines.push(` - Set ${path} to ${change.rhs}`);
130
+ }
131
+ else if (change.kind === DiffKind.DELETE) {
132
+ lines.push(` - Remove ${path}`);
133
+ }
134
+ else if (change.kind === DiffKind.NEW) {
135
+ lines.push(` - Add ${path} and set it to ${change.rhs}`);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ sections.push(lines.join('\n'));
141
+ }
119
142
  if (snapshotDiff.relations.length > 0) {
120
143
  const lines = [chalk.underline.bold('Relations:')];
121
144
  for (const { collection, field, related_collection, diff } of snapshotDiff.relations) {
@@ -169,13 +192,13 @@ export async function apply(snapshotPath, options) {
169
192
  process.exit(1);
170
193
  }
171
194
  }
172
- function formatPath(path) {
195
+ export function formatPath(path) {
173
196
  if (path.length === 1) {
174
197
  return path.toString();
175
198
  }
176
199
  return path.slice(1).join('.');
177
200
  }
178
- function formatRelatedCollection(relatedCollection) {
201
+ export function formatRelatedCollection(relatedCollection) {
179
202
  // Related collection doesn't exist for a2o relationship types
180
203
  if (relatedCollection) {
181
204
  return ` → ${relatedCollection}`;
@@ -11,13 +11,18 @@ router.post('/', asyncHandler(async (req, res, next) => {
11
11
  accountability: req.accountability,
12
12
  schema: req.schema,
13
13
  });
14
+ const attemptConcurrentIndex = 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false';
14
15
  if (Array.isArray(req.body)) {
15
- const collectionKey = await collectionsService.createMany(req.body);
16
+ const collectionKey = await collectionsService.createMany(req.body, {
17
+ attemptConcurrentIndex,
18
+ });
16
19
  const records = await collectionsService.readMany(collectionKey);
17
20
  res.locals['payload'] = { data: records || null };
18
21
  }
19
22
  else {
20
- const collectionKey = await collectionsService.createOne(req.body);
23
+ const collectionKey = await collectionsService.createOne(req.body, {
24
+ attemptConcurrentIndex,
25
+ });
21
26
  const record = await collectionsService.readOne(collectionKey);
22
27
  res.locals['payload'] = { data: record || null };
23
28
  }
@@ -1,5 +1,5 @@
1
1
  import { TYPES } from '@directus/constants';
2
- import { isDirectusError } from '@directus/errors';
2
+ import { ForbiddenError, isDirectusError } from '@directus/errors';
3
3
  import { Router } from 'express';
4
4
  import Joi from 'joi';
5
5
  import { ALIAS_TYPES } from '../constants.js';
@@ -7,8 +7,9 @@ import { ErrorCode, InvalidPayloadError } from '@directus/errors';
7
7
  import validateCollection from '../middleware/collection-exists.js';
8
8
  import { respond } from '../middleware/respond.js';
9
9
  import useCollection from '../middleware/use-collection.js';
10
- import { FieldsService } from '../services/fields.js';
10
+ import { FieldsService, systemFieldUpdateSchema } from '../services/fields.js';
11
11
  import asyncHandler from '../utils/async-handler.js';
12
+ import { isSystemField } from '@directus/system-data';
12
13
  const router = Router();
13
14
  router.use(useCollection('directus_fields'));
14
15
  router.get('/', asyncHandler(async (req, res, next) => {
@@ -64,7 +65,9 @@ router.post('/:collection', validateCollection, asyncHandler(async (req, res, ne
64
65
  throw new InvalidPayloadError({ reason: error.message });
65
66
  }
66
67
  const field = req.body;
67
- await service.createField(req.params['collection'], field);
68
+ await service.createField(req.params['collection'], field, undefined, {
69
+ attemptConcurrentIndex: 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false',
70
+ });
68
71
  try {
69
72
  const createdField = await service.readOne(req.params['collection'], field.field);
70
73
  res.locals['payload'] = { data: createdField || null };
@@ -85,7 +88,16 @@ router.patch('/:collection', validateCollection, asyncHandler(async (req, res, n
85
88
  if (Array.isArray(req.body) === false) {
86
89
  throw new InvalidPayloadError({ reason: 'Submitted body has to be an array' });
87
90
  }
88
- await service.updateFields(req.params['collection'], req.body);
91
+ for (const fieldData of req.body) {
92
+ if (isSystemField(req.params['collection'], fieldData['field'])) {
93
+ const { error } = systemFieldUpdateSchema.safeParse(fieldData);
94
+ if (error)
95
+ throw error.issues.map((details) => new InvalidPayloadError({ reason: details.message }));
96
+ }
97
+ }
98
+ await service.updateFields(req.params['collection'], req.body, {
99
+ attemptConcurrentIndex: 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false',
100
+ });
89
101
  try {
90
102
  const results = [];
91
103
  for (const field of req.body) {
@@ -120,14 +132,22 @@ router.patch('/:collection/:field', validateCollection, asyncHandler(async (req,
120
132
  accountability: req.accountability,
121
133
  schema: req.schema,
122
134
  });
123
- const { error } = updateSchema.validate(req.body);
124
- if (error) {
125
- throw new InvalidPayloadError({ reason: error.message });
135
+ if (isSystemField(req.params['collection'], req.params['field'])) {
136
+ const { error } = systemFieldUpdateSchema.safeParse(req.body);
137
+ if (error)
138
+ throw error.issues.map((details) => new InvalidPayloadError({ reason: details.message }));
139
+ }
140
+ else {
141
+ const { error } = updateSchema.validate(req.body);
142
+ if (error)
143
+ throw new InvalidPayloadError({ reason: error.message });
126
144
  }
127
145
  const fieldData = req.body;
128
146
  if (!fieldData.field)
129
147
  fieldData.field = req.params['field'];
130
- await service.updateField(req.params['collection'], fieldData);
148
+ await service.updateField(req.params['collection'], fieldData, {
149
+ attemptConcurrentIndex: 'concurrentIndexCreation' in req.query && req.query['concurrentIndexCreation'] !== 'false',
150
+ });
131
151
  try {
132
152
  const updatedField = await service.readOne(req.params['collection'], req.params['field']);
133
153
  res.locals['payload'] = { data: updatedField || null };
@@ -145,6 +165,9 @@ router.delete('/:collection/:field', validateCollection, asyncHandler(async (req
145
165
  accountability: req.accountability,
146
166
  schema: req.schema,
147
167
  });
168
+ if (isSystemField(req.params['collection'], req.params['field'])) {
169
+ throw new ForbiddenError();
170
+ }
148
171
  await service.deleteField(req.params['collection'], req.params['field']);
149
172
  return next();
150
173
  }), respond);
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -0,0 +1,33 @@
1
+ import { ForbiddenError } from '@directus/errors';
2
+ import { Router } from 'express';
3
+ import { DirectusMCP } from '../mcp/index.js';
4
+ import { SettingsService } from '../services/settings.js';
5
+ import asyncHandler from '../utils/async-handler.js';
6
+ const router = Router();
7
+ const mcpHandler = asyncHandler(async (req, res) => {
8
+ const settings = new SettingsService({
9
+ schema: req.schema,
10
+ });
11
+ const { mcp_enabled, mcp_allow_deletes, mcp_prompts_collection, mcp_system_prompt, mcp_system_prompt_enabled } = await settings.readSingleton({
12
+ fields: [
13
+ 'mcp_enabled',
14
+ 'mcp_allow_deletes',
15
+ 'mcp_prompts_collection',
16
+ 'mcp_system_prompt',
17
+ 'mcp_system_prompt_enabled',
18
+ ],
19
+ });
20
+ if (!mcp_enabled) {
21
+ throw new ForbiddenError({ reason: 'MCP must be enabled' });
22
+ }
23
+ const mcp = new DirectusMCP({
24
+ promptsCollection: mcp_prompts_collection,
25
+ allowDeletes: mcp_allow_deletes,
26
+ systemPromptEnabled: mcp_system_prompt_enabled,
27
+ systemPrompt: mcp_system_prompt,
28
+ });
29
+ mcp.handleRequest(req, res);
30
+ });
31
+ router.get('/', mcpHandler);
32
+ router.post('/', mcpHandler);
33
+ export default router;
@@ -1,10 +1,12 @@
1
- import { RouteNotFoundError } from '@directus/errors';
1
+ import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
2
2
  import { format } from 'date-fns';
3
3
  import { Router } from 'express';
4
4
  import { respond } from '../middleware/respond.js';
5
+ import { SettingsService } from '../services/index.js';
5
6
  import { ServerService } from '../services/server.js';
6
7
  import { SpecificationService } from '../services/specifications.js';
7
8
  import asyncHandler from '../utils/async-handler.js';
9
+ import { createAdmin } from '../utils/create-admin.js';
8
10
  const router = Router();
9
11
  router.get('/specs/oas', asyncHandler(async (req, res, next) => {
10
12
  const service = new SpecificationService({
@@ -54,4 +56,27 @@ router.get('/health', asyncHandler(async (req, res, next) => {
54
56
  res.locals['cache'] = false;
55
57
  return next();
56
58
  }), respond);
59
+ router.post('/setup', asyncHandler(async (req, _res, next) => {
60
+ const serverService = new ServerService({ schema: req.schema });
61
+ if (await serverService.isSetupCompleted()) {
62
+ throw new ForbiddenError();
63
+ }
64
+ try {
65
+ await createAdmin(req.schema, {
66
+ email: req.body.project_owner,
67
+ password: req.body.password,
68
+ first_name: req.body.first_name,
69
+ last_name: req.body.last_name,
70
+ });
71
+ const settingsService = new SettingsService({ schema: req.schema });
72
+ settingsService.setOwner(req.body);
73
+ }
74
+ catch (error) {
75
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
76
+ return next();
77
+ }
78
+ throw error;
79
+ }
80
+ return next();
81
+ }), respond);
57
82
  export default router;
@@ -1,6 +1,5 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, isDirectusError } from '@directus/errors';
2
2
  import express from 'express';
3
- import { ErrorCode } from '@directus/errors';
4
3
  import { respond } from '../middleware/respond.js';
5
4
  import useCollection from '../middleware/use-collection.js';
6
5
  import { SettingsService } from '../services/settings.js';
@@ -16,6 +15,14 @@ router.get('/', asyncHandler(async (req, res, next) => {
16
15
  res.locals['payload'] = { data: records || null };
17
16
  return next();
18
17
  }), respond);
18
+ router.post('/owner', asyncHandler(async (req, _res, next) => {
19
+ const service = new SettingsService({
20
+ accountability: req.accountability,
21
+ schema: req.schema,
22
+ });
23
+ await service.setOwner(req.body);
24
+ return next();
25
+ }), respond);
19
26
  router.patch('/', asyncHandler(async (req, res, next) => {
20
27
  const service = new SettingsService({
21
28
  accountability: req.accountability,
@@ -1,6 +1,8 @@
1
1
  import { ErrorCode, ForbiddenError, InvalidCredentialsError, InvalidPayloadError, isDirectusError, } from '@directus/errors';
2
2
  import express from 'express';
3
3
  import Joi from 'joi';
4
+ import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
5
+ import { getDatabase } from '../database/index.js';
4
6
  import checkRateLimit from '../middleware/rate-limiter-registration.js';
5
7
  import { respond } from '../middleware/respond.js';
6
8
  import useCollection from '../middleware/use-collection.js';
@@ -231,19 +233,27 @@ router.post('/me/tfa/generate/', asyncHandler(async (req, res, next) => {
231
233
  if (!req.accountability?.user) {
232
234
  throw new InvalidCredentialsError();
233
235
  }
234
- if (!req.body.password) {
236
+ const currentUser = await getDatabase()
237
+ .select('provider')
238
+ .from('directus_users')
239
+ .where({ id: req.accountability.user })
240
+ .first();
241
+ const requiresPassword = currentUser?.['provider'] === DEFAULT_AUTH_PROVIDER;
242
+ if (requiresPassword && !req.body.password) {
235
243
  throw new InvalidPayloadError({ reason: `"password" is required` });
236
244
  }
237
245
  const service = new TFAService({
238
246
  accountability: req.accountability,
239
247
  schema: req.schema,
240
248
  });
241
- const authService = new AuthenticationService({
242
- accountability: req.accountability,
243
- schema: req.schema,
244
- });
245
- await authService.verifyPassword(req.accountability.user, req.body.password);
246
- const { url, secret } = await service.generateTFA(req.accountability.user);
249
+ if (requiresPassword) {
250
+ const authService = new AuthenticationService({
251
+ accountability: req.accountability,
252
+ schema: req.schema,
253
+ });
254
+ await authService.verifyPassword(req.accountability.user, req.body.password);
255
+ }
256
+ const { url, secret } = await service.generateTFA(req.accountability.user, requiresPassword);
247
257
  res.locals['payload'] = { data: { secret, otpauth_url: url } };
248
258
  return next();
249
259
  }), respond);
@@ -154,9 +154,10 @@ router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
154
154
  });
155
155
  const version = await service.readOne(req.params['pk']);
156
156
  const { outdated, mainHash } = await service.verifyHash(version['collection'], version['item'], version['hash']);
157
- const current = assign({}, version['delta']);
157
+ const delta = version.delta ?? {};
158
+ delta[req.schema.collections[version.collection].primary] = version.item;
158
159
  const main = await service.getMainItem(version['collection'], version['item']);
159
- res.locals['payload'] = { data: { outdated, mainHash, current, main } };
160
+ res.locals['payload'] = { data: { outdated, mainHash, current: delta, main } };
160
161
  return next();
161
162
  }), respond);
162
163
  router.post('/:pk/save', asyncHandler(async (req, res, next) => {
@@ -1,3 +1,3 @@
1
- import type { MSSQLError } from './types.js';
2
1
  import type { Item } from '@directus/types';
2
+ import type { MSSQLError } from './types.js';
3
3
  export declare function extractError(error: MSSQLError, data: Partial<Item>): Promise<MSSQLError | Error>;
@@ -5,14 +5,15 @@ var MSSQLErrorCodes;
5
5
  MSSQLErrorCodes[MSSQLErrorCodes["FOREIGN_KEY_VIOLATION"] = 547] = "FOREIGN_KEY_VIOLATION";
6
6
  MSSQLErrorCodes[MSSQLErrorCodes["NOT_NULL_VIOLATION"] = 515] = "NOT_NULL_VIOLATION";
7
7
  MSSQLErrorCodes[MSSQLErrorCodes["NUMERIC_VALUE_OUT_OF_RANGE"] = 220] = "NUMERIC_VALUE_OUT_OF_RANGE";
8
- MSSQLErrorCodes[MSSQLErrorCodes["UNIQUE_VIOLATION"] = 2601] = "UNIQUE_VIOLATION";
8
+ MSSQLErrorCodes[MSSQLErrorCodes["UNIQUE_VIOLATION_INDEX"] = 2601] = "UNIQUE_VIOLATION_INDEX";
9
+ MSSQLErrorCodes[MSSQLErrorCodes["UNIQUE_VIOLATION_CONSTRAINT"] = 2627] = "UNIQUE_VIOLATION_CONSTRAINT";
9
10
  MSSQLErrorCodes[MSSQLErrorCodes["VALUE_LIMIT_VIOLATION"] = 2628] = "VALUE_LIMIT_VIOLATION";
10
11
  })(MSSQLErrorCodes || (MSSQLErrorCodes = {}));
11
12
  export async function extractError(error, data) {
12
13
  switch (error.number) {
13
- case MSSQLErrorCodes.UNIQUE_VIOLATION:
14
- case 2627:
15
- return await uniqueViolation();
14
+ case MSSQLErrorCodes.UNIQUE_VIOLATION_CONSTRAINT:
15
+ case MSSQLErrorCodes.UNIQUE_VIOLATION_INDEX:
16
+ return await uniqueViolation(error);
16
17
  case MSSQLErrorCodes.NUMERIC_VALUE_OUT_OF_RANGE:
17
18
  return numericValueOutOfRange();
18
19
  case MSSQLErrorCodes.VALUE_LIMIT_VIOLATION:
@@ -23,14 +24,20 @@ export async function extractError(error, data) {
23
24
  return foreignKeyViolation();
24
25
  }
25
26
  return error;
26
- async function uniqueViolation() {
27
+ async function uniqueViolation(error) {
27
28
  /**
28
29
  * NOTE:
29
- * SQL Server doesn't return the name of the offending column when a unique constraint is thrown:
30
+ * SQL Server doesn't return the name of the offending column when a unique error is thrown:
30
31
  *
32
+ * Constraint:
31
33
  * insert into [articles] ([unique]) values (@p0)
32
- * - Violation of UNIQUE KEY constraint 'UQ__articles__5A062640242004EB'.
33
- * Cannot insert duplicate key in object 'dbo.articles'. The duplicate key value is (rijk).
34
+ * - Violation of UNIQUE KEY constraint 'unique_contraint_name'. Cannot insert duplicate key in object 'dbo.article'.
35
+ * The duplicate key value is (rijk).
36
+ *
37
+ * Index:
38
+ * insert into [articles] ([unique]) values (@p0)
39
+ * - Cannot insert duplicate key row in object 'dbo.articles' with unique index 'unique_index_name'.
40
+ * The duplicate key value is (rijk).
34
41
  *
35
42
  * While it's not ideal, the best next thing we can do is extract the column name from
36
43
  * information_schema when this happens
@@ -41,8 +48,9 @@ export async function extractError(error, data) {
41
48
  const parenMatches = error.message.match(betweenParens);
42
49
  if (!quoteMatches || !parenMatches)
43
50
  return error;
44
- const keyName = quoteMatches[1].slice(1, -1);
45
- let collection = quoteMatches[0].slice(1, -1);
51
+ const [keyNameMatchIndex, collectionNameMatchIndex] = error.number === MSSQLErrorCodes.UNIQUE_VIOLATION_INDEX ? [1, 0] : [0, 1];
52
+ const keyName = quoteMatches[keyNameMatchIndex].slice(1, -1);
53
+ let collection = quoteMatches[collectionNameMatchIndex].slice(1, -1);
46
54
  let field = null;
47
55
  if (keyName) {
48
56
  const database = getDatabase();
@@ -1,6 +1,6 @@
1
- import { DatabaseHelper } from '../types.js';
2
- import { generateAlias } from '../../run-ast/lib/apply-query/index.js';
3
1
  import { applyFilter } from '../../run-ast/lib/apply-query/filter/index.js';
2
+ import { generateRelationalQueryAlias } from '../../run-ast/utils/generate-alias.js';
3
+ import { DatabaseHelper } from '../types.js';
4
4
  export class FnHelper extends DatabaseHelper {
5
5
  schema;
6
6
  constructor(knex, schema) {
@@ -16,7 +16,7 @@ export class FnHelper extends DatabaseHelper {
16
16
  throw new Error(`Field ${collectionName}.${column} isn't a nested relational collection`);
17
17
  }
18
18
  // generate a unique alias for the relation collection, to prevent collisions in self referencing relations
19
- const alias = generateAlias();
19
+ const alias = generateRelationalQueryAlias(table, column, collectionName, options);
20
20
  let countQuery = this.knex
21
21
  .count('*')
22
22
  .from({ [alias]: relation.collection })
@@ -1,10 +1,11 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
2
  import { type Knex } from 'knex';
3
- import type { Options, SortRecord } from '../types.js';
3
+ import type { CreateIndexOptions, Options, SortRecord } from '../types.js';
4
4
  import { SchemaHelper } from '../types.js';
5
5
  export declare class SchemaHelperCockroachDb extends SchemaHelper {
6
6
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
7
7
  constraintName(existingName: string): string;
8
8
  getDatabaseSize(): Promise<number | null>;
9
9
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
10
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
10
11
  }
@@ -44,4 +44,17 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
44
44
  groupByFields.push(...sortRecords.map(({ alias }) => alias));
45
45
  }
46
46
  }
47
+ async createIndex(collection, field, options = {}) {
48
+ const isUnique = Boolean(options.unique);
49
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
50
+ // https://www.cockroachlabs.com/docs/stable/create-index
51
+ if (options.attemptConcurrentIndex) {
52
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX CONCURRENTLY ?? ON ?? (??)`, [
53
+ constraintName,
54
+ collection,
55
+ field,
56
+ ]);
57
+ }
58
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
59
+ }
47
60
  }
@@ -1,5 +1,5 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
2
+ import { SchemaHelper, type CreateIndexOptions, type SortRecord, type Sql } from '../types.js';
3
3
  export declare class SchemaHelperMSSQL extends SchemaHelper {
4
4
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
5
5
  applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
@@ -10,4 +10,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
10
10
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
11
11
  getColumnNameMaxLength(): number;
12
12
  getTableNameMaxLength(): number;
13
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
13
14
  }
@@ -60,4 +60,27 @@ export class SchemaHelperMSSQL extends SchemaHelper {
60
60
  getTableNameMaxLength() {
61
61
  return 128;
62
62
  }
63
+ async createIndex(collection, field, options = {}) {
64
+ const isUnique = Boolean(options.unique);
65
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
66
+ /*
67
+ Online index operations are not available in every edition of Microsoft SQL Server.
68
+ For a list of features that are supported by the editions of SQL Server, see Editions and supported features of SQL Server 2022.
69
+
70
+ https://learn.microsoft.com/en-us/sql/sql-server/editions-and-components-of-sql-server-2022?view=sql-server-ver16#rdbms-high-availability
71
+ */
72
+ const edition = await this.knex
73
+ .raw(`SELECT SERVERPROPERTY('edition') AS edition`)
74
+ .then((data) => data?.[0]?.['edition']);
75
+ if (options.attemptConcurrentIndex && typeof edition === 'string' && edition.startsWith('Enterprise')) {
76
+ // https://learn.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql?view=sql-server-ver16#online---on--off-
77
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??) WITH (ONLINE = ON)`, [
78
+ constraintName,
79
+ collection,
80
+ field,
81
+ ]);
82
+ }
83
+ // Fall back to blocking index creation for non-enterprise editions
84
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
85
+ }
63
86
  }
@@ -1,7 +1,8 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord } from '../types.js';
2
+ import { SchemaHelper, type CreateIndexOptions, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperMySQL extends SchemaHelper {
4
4
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
5
5
  getDatabaseSize(): Promise<number | null>;
6
6
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
7
8
  }
@@ -54,4 +54,29 @@ export class SchemaHelperMySQL extends SchemaHelper {
54
54
  groupByFields.push(...sortRecords.map(({ alias }) => alias));
55
55
  }
56
56
  }
57
+ async createIndex(collection, field, options = {}) {
58
+ const isUnique = Boolean(options.unique);
59
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
60
+ const blockingQuery = this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [
61
+ constraintName,
62
+ collection,
63
+ field,
64
+ ]);
65
+ if (options.attemptConcurrentIndex) {
66
+ /*
67
+ Seems it is not possible to determine whether "ALGORITHM=INPLACE LOCK=NONE" will be supported
68
+ so we're just going to send it and fall back to blocking index creation on error
69
+
70
+ https://dev.mysql.com/doc/refman/8.4/en/create-index.html#:~:text=engine%20is%20changed.-,Table%20Copying%20and%20Locking%20Options,-ALGORITHM%20and%20LOCK
71
+ */
72
+ return this.knex
73
+ .raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??) ALGORITHM=INPLACE LOCK=NONE`, [
74
+ constraintName,
75
+ collection,
76
+ field,
77
+ ])
78
+ .catch(() => blockingQuery);
79
+ }
80
+ return blockingQuery;
81
+ }
57
82
  }
@@ -2,7 +2,7 @@ import type { KNEX_TYPES } from '@directus/constants';
2
2
  import type { Column } from '@directus/schema';
3
3
  import type { Field, RawField, Relation, Type } from '@directus/types';
4
4
  import type { Knex } from 'knex';
5
- import type { Options, SortRecord, Sql } from '../types.js';
5
+ import type { CreateIndexOptions, Options, SortRecord, Sql } from '../types.js';
6
6
  import { SchemaHelper } from '../types.js';
7
7
  export declare class SchemaHelperOracle extends SchemaHelper {
8
8
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
@@ -20,4 +20,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
20
20
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
21
21
  getColumnNameMaxLength(): number;
22
22
  getTableNameMaxLength(): number;
23
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
23
24
  }
@@ -102,4 +102,17 @@ export class SchemaHelperOracle extends SchemaHelper {
102
102
  getTableNameMaxLength() {
103
103
  return 128;
104
104
  }
105
+ async createIndex(collection, field, options = {}) {
106
+ const isUnique = Boolean(options.unique);
107
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
108
+ if (options.attemptConcurrentIndex) {
109
+ // https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/CREATE-INDEX.html#GUID-1F89BBC0-825F-4215-AF71-7588E31D8BFE__GUID-041E5429-065B-43D5-AC7F-66810140842C
110
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??) ONLINE`, [
111
+ constraintName,
112
+ collection,
113
+ field,
114
+ ]);
115
+ }
116
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
117
+ }
105
118
  }
@@ -1,7 +1,8 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type SortRecord } from '../types.js';
2
+ import { SchemaHelper, type CreateIndexOptions, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
4
4
  generateIndexName(type: 'unique' | 'foreign' | 'index', collection: string, fields: string | string[]): string;
5
5
  getDatabaseSize(): Promise<number | null>;
6
6
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
7
8
  }
@@ -38,4 +38,17 @@ export class SchemaHelperPostgres extends SchemaHelper {
38
38
  groupByFields.push(...sortRecords.map(({ alias }) => alias));
39
39
  }
40
40
  }
41
+ async createIndex(collection, field, options = {}) {
42
+ const isUnique = Boolean(options.unique);
43
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
44
+ // https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY
45
+ if (options.attemptConcurrentIndex) {
46
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX CONCURRENTLY ?? ON ?? (??)`, [
47
+ constraintName,
48
+ collection,
49
+ field,
50
+ ]);
51
+ }
52
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
53
+ }
41
54
  }
@@ -16,6 +16,10 @@ export type SortRecord = {
16
16
  alias: string;
17
17
  column: Knex.Raw;
18
18
  };
19
+ export type CreateIndexOptions = {
20
+ attemptConcurrentIndex?: boolean;
21
+ unique?: boolean;
22
+ };
19
23
  export declare abstract class SchemaHelper extends DatabaseHelper {
20
24
  isOneOfClients(clients: DatabaseClient[]): boolean;
21
25
  changeNullable(table: string, column: string, nullable: boolean): Promise<void>;
@@ -41,4 +45,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
41
45
  addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
42
46
  getColumnNameMaxLength(): number;
43
47
  getTableNameMaxLength(): number;
48
+ createIndex(collection: string, field: string, options?: CreateIndexOptions): Promise<Knex.SchemaBuilder>;
44
49
  }
@@ -118,4 +118,10 @@ export class SchemaHelper extends DatabaseHelper {
118
118
  getTableNameMaxLength() {
119
119
  return 64;
120
120
  }
121
+ async createIndex(collection, field, options = {}) {
122
+ // fall back to concurrent index creation
123
+ const isUnique = Boolean(options.unique);
124
+ const constraintName = this.generateIndexName(isUnique ? 'unique' : 'index', collection, field);
125
+ return this.knex.raw(`CREATE ${isUnique ? 'UNIQUE ' : ''}INDEX ?? ON ?? (??)`, [constraintName, collection, field]);
126
+ }
121
127
  }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;