@directus/api 31.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 (135) hide show
  1. package/dist/app.js +2 -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 +11 -8
  8. package/dist/auth/drivers/openid.d.ts +0 -2
  9. package/dist/auth/drivers/openid.js +11 -8
  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/server.js +26 -1
  20. package/dist/controllers/settings.js +9 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/database/helpers/fn/types.js +3 -3
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +23 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +25 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +13 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +13 -0
  33. package/dist/database/helpers/schema/types.d.ts +5 -0
  34. package/dist/database/helpers/schema/types.js +6 -0
  35. package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
  36. package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
  37. package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
  38. package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
  39. package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
  40. package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
  41. package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
  42. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  43. package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
  44. package/dist/database/run-ast/lib/apply-query/index.js +4 -6
  45. package/dist/database/run-ast/lib/apply-query/search.js +2 -0
  46. package/dist/database/run-ast/lib/get-db-query.js +7 -6
  47. package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
  48. package/dist/database/run-ast/utils/generate-alias.js +57 -0
  49. package/dist/flows.js +1 -0
  50. package/dist/mcp/schema.d.ts +14 -14
  51. package/dist/mcp/schema.js +6 -6
  52. package/dist/mcp/server.d.ts +9 -3
  53. package/dist/mcp/server.js +1 -1
  54. package/dist/mcp/tools/collections.d.ts +1 -1
  55. package/dist/mcp/tools/fields.d.ts +1 -1
  56. package/dist/mcp/tools/files.d.ts +25 -25
  57. package/dist/mcp/tools/flows.d.ts +36 -36
  58. package/dist/mcp/tools/folders.d.ts +18 -18
  59. package/dist/mcp/tools/items.d.ts +18 -18
  60. package/dist/mcp/tools/operations.d.ts +19 -19
  61. package/dist/mcp/tools/prompts/items.md +1 -1
  62. package/dist/metrics/lib/create-metrics.js +16 -25
  63. package/dist/middleware/collection-exists.js +2 -2
  64. package/dist/operations/mail/index.js +3 -1
  65. package/dist/operations/mail/rate-limiter.d.ts +1 -0
  66. package/dist/operations/mail/rate-limiter.js +29 -0
  67. package/dist/permissions/modules/process-payload/process-payload.js +3 -10
  68. package/dist/permissions/modules/validate-access/validate-access.js +2 -3
  69. package/dist/schedules/metrics.js +6 -2
  70. package/dist/schedules/project.d.ts +4 -0
  71. package/dist/schedules/project.js +27 -0
  72. package/dist/services/collections.d.ts +3 -3
  73. package/dist/services/collections.js +16 -1
  74. package/dist/services/fields.d.ts +21 -5
  75. package/dist/services/fields.js +105 -28
  76. package/dist/services/graphql/resolvers/query.js +1 -1
  77. package/dist/services/graphql/resolvers/system-admin.js +49 -5
  78. package/dist/services/graphql/schema/parse-query.js +8 -8
  79. package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
  80. package/dist/services/graphql/utils/aggregate-query.js +5 -1
  81. package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
  82. package/dist/services/import-export.d.ts +9 -1
  83. package/dist/services/import-export.js +287 -101
  84. package/dist/services/items.d.ts +1 -1
  85. package/dist/services/items.js +36 -20
  86. package/dist/services/mail/index.js +2 -0
  87. package/dist/services/mail/rate-limiter.d.ts +1 -0
  88. package/dist/services/mail/rate-limiter.js +29 -0
  89. package/dist/services/meta.js +28 -24
  90. package/dist/services/schema.js +4 -1
  91. package/dist/services/server.d.ts +1 -0
  92. package/dist/services/server.js +14 -18
  93. package/dist/services/settings.d.ts +2 -1
  94. package/dist/services/settings.js +15 -0
  95. package/dist/services/tus/server.js +14 -9
  96. package/dist/telemetry/lib/get-report.js +4 -4
  97. package/dist/telemetry/lib/send-report.d.ts +6 -1
  98. package/dist/telemetry/lib/send-report.js +3 -1
  99. package/dist/telemetry/types/report.d.ts +17 -1
  100. package/dist/telemetry/utils/get-settings.d.ts +9 -0
  101. package/dist/telemetry/utils/get-settings.js +14 -0
  102. package/dist/test-utils/README.md +760 -0
  103. package/dist/test-utils/cache.d.ts +51 -0
  104. package/dist/test-utils/cache.js +59 -0
  105. package/dist/test-utils/database.d.ts +48 -0
  106. package/dist/test-utils/database.js +52 -0
  107. package/dist/test-utils/emitter.d.ts +35 -0
  108. package/dist/test-utils/emitter.js +38 -0
  109. package/dist/test-utils/fields-service.d.ts +28 -0
  110. package/dist/test-utils/fields-service.js +36 -0
  111. package/dist/test-utils/items-service.d.ts +23 -0
  112. package/dist/test-utils/items-service.js +37 -0
  113. package/dist/test-utils/knex.d.ts +164 -0
  114. package/dist/test-utils/knex.js +268 -0
  115. package/dist/test-utils/schema.d.ts +26 -0
  116. package/dist/test-utils/schema.js +35 -0
  117. package/dist/types/auth.d.ts +0 -2
  118. package/dist/utils/apply-diff.js +15 -0
  119. package/dist/utils/create-admin.d.ts +11 -0
  120. package/dist/utils/create-admin.js +50 -0
  121. package/dist/utils/get-schema.js +5 -3
  122. package/dist/utils/get-snapshot-diff.js +49 -5
  123. package/dist/utils/get-snapshot.js +13 -7
  124. package/dist/utils/sanitize-schema.d.ts +11 -4
  125. package/dist/utils/sanitize-schema.js +9 -6
  126. package/dist/utils/schedule.js +15 -19
  127. package/dist/utils/validate-diff.js +31 -0
  128. package/dist/utils/validate-snapshot.js +7 -0
  129. package/dist/websocket/controllers/hooks.js +12 -20
  130. package/dist/websocket/messages.d.ts +3 -3
  131. package/package.json +63 -65
  132. package/dist/cli/utils/defaults.d.ts +0 -4
  133. package/dist/cli/utils/defaults.js +0 -17
  134. package/dist/telemetry/utils/get-project-id.d.ts +0 -2
  135. package/dist/telemetry/utils/get-project-id.js +0 -4
package/dist/app.js CHANGED
@@ -64,6 +64,7 @@ import metricsSchedule from './schedules/metrics.js';
64
64
  import retentionSchedule from './schedules/retention.js';
65
65
  import telemetrySchedule from './schedules/telemetry.js';
66
66
  import tusSchedule from './schedules/tus.js';
67
+ import projectSchedule from './schedules/project.js';
67
68
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
68
69
  import { Url } from './utils/url.js';
69
70
  import { validateStorage } from './utils/validate-storage.js';
@@ -262,6 +263,7 @@ export default async function createApp() {
262
263
  await telemetrySchedule();
263
264
  await tusSchedule();
264
265
  await metricsSchedule();
266
+ await projectSchedule();
265
267
  await emitter.emitInit('app.after', { app });
266
268
  return app;
267
269
  }
@@ -1,10 +1,11 @@
1
1
  import type { SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
+ import { UsersService } from '../services/users.js';
3
4
  import type { AuthDriverOptions, User } from '../types/index.js';
4
5
  export declare abstract class AuthDriver {
5
6
  knex: Knex;
6
- schema: SchemaOverview;
7
7
  constructor(options: AuthDriverOptions, _config: Record<string, any>);
8
+ protected getUsersService(schema: SchemaOverview): UsersService;
8
9
  /**
9
10
  * Get user id for a given provider payload
10
11
  *
package/dist/auth/auth.js CHANGED
@@ -1,9 +1,14 @@
1
+ import { UsersService } from '../services/users.js';
1
2
  export class AuthDriver {
2
3
  knex;
3
- schema;
4
4
  constructor(options, _config) {
5
5
  this.knex = options.knex;
6
- this.schema = options.schema;
6
+ }
7
+ getUsersService(schema) {
8
+ return new UsersService({
9
+ knex: this.knex,
10
+ schema,
11
+ });
7
12
  }
8
13
  /**
9
14
  * Check with the (external) provider if the user is allowed entry to Directus
@@ -1,11 +1,9 @@
1
1
  import { Router } from 'express';
2
2
  import type { Client } from 'ldapjs';
3
- import { UsersService } from '../../services/users.js';
4
3
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
4
  import { AuthDriver } from '../auth.js';
6
5
  export declare class LDAPAuthDriver extends AuthDriver {
7
6
  bindClient: Client;
8
- usersService: UsersService;
9
7
  config: Record<string, any>;
10
8
  constructor(options: AuthDriverOptions, config: Record<string, any>);
11
9
  private validateBindClient;
@@ -10,17 +10,16 @@ import { useLogger } from '../../logger/index.js';
10
10
  import { respond } from '../../middleware/respond.js';
11
11
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
13
- import { UsersService } from '../../services/users.js';
14
13
  import asyncHandler from '../../utils/async-handler.js';
15
14
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
16
15
  import { AuthDriver } from '../auth.js';
16
+ import { getSchema } from '../../utils/get-schema.js';
17
17
  // 0x2: ACCOUNTDISABLE
18
18
  // 0x10: LOCKOUT
19
19
  // 0x800000: PASSWORD_EXPIRED
20
20
  const INVALID_ACCOUNT_FLAGS = 0x800012;
21
21
  export class LDAPAuthDriver extends AuthDriver {
22
22
  bindClient;
23
- usersService;
24
23
  config;
25
24
  constructor(options, config) {
26
25
  super(options, config);
@@ -39,7 +38,6 @@ export class LDAPAuthDriver extends AuthDriver {
39
38
  this.bindClient.on('error', (err) => {
40
39
  logger.warn(err);
41
40
  });
42
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
43
41
  this.config = config;
44
42
  }
45
43
  async validateBindClient() {
@@ -216,9 +214,11 @@ export class LDAPAuthDriver extends AuthDriver {
216
214
  email: userInfo.email,
217
215
  };
218
216
  }
219
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
217
+ const schema = await getSchema();
218
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema, accountability: null });
220
219
  // Update user to update properties that might have changed
221
- await this.usersService.updateOne(userId, updatedUserPayload);
220
+ const usersService = this.getUsersService(schema);
221
+ await usersService.updateOne(userId, updatedUserPayload);
222
222
  return userId;
223
223
  }
224
224
  if (!userInfo) {
@@ -232,11 +232,13 @@ export class LDAPAuthDriver extends AuthDriver {
232
232
  external_identifier: userInfo.dn,
233
233
  role: userRole?.id ?? defaultRoleId,
234
234
  };
235
+ const schema = await getSchema();
235
236
  // Run hook so the end user has the chance to augment the
236
237
  // user that is about to be created
237
- const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
238
+ const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema, accountability: null });
238
239
  try {
239
- await this.usersService.createOne(updatedUserPayload);
240
+ const usersService = this.getUsersService(schema);
241
+ await usersService.createOne(updatedUserPayload);
240
242
  }
241
243
  catch (e) {
242
244
  if (isDirectusError(e, ErrorCode.RecordNotUnique)) {
@@ -1,13 +1,11 @@
1
1
  import { Router } from 'express';
2
2
  import type { Client } from 'openid-client';
3
- import { UsersService } from '../../services/users.js';
4
3
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
4
  import type { RoleMap } from '../../types/rolemap.js';
6
5
  import { LocalAuthDriver } from './local.js';
7
6
  export declare class OAuth2AuthDriver extends LocalAuthDriver {
8
7
  client: Client;
9
8
  redirectUrl: string;
10
- usersService: UsersService;
11
9
  config: Record<string, any>;
12
10
  roleMap: RoleMap;
13
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
@@ -13,7 +13,6 @@ import { useLogger } from '../../logger/index.js';
13
13
  import { respond } from '../../middleware/respond.js';
14
14
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
15
15
  import { AuthenticationService } from '../../services/authentication.js';
16
- import { UsersService } from '../../services/users.js';
17
16
  import asyncHandler from '../../utils/async-handler.js';
18
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
19
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
@@ -22,10 +21,10 @@ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js
22
21
  import { verifyJWT } from '../../utils/jwt.js';
23
22
  import { Url } from '../../utils/url.js';
24
23
  import { LocalAuthDriver } from './local.js';
24
+ import { getSchema } from '../../utils/get-schema.js';
25
25
  export class OAuth2AuthDriver extends LocalAuthDriver {
26
26
  client;
27
27
  redirectUrl;
28
- usersService;
29
28
  config;
30
29
  roleMap;
31
30
  constructor(options, config) {
@@ -39,7 +38,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
39
38
  }
40
39
  const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', additionalConfig['provider'], 'callback');
41
40
  this.redirectUrl = redirectUrl.toString();
42
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
43
41
  this.config = additionalConfig;
44
42
  this.roleMap = {};
45
43
  const roleMapping = this.config['roleMapping'];
@@ -177,14 +175,16 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
177
175
  email: userPayload.email,
178
176
  };
179
177
  }
178
+ const schema = await getSchema();
180
179
  const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
181
180
  identifier,
182
181
  provider: this.config['provider'],
183
182
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
184
- }, { database: getDatabase(), schema: this.schema, accountability: null });
183
+ }, { database: getDatabase(), schema, accountability: null });
185
184
  // Update user to update refresh_token and other properties that might have changed
186
185
  if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
187
- await this.usersService.updateOne(userId, updatedUserPayload);
186
+ const usersService = this.getUsersService(schema);
187
+ await usersService.updateOne(userId, updatedUserPayload);
188
188
  }
189
189
  return userId;
190
190
  }
@@ -193,15 +193,17 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
193
193
  logger.warn(`[OAuth2] User doesn't exist, and public registration not allowed for provider "${provider}"`);
194
194
  throw new InvalidCredentialsError();
195
195
  }
196
+ const schema = await getSchema();
196
197
  // Run hook so the end user has the chance to augment the
197
198
  // user that is about to be created
198
199
  const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
199
200
  identifier,
200
201
  provider: this.config['provider'],
201
202
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
202
- }, { database: getDatabase(), schema: this.schema, accountability: null });
203
+ }, { database: getDatabase(), schema, accountability: null });
203
204
  try {
204
- await this.usersService.createOne(updatedUserPayload);
205
+ const usersService = this.getUsersService(schema);
206
+ await usersService.createOne(updatedUserPayload);
205
207
  }
206
208
  catch (e) {
207
209
  if (isDirectusError(e, ErrorCode.RecordNotUnique)) {
@@ -231,7 +233,8 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
231
233
  const tokenSet = await this.client.refresh(authData['refreshToken']);
232
234
  // Update user refreshToken if provided
233
235
  if (tokenSet.refresh_token) {
234
- await this.usersService.updateOne(user.id, {
236
+ const usersService = this.getUsersService(await getSchema());
237
+ await usersService.updateOne(user.id, {
235
238
  auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
236
239
  });
237
240
  }
@@ -1,13 +1,11 @@
1
1
  import { Router } from 'express';
2
2
  import type { Client } from 'openid-client';
3
- import { UsersService } from '../../services/users.js';
4
3
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
4
  import type { RoleMap } from '../../types/rolemap.js';
6
5
  import { LocalAuthDriver } from './local.js';
7
6
  export declare class OpenIDAuthDriver extends LocalAuthDriver {
8
7
  client: null | Client;
9
8
  redirectUrl: string;
10
- usersService: UsersService;
11
9
  config: Record<string, any>;
12
10
  roleMap: RoleMap;
13
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
@@ -13,7 +13,6 @@ import { useLogger } from '../../logger/index.js';
13
13
  import { respond } from '../../middleware/respond.js';
14
14
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
15
15
  import { AuthenticationService } from '../../services/authentication.js';
16
- import { UsersService } from '../../services/users.js';
17
16
  import asyncHandler from '../../utils/async-handler.js';
18
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
19
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
@@ -22,10 +21,10 @@ import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js
22
21
  import { verifyJWT } from '../../utils/jwt.js';
23
22
  import { Url } from '../../utils/url.js';
24
23
  import { LocalAuthDriver } from './local.js';
24
+ import { getSchema } from '../../utils/get-schema.js';
25
25
  export class OpenIDAuthDriver extends LocalAuthDriver {
26
26
  client;
27
27
  redirectUrl;
28
- usersService;
29
28
  config;
30
29
  roleMap;
31
30
  constructor(options, config) {
@@ -40,7 +39,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
40
39
  }
41
40
  const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', provider, 'callback');
42
41
  this.redirectUrl = redirectUrl.toString();
43
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
44
42
  this.config = config;
45
43
  this.roleMap = {};
46
44
  const roleMapping = this.config['roleMapping'];
@@ -227,14 +225,16 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
227
225
  email: userPayload.email,
228
226
  };
229
227
  }
228
+ const schema = await getSchema();
230
229
  const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
231
230
  identifier,
232
231
  provider: this.config['provider'],
233
232
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
234
- }, { database: getDatabase(), schema: this.schema, accountability: null });
233
+ }, { database: getDatabase(), schema, accountability: null });
235
234
  // Update user to update refresh_token and other properties that might have changed
236
235
  if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
237
- await this.usersService.updateOne(userId, updatedUserPayload);
236
+ const usersService = this.getUsersService(schema);
237
+ await usersService.updateOne(userId, updatedUserPayload);
238
238
  }
239
239
  return userId;
240
240
  }
@@ -244,15 +244,17 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
244
244
  logger.warn(`[OpenID] User doesn't exist, and public registration not allowed for provider "${provider}"`);
245
245
  throw new InvalidCredentialsError();
246
246
  }
247
+ const schema = await getSchema();
247
248
  // Run hook so the end user has the chance to augment the
248
249
  // user that is about to be created
249
250
  const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
250
251
  identifier,
251
252
  provider: this.config['provider'],
252
253
  providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
253
- }, { database: getDatabase(), schema: this.schema, accountability: null });
254
+ }, { database: getDatabase(), schema, accountability: null });
254
255
  try {
255
- await this.usersService.createOne(updatedUserPayload);
256
+ const usersService = this.getUsersService(schema);
257
+ await usersService.createOne(updatedUserPayload);
256
258
  }
257
259
  catch (e) {
258
260
  if (isDirectusError(e, ErrorCode.RecordNotUnique)) {
@@ -283,7 +285,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
283
285
  const tokenSet = await client.refresh(authData['refreshToken']);
284
286
  // Update user refreshToken if provided
285
287
  if (tokenSet.refresh_token) {
286
- await this.usersService.updateOne(user.id, {
288
+ const usersService = this.getUsersService(await getSchema());
289
+ await usersService.updateOne(user.id, {
287
290
  auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
288
291
  });
289
292
  }
@@ -1,11 +1,9 @@
1
1
  import * as samlify from 'samlify';
2
- import { UsersService } from '../../services/users.js';
3
2
  import type { AuthDriverOptions, User } from '../../types/index.js';
4
3
  import { LocalAuthDriver } from './local.js';
5
4
  export declare class SAMLAuthDriver extends LocalAuthDriver {
6
5
  sp: samlify.ServiceProviderInstance;
7
6
  idp: samlify.IdentityProviderInstance;
8
- usersService: UsersService;
9
7
  config: Record<string, any>;
10
8
  constructor(options: AuthDriverOptions, config: Record<string, any>);
11
9
  fetchUserID(identifier: string): Promise<any>;
@@ -10,22 +10,20 @@ import emitter from '../../emitter.js';
10
10
  import { useLogger } from '../../logger/index.js';
11
11
  import { respond } from '../../middleware/respond.js';
12
12
  import { AuthenticationService } from '../../services/authentication.js';
13
- import { UsersService } from '../../services/users.js';
14
13
  import asyncHandler from '../../utils/async-handler.js';
15
14
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
16
15
  import { LocalAuthDriver } from './local.js';
17
16
  import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
17
+ import { getSchema } from '../../utils/get-schema.js';
18
18
  // Register the samlify schema validator
19
19
  samlify.setSchemaValidator(validator);
20
20
  export class SAMLAuthDriver extends LocalAuthDriver {
21
21
  sp;
22
22
  idp;
23
- usersService;
24
23
  config;
25
24
  constructor(options, config) {
26
25
  super(options, config);
27
26
  this.config = config;
28
- this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
29
27
  this.sp = samlify.ServiceProvider(getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_SP`));
30
28
  this.idp = samlify.IdentityProvider(getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_IDP`));
31
29
  }
@@ -63,11 +61,13 @@ export class SAMLAuthDriver extends LocalAuthDriver {
63
61
  external_identifier: identifier.toLowerCase(),
64
62
  role: this.config['defaultRoleId'],
65
63
  };
64
+ const schema = await getSchema();
66
65
  // Run hook so the end user has the chance to augment the
67
66
  // user that is about to be created
68
- const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: identifier.toLowerCase(), provider: this.config['provider'], providerPayload: { ...payload } }, { database: getDatabase(), schema: this.schema, accountability: null });
67
+ const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, { identifier: identifier.toLowerCase(), provider: this.config['provider'], providerPayload: { ...payload } }, { database: getDatabase(), schema, accountability: null });
69
68
  try {
70
- return await this.usersService.createOne(updatedUserPayload);
69
+ const usersService = this.getUsersService(schema);
70
+ return await usersService.createOne(updatedUserPayload);
71
71
  }
72
72
  catch (error) {
73
73
  if (isDirectusError(error, ErrorCode.RecordNotUnique)) {
package/dist/auth.js CHANGED
@@ -6,7 +6,6 @@ import { DEFAULT_AUTH_PROVIDER } from './constants.js';
6
6
  import getDatabase from './database/index.js';
7
7
  import { useLogger } from './logger/index.js';
8
8
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
9
- import { getSchema } from './utils/get-schema.js';
10
9
  const providers = new Map();
11
10
  export function getAuthProvider(provider) {
12
11
  const logger = useLogger();
@@ -19,7 +18,7 @@ export function getAuthProvider(provider) {
19
18
  export async function registerAuthProviders() {
20
19
  const env = useEnv();
21
20
  const logger = useLogger();
22
- const options = { knex: getDatabase(), schema: await getSchema() };
21
+ const options = { knex: getDatabase() };
23
22
  const providerNames = toArray(env['AUTH_PROVIDERS']);
24
23
  // Register default provider if not disabled
25
24
  if (!env['AUTH_DISABLE_DEFAULT']) {
@@ -3,13 +3,10 @@ import getDatabase, { hasDatabaseConnection, isInstalled, validateDatabaseConnec
3
3
  import runMigrations from '../../../database/migrations/run.js';
4
4
  import installDatabase from '../../../database/seeds/run.js';
5
5
  import { useLogger } from '../../../logger/index.js';
6
- import { AccessService } from '../../../services/access.js';
7
- import { PoliciesService } from '../../../services/policies.js';
8
- import { RolesService } from '../../../services/roles.js';
9
6
  import { SettingsService } from '../../../services/settings.js';
10
- import { UsersService } from '../../../services/users.js';
11
7
  import { getSchema } from '../../../utils/get-schema.js';
12
- import { defaultAdminPolicy, defaultAdminRole, defaultAdminUser } from '../../utils/defaults.js';
8
+ import { createAdmin } from '../../../utils/create-admin.js';
9
+ import { email } from 'zod';
13
10
  export default async function bootstrap({ skipAdminInit }) {
14
11
  const logger = useLogger();
15
12
  logger.info('Initializing bootstrap...');
@@ -23,15 +20,23 @@ export default async function bootstrap({ skipAdminInit }) {
23
20
  await runMigrations(database, 'latest');
24
21
  const schema = await getSchema();
25
22
  if (skipAdminInit == null) {
26
- await createDefaultAdmin(schema);
23
+ await createAdmin(schema);
27
24
  }
28
25
  else {
29
26
  logger.info('Skipping creation of default Admin user and role...');
30
27
  }
28
+ const settingsService = new SettingsService({ schema });
31
29
  if (env['PROJECT_NAME'] && typeof env['PROJECT_NAME'] === 'string' && env['PROJECT_NAME'].length > 0) {
32
- const settingsService = new SettingsService({ schema });
33
30
  await settingsService.upsertSingleton({ project_name: env['PROJECT_NAME'] });
34
31
  }
32
+ if (email().safeParse(env['PROJECT_OWNER']).success) {
33
+ await settingsService.setOwner({
34
+ project_owner: env['PROJECT_OWNER'],
35
+ org_name: null,
36
+ project_usage: null,
37
+ product_updates: false,
38
+ });
39
+ }
35
40
  }
36
41
  else {
37
42
  logger.info('Database already initialized, skipping install');
@@ -55,29 +60,3 @@ async function waitForDatabase(database) {
55
60
  await validateDatabaseConnection(database);
56
61
  return database;
57
62
  }
58
- async function createDefaultAdmin(schema) {
59
- const logger = useLogger();
60
- const env = useEnv();
61
- const { nanoid } = await import('nanoid');
62
- logger.info('Setting up first admin role...');
63
- const accessService = new AccessService({ schema });
64
- const policiesService = new PoliciesService({ schema });
65
- const rolesService = new RolesService({ schema });
66
- const role = await rolesService.createOne(defaultAdminRole);
67
- const policy = await policiesService.createOne(defaultAdminPolicy);
68
- await accessService.createOne({ policy, role });
69
- logger.info('Adding first admin user...');
70
- const usersService = new UsersService({ schema });
71
- let adminEmail = env['ADMIN_EMAIL'];
72
- if (!adminEmail) {
73
- logger.info('No admin email provided. Defaulting to "admin@example.com"');
74
- adminEmail = 'admin@example.com';
75
- }
76
- let adminPassword = env['ADMIN_PASSWORD'];
77
- if (!adminPassword) {
78
- adminPassword = nanoid(12);
79
- logger.info(`No admin password provided. Defaulting to "${adminPassword}"`);
80
- }
81
- const token = env['ADMIN_TOKEN'] ?? null;
82
- await usersService.createOne({ ...defaultAdminUser, email: adminEmail, password: adminPassword, token, role });
83
- }
@@ -9,7 +9,7 @@ import runSeed from '../../../database/seeds/run.js';
9
9
  import { generateHash } from '../../../utils/generate-hash.js';
10
10
  import createDBConnection from '../../utils/create-db-connection.js';
11
11
  import createEnv from '../../utils/create-env/index.js';
12
- import { defaultAdminPolicy, defaultAdminRole, defaultAdminUser } from '../../utils/defaults.js';
12
+ import { defaultAdminPolicy, defaultAdminRole, defaultAdminUser } from '../../../utils/create-admin.js';
13
13
  import { drivers, getDriverForClient } from '../../utils/drivers.js';
14
14
  import { databaseQuestions } from './questions.js';
15
15
  export default async function init() {
@@ -1,5 +1,9 @@
1
+ import type { SnapshotDiff } from '@directus/types';
2
+ export declare function filterSnapshotDiff(snapshot: SnapshotDiff, filters: string[]): SnapshotDiff;
1
3
  export declare function apply(snapshotPath: string, options?: {
2
4
  yes: boolean;
3
5
  dryRun: boolean;
4
6
  ignoreRules: string;
5
7
  }): Promise<void>;
8
+ export declare function formatPath(path: any[]): string;
9
+ export declare function formatRelatedCollection(relatedCollection: string | null): string;
@@ -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);