@directus/api 19.0.2 → 19.1.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 (70) hide show
  1. package/dist/app.js +8 -7
  2. package/dist/auth/drivers/oauth2.js +3 -2
  3. package/dist/auth/drivers/openid.js +3 -2
  4. package/dist/cli/utils/create-env/env-stub.liquid +0 -3
  5. package/dist/cli/utils/create-env/index.js +0 -2
  6. package/dist/controllers/auth.js +3 -2
  7. package/dist/controllers/extensions.js +30 -19
  8. package/dist/controllers/users.js +25 -0
  9. package/dist/database/helpers/fn/types.js +4 -3
  10. package/dist/database/helpers/index.d.ts +2 -0
  11. package/dist/database/helpers/index.js +2 -0
  12. package/dist/database/helpers/number/dialects/default.d.ts +3 -0
  13. package/dist/database/helpers/number/dialects/default.js +3 -0
  14. package/dist/database/helpers/number/dialects/mssql.d.ts +7 -0
  15. package/dist/database/helpers/number/dialects/mssql.js +11 -0
  16. package/dist/database/helpers/number/dialects/oracle.d.ts +6 -0
  17. package/dist/database/helpers/number/dialects/oracle.js +7 -0
  18. package/dist/database/helpers/number/dialects/postgres.d.ts +5 -0
  19. package/dist/database/helpers/number/dialects/postgres.js +15 -0
  20. package/dist/database/helpers/number/dialects/sqlite.d.ts +6 -0
  21. package/dist/database/helpers/number/dialects/sqlite.js +7 -0
  22. package/dist/database/helpers/number/index.d.ts +7 -0
  23. package/dist/database/helpers/number/index.js +7 -0
  24. package/dist/database/helpers/number/types.d.ts +12 -0
  25. package/dist/database/helpers/number/types.js +9 -0
  26. package/dist/database/helpers/number/utils/decimal-limit.d.ts +4 -0
  27. package/dist/database/helpers/number/utils/decimal-limit.js +10 -0
  28. package/dist/database/helpers/number/utils/maybe-stringify-big-int.d.ts +1 -0
  29. package/dist/database/helpers/number/utils/maybe-stringify-big-int.js +6 -0
  30. package/dist/database/helpers/number/utils/number-in-range.d.ts +3 -0
  31. package/dist/database/helpers/number/utils/number-in-range.js +20 -0
  32. package/dist/database/migrations/20240422A-public-registration.d.ts +3 -0
  33. package/dist/database/migrations/20240422A-public-registration.js +14 -0
  34. package/dist/database/run-ast.js +5 -4
  35. package/dist/extensions/lib/get-extensions-settings.js +48 -11
  36. package/dist/extensions/lib/installation/manager.js +2 -2
  37. package/dist/middleware/rate-limiter-global.js +1 -1
  38. package/dist/middleware/rate-limiter-registration.d.ts +5 -0
  39. package/dist/middleware/rate-limiter-registration.js +32 -0
  40. package/dist/services/authentication.js +3 -2
  41. package/dist/services/authorization.js +4 -4
  42. package/dist/services/fields.js +2 -2
  43. package/dist/services/graphql/index.js +41 -2
  44. package/dist/services/mail/templates/user-registration.liquid +37 -0
  45. package/dist/services/meta.js +1 -1
  46. package/dist/services/payload.d.ts +2 -0
  47. package/dist/services/payload.js +16 -4
  48. package/dist/services/server.js +3 -1
  49. package/dist/services/shares.js +2 -1
  50. package/dist/services/users.d.ts +3 -1
  51. package/dist/services/users.js +92 -5
  52. package/dist/utils/apply-query.d.ts +1 -1
  53. package/dist/utils/apply-query.js +54 -28
  54. package/dist/utils/get-accountability-for-token.js +6 -3
  55. package/dist/utils/get-secret.d.ts +4 -0
  56. package/dist/utils/get-secret.js +14 -0
  57. package/dist/utils/parse-filter-key.d.ts +7 -0
  58. package/dist/utils/parse-filter-key.js +22 -0
  59. package/dist/utils/parse-numeric-string.d.ts +2 -0
  60. package/dist/utils/parse-numeric-string.js +21 -0
  61. package/dist/utils/sanitize-query.js +10 -5
  62. package/dist/utils/transaction.d.ts +1 -1
  63. package/dist/utils/transaction.js +39 -2
  64. package/dist/utils/validate-query.js +0 -2
  65. package/dist/utils/verify-session-jwt.d.ts +7 -0
  66. package/dist/utils/verify-session-jwt.js +22 -0
  67. package/dist/websocket/messages.d.ts +78 -50
  68. package/package.json +60 -61
  69. package/dist/utils/strip-function.d.ts +0 -4
  70. package/dist/utils/strip-function.js +0 -12
package/dist/app.js CHANGED
@@ -58,20 +58,13 @@ import schema from './middleware/schema.js';
58
58
  import { initTelemetry } from './telemetry/index.js';
59
59
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
60
60
  import { Url } from './utils/url.js';
61
- import { validateEnv } from './utils/validate-env.js';
62
61
  import { validateStorage } from './utils/validate-storage.js';
63
62
  const require = createRequire(import.meta.url);
64
63
  export default async function createApp() {
65
64
  const env = useEnv();
66
65
  const logger = useLogger();
67
66
  const helmet = await import('helmet');
68
- validateEnv(['KEY', 'SECRET']);
69
- if (!new Url(env['PUBLIC_URL']).isAbsolute()) {
70
- logger.warn('PUBLIC_URL should be a full URL');
71
- }
72
- await validateStorage();
73
67
  await validateDatabaseConnection();
74
- await validateDatabaseExtensions();
75
68
  if ((await isInstalled()) === false) {
76
69
  logger.error(`Database doesn't have Directus tables installed.`);
77
70
  process.exit(1);
@@ -79,6 +72,14 @@ export default async function createApp() {
79
72
  if ((await validateMigrations()) === false) {
80
73
  logger.warn(`Database migrations have not all been run`);
81
74
  }
75
+ if (!env['SECRET']) {
76
+ logger.warn(`"SECRET" env variable is missing. Using a random value instead. Tokens will not persist between restarts. This is not appropriate for production usage.`);
77
+ }
78
+ if (!new Url(env['PUBLIC_URL']).isAbsolute()) {
79
+ logger.warn('"PUBLIC_URL" should be a full URL');
80
+ }
81
+ await validateDatabaseExtensions();
82
+ await validateStorage();
82
83
  await registerAuthProviders();
83
84
  const extensionManager = getExtensionManager();
84
85
  const flowManager = getFlowManager();
@@ -19,6 +19,7 @@ import { getIPFromReq } from '../../utils/get-ip-from-req.js';
19
19
  import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
20
20
  import { Url } from '../../utils/url.js';
21
21
  import { LocalAuthDriver } from './local.js';
22
+ import { getSecret } from '../../utils/get-secret.js';
22
23
  export class OAuth2AuthDriver extends LocalAuthDriver {
23
24
  client;
24
25
  redirectUrl;
@@ -224,7 +225,7 @@ export function createOAuth2AuthRouter(providerName) {
224
225
  if (isLoginRedirectAllowed(redirect, providerName) === false) {
225
226
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
226
227
  }
227
- const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'], {
228
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
228
229
  expiresIn: '5m',
229
230
  issuer: 'directus',
230
231
  });
@@ -241,7 +242,7 @@ export function createOAuth2AuthRouter(providerName) {
241
242
  const logger = useLogger();
242
243
  let tokenData;
243
244
  try {
244
- tokenData = jwt.verify(req.cookies[`oauth2.${providerName}`], env['SECRET'], {
245
+ tokenData = jwt.verify(req.cookies[`oauth2.${providerName}`], getSecret(), {
245
246
  issuer: 'directus',
246
247
  });
247
248
  }
@@ -16,6 +16,7 @@ import { UsersService } from '../../services/users.js';
16
16
  import asyncHandler from '../../utils/async-handler.js';
17
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
18
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
19
+ import { getSecret } from '../../utils/get-secret.js';
19
20
  import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
20
21
  import { Url } from '../../utils/url.js';
21
22
  import { LocalAuthDriver } from './local.js';
@@ -245,7 +246,7 @@ export function createOpenIDAuthRouter(providerName) {
245
246
  if (isLoginRedirectAllowed(redirect, providerName) === false) {
246
247
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
247
248
  }
248
- const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'], {
249
+ const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
249
250
  expiresIn: '5m',
250
251
  issuer: 'directus',
251
252
  });
@@ -262,7 +263,7 @@ export function createOpenIDAuthRouter(providerName) {
262
263
  const logger = useLogger();
263
264
  let tokenData;
264
265
  try {
265
- tokenData = jwt.verify(req.cookies[`openid.${providerName}`], env['SECRET'], {
266
+ tokenData = jwt.verify(req.cookies[`openid.${providerName}`], getSecret(), {
266
267
  issuer: 'directus',
267
268
  });
268
269
  }
@@ -195,9 +195,6 @@ STORAGE_LOCAL_ROOT="./uploads"
195
195
 
196
196
  {{ security }}
197
197
 
198
- # Unique identifier for the project
199
- # KEY="xxxxxxx-xxxxxx-xxxxxxxx-xxxxxxxxxx"
200
-
201
198
  # Secret string for the project
202
199
  # SECRET="abcdef"
203
200
 
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import { Liquid } from 'liquidjs';
3
- import { randomUUID } from 'node:crypto';
4
3
  import { dirname } from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import path from 'path';
@@ -17,7 +16,6 @@ export default async function createEnv(client, credentials, directory) {
17
16
  const { nanoid } = await import('nanoid');
18
17
  const config = {
19
18
  security: {
20
- KEY: randomUUID(),
21
19
  SECRET: nanoid(32),
22
20
  },
23
21
  database: {
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
2
2
  import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
3
  import { Router } from 'express';
4
4
  import { createLDAPAuthRouter, createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter, createSAMLAuthRouter, } from '../auth/drivers/index.js';
5
- import { REFRESH_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, SESSION_COOKIE_OPTIONS } from '../constants.js';
5
+ import { DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../constants.js';
6
6
  import { useLogger } from '../logger.js';
7
7
  import { respond } from '../middleware/respond.js';
8
8
  import { AuthenticationService } from '../services/authentication.js';
@@ -10,6 +10,7 @@ import { UsersService } from '../services/users.js';
10
10
  import asyncHandler from '../utils/async-handler.js';
11
11
  import { getAuthProviders } from '../utils/get-auth-providers.js';
12
12
  import { getIPFromReq } from '../utils/get-ip-from-req.js';
13
+ import { getSecret } from '../utils/get-secret.js';
13
14
  import isDirectusJWT from '../utils/is-directus-jwt.js';
14
15
  import { verifyAccessJWT } from '../utils/jwt.js';
15
16
  const router = Router();
@@ -63,7 +64,7 @@ function getCurrentRefreshToken(req, mode) {
63
64
  if (mode === 'session') {
64
65
  const token = req.cookies[env['SESSION_COOKIE_NAME']];
65
66
  if (isDirectusJWT(token)) {
66
- const payload = verifyAccessJWT(token, env['SECRET']);
67
+ const payload = verifyAccessJWT(token, getSecret());
67
68
  return payload.session;
68
69
  }
69
70
  }
@@ -1,9 +1,10 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from '@directus/errors';
2
+ import { ErrorCode, ForbiddenError, isDirectusError, RouteNotFoundError } from '@directus/errors';
3
3
  import { EXTENSION_TYPES } from '@directus/extensions';
4
4
  import { account, describe, list, } from '@directus/extensions-registry';
5
5
  import { isIn } from '@directus/utils';
6
6
  import express from 'express';
7
+ import { isNil } from 'lodash-es';
7
8
  import { UUID_REGEX } from '../constants.js';
8
9
  import { getExtensionManager } from '../extensions/index.js';
9
10
  import { respond } from '../middleware/respond.js';
@@ -28,28 +29,38 @@ router.get('/registry', asyncHandler(async (req, res, next) => {
28
29
  if (req.accountability && req.accountability.admin !== true) {
29
30
  throw new ForbiddenError();
30
31
  }
31
- const { search, limit, offset, type, by, sort } = req.query;
32
+ const { search, limit, offset, sort, filter } = req.sanitizedQuery;
32
33
  const query = {};
33
- if (typeof search === 'string') {
34
+ if (!isNil(search)) {
34
35
  query.search = search;
35
36
  }
36
- if (typeof limit === 'string') {
37
- query.limit = Number(limit);
38
- }
39
- if (typeof offset === 'string') {
40
- query.offset = Number(offset);
41
- }
42
- if (typeof by === 'string') {
43
- query.by = by;
44
- }
45
- if (typeof sort === 'string' && isIn(sort, ['popular', 'recent', 'downloads'])) {
46
- query.sort = sort;
47
- }
48
- if (typeof type === 'string') {
49
- if (isIn(type, EXTENSION_TYPES) === false) {
50
- throw new ForbiddenError();
37
+ if (!isNil(limit)) {
38
+ query.limit = limit;
39
+ }
40
+ if (!isNil(offset)) {
41
+ query.offset = offset;
42
+ }
43
+ if (filter) {
44
+ const getFilterValue = (key) => {
45
+ const field = filter[key];
46
+ if (!field || !('_eq' in field) || typeof field._eq !== 'string')
47
+ return;
48
+ return field._eq;
49
+ };
50
+ const by = getFilterValue('by');
51
+ const type = getFilterValue('type');
52
+ if (by) {
53
+ query.by = by;
51
54
  }
52
- query.type = type;
55
+ if (type) {
56
+ if (isIn(type, EXTENSION_TYPES) === false) {
57
+ throw new ForbiddenError();
58
+ }
59
+ query.type = type;
60
+ }
61
+ }
62
+ if (!isNil(sort) && sort[0] && isIn(sort[0], ['popular', 'recent', 'downloads'])) {
63
+ query.sort = sort[0];
53
64
  }
54
65
  if (env['MARKETPLACE_TRUST'] === 'sandbox') {
55
66
  query.sandbox = true;
@@ -1,6 +1,7 @@
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 checkRateLimit from '../middleware/rate-limiter-registration.js';
4
5
  import { respond } from '../middleware/respond.js';
5
6
  import useCollection from '../middleware/use-collection.js';
6
7
  import { validateBatch } from '../middleware/validate-batch.js';
@@ -354,4 +355,28 @@ router.post('/:pk/tfa/disable', asyncHandler(async (req, _res, next) => {
354
355
  await service.disableTFA(req.params['pk']);
355
356
  return next();
356
357
  }), respond);
358
+ const registerSchema = Joi.object({
359
+ email: Joi.string().email().required(),
360
+ password: Joi.string().required(),
361
+ first_name: Joi.string(),
362
+ last_name: Joi.string(),
363
+ });
364
+ router.post('/register', checkRateLimit, asyncHandler(async (req, _res, next) => {
365
+ const { error, value } = registerSchema.validate(req.body);
366
+ if (error)
367
+ throw new InvalidPayloadError({ reason: error.message });
368
+ const usersService = new UsersService({ accountability: null, schema: req.schema });
369
+ await usersService.registerUser(value);
370
+ return next();
371
+ }), respond);
372
+ const verifyRegistrationSchema = Joi.string();
373
+ router.get('/register/verify-email', asyncHandler(async (req, res, _next) => {
374
+ const { error, value } = verifyRegistrationSchema.validate(req.query['token']);
375
+ if (error) {
376
+ return res.redirect('/admin/login');
377
+ }
378
+ const service = new UsersService({ accountability: null, schema: req.schema });
379
+ const id = await service.verifyRegistration(value);
380
+ return res.redirect(`/admin/users/${id}`);
381
+ }), respond);
357
382
  export default router;
@@ -1,4 +1,4 @@
1
- import { applyFilter } from '../../../utils/apply-query.js';
1
+ import { applyFilter, generateAlias } from '../../../utils/apply-query.js';
2
2
  import { DatabaseHelper } from '../types.js';
3
3
  export class FnHelper extends DatabaseHelper {
4
4
  schema;
@@ -14,10 +14,11 @@ export class FnHelper extends DatabaseHelper {
14
14
  if (!relation) {
15
15
  throw new Error(`Field ${collectionName}.${column} isn't a nested relational collection`);
16
16
  }
17
+ const alias = generateAlias();
17
18
  let countQuery = this.knex
18
19
  .count('*')
19
- .from(relation.collection)
20
- .where(relation.field, '=', this.knex.raw(`??.??`, [table, currentPrimary]));
20
+ .from({ [alias]: relation.collection })
21
+ .where(this.knex.raw(`??.??`, [alias, relation.field]), '=', this.knex.raw(`??.??`, [table, currentPrimary]));
21
22
  if (options?.query?.filter) {
22
23
  countQuery = applyFilter(this.knex, this.schema, countQuery, options.query.filter, relation.collection, {}).query;
23
24
  }
@@ -5,11 +5,13 @@ import * as fnHelpers from './fn/index.js';
5
5
  import * as geometryHelpers from './geometry/index.js';
6
6
  import * as schemaHelpers from './schema/index.js';
7
7
  import * as sequenceHelpers from './sequence/index.js';
8
+ import * as numberHelpers from './number/index.js';
8
9
  export declare function getHelpers(database: Knex): {
9
10
  date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
10
11
  st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
11
12
  schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle;
12
13
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
14
+ number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
13
15
  };
14
16
  export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.mysql | fnHelpers.postgres | fnHelpers.mssql | fnHelpers.sqlite | fnHelpers.oracle;
15
17
  export type Helpers = ReturnType<typeof getHelpers>;
@@ -4,6 +4,7 @@ import * as fnHelpers from './fn/index.js';
4
4
  import * as geometryHelpers from './geometry/index.js';
5
5
  import * as schemaHelpers from './schema/index.js';
6
6
  import * as sequenceHelpers from './sequence/index.js';
7
+ import * as numberHelpers from './number/index.js';
7
8
  export function getHelpers(database) {
8
9
  const client = getDatabaseClient(database);
9
10
  return {
@@ -11,6 +12,7 @@ export function getHelpers(database) {
11
12
  st: new geometryHelpers[client](database),
12
13
  schema: new schemaHelpers[client](database),
13
14
  sequence: new sequenceHelpers[client](database),
15
+ number: new numberHelpers[client](database),
14
16
  };
15
17
  }
16
18
  export function getFunctions(database, schema) {
@@ -0,0 +1,3 @@
1
+ import { NumberDatabaseHelper } from '../types.js';
2
+ export declare class NumberHelperDefault extends NumberDatabaseHelper {
3
+ }
@@ -0,0 +1,3 @@
1
+ import { NumberDatabaseHelper } from '../types.js';
2
+ export class NumberHelperDefault extends NumberDatabaseHelper {
3
+ }
@@ -0,0 +1,7 @@
1
+ import type { Knex } from 'knex';
2
+ import { NumberDatabaseHelper, type NumberInfo } from '../types.js';
3
+ import type { NumericValue } from '@directus/types';
4
+ export declare class NumberHelperMSSQL extends NumberDatabaseHelper {
5
+ addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
6
+ isNumberValid(value: NumericValue, info: NumberInfo): boolean;
7
+ }
@@ -0,0 +1,11 @@
1
+ import { maybeStringifyBigInt } from '../utils/maybe-stringify-big-int.js';
2
+ import { numberInRange } from '../utils/number-in-range.js';
3
+ import { NumberDatabaseHelper } from '../types.js';
4
+ export class NumberHelperMSSQL extends NumberDatabaseHelper {
5
+ addSearchCondition(dbQuery, collection, name, value) {
6
+ return dbQuery.orWhere({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
7
+ }
8
+ isNumberValid(value, info) {
9
+ return numberInRange(value, info);
10
+ }
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { Knex } from 'knex';
2
+ import { NumberDatabaseHelper } from '../types.js';
3
+ import type { NumericValue } from '@directus/types';
4
+ export declare class NumberHelperOracle extends NumberDatabaseHelper {
5
+ addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
6
+ }
@@ -0,0 +1,7 @@
1
+ import { NumberDatabaseHelper } from '../types.js';
2
+ import { maybeStringifyBigInt } from '../utils/maybe-stringify-big-int.js';
3
+ export class NumberHelperOracle extends NumberDatabaseHelper {
4
+ addSearchCondition(dbQuery, collection, name, value) {
5
+ return dbQuery.orWhere({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ import type { NumericValue } from '@directus/types';
2
+ import { NumberDatabaseHelper, type NumberInfo } from '../types.js';
3
+ export declare class NumberHelperPostgres extends NumberDatabaseHelper {
4
+ isNumberValid(value: NumericValue, info: NumberInfo): boolean;
5
+ }
@@ -0,0 +1,15 @@
1
+ import { NumberDatabaseHelper } from '../types.js';
2
+ import { numberInRange } from '../utils/number-in-range.js';
3
+ export class NumberHelperPostgres extends NumberDatabaseHelper {
4
+ isNumberValid(value, info) {
5
+ // Check that number is within the range of the provided type
6
+ if (numberInRange(value, info)) {
7
+ // Ensure that only integer values are used for integer types
8
+ if (typeof value !== 'bigint' && ['integer', 'bigInteger'].includes(info.type)) {
9
+ return value % 1 === 0;
10
+ }
11
+ return true;
12
+ }
13
+ return false;
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ import type { Knex } from 'knex';
2
+ import { NumberDatabaseHelper } from '../types.js';
3
+ import type { NumericValue } from '@directus/types';
4
+ export declare class NumberHelperSQLite extends NumberDatabaseHelper {
5
+ addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
6
+ }
@@ -0,0 +1,7 @@
1
+ import { NumberDatabaseHelper } from '../types.js';
2
+ import { maybeStringifyBigInt } from '../utils/maybe-stringify-big-int.js';
3
+ export class NumberHelperSQLite extends NumberDatabaseHelper {
4
+ addSearchCondition(dbQuery, collection, name, value) {
5
+ return dbQuery.orWhere({ [`${collection}.${name}`]: maybeStringifyBigInt(value) });
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ export { NumberHelperPostgres as postgres } from './dialects/postgres.js';
2
+ export { NumberHelperPostgres as redshift } from './dialects/postgres.js';
3
+ export { NumberHelperDefault as cockroachdb } from './dialects/default.js';
4
+ export { NumberHelperOracle as oracle } from './dialects/oracle.js';
5
+ export { NumberHelperSQLite as sqlite } from './dialects/sqlite.js';
6
+ export { NumberHelperDefault as mysql } from './dialects/default.js';
7
+ export { NumberHelperMSSQL as mssql } from './dialects/mssql.js';
@@ -0,0 +1,7 @@
1
+ export { NumberHelperPostgres as postgres } from './dialects/postgres.js';
2
+ export { NumberHelperPostgres as redshift } from './dialects/postgres.js';
3
+ export { NumberHelperDefault as cockroachdb } from './dialects/default.js';
4
+ export { NumberHelperOracle as oracle } from './dialects/oracle.js';
5
+ export { NumberHelperSQLite as sqlite } from './dialects/sqlite.js';
6
+ export { NumberHelperDefault as mysql } from './dialects/default.js';
7
+ export { NumberHelperMSSQL as mssql } from './dialects/mssql.js';
@@ -0,0 +1,12 @@
1
+ import type { Knex } from 'knex';
2
+ import { DatabaseHelper } from '../types.js';
3
+ import type { NumericType, NumericValue } from '@directus/types';
4
+ export type NumberInfo = {
5
+ type: NumericType;
6
+ precision: number | null;
7
+ scale: number | null;
8
+ };
9
+ export declare abstract class NumberDatabaseHelper extends DatabaseHelper {
10
+ addSearchCondition(dbQuery: Knex.QueryBuilder, collection: string, name: string, value: NumericValue): Knex.QueryBuilder;
11
+ isNumberValid(_value: NumericValue, _info: NumberInfo): boolean;
12
+ }
@@ -0,0 +1,9 @@
1
+ import { DatabaseHelper } from '../types.js';
2
+ export class NumberDatabaseHelper extends DatabaseHelper {
3
+ addSearchCondition(dbQuery, collection, name, value) {
4
+ return dbQuery.orWhere({ [`${collection}.${name}`]: value });
5
+ }
6
+ isNumberValid(_value, _info) {
7
+ return true;
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ export declare function calculateDecimalLimit(precision: number | null, scale: number | null): {
2
+ max: number;
3
+ min: number;
4
+ };
@@ -0,0 +1,10 @@
1
+ import { DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE } from '@directus/constants';
2
+ export function calculateDecimalLimit(precision, scale) {
3
+ if (precision === null || scale === null) {
4
+ precision = DEFAULT_NUMERIC_PRECISION;
5
+ scale = DEFAULT_NUMERIC_SCALE;
6
+ }
7
+ const max = 10 ** (precision - scale) - 10 ** -scale;
8
+ const min = -(10 ** (precision - scale)) + 10 ** -scale;
9
+ return { max, min };
10
+ }
@@ -0,0 +1 @@
1
+ export declare function maybeStringifyBigInt(value: number | bigint): string | number;
@@ -0,0 +1,6 @@
1
+ export function maybeStringifyBigInt(value) {
2
+ if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
3
+ return String(value);
4
+ }
5
+ return Number(value);
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { NumericValue } from '@directus/types';
2
+ import type { NumberInfo } from '../types.js';
3
+ export declare function numberInRange(value: NumericValue, info: NumberInfo): boolean;
@@ -0,0 +1,20 @@
1
+ import { MAX_SAFE_INT32, MAX_SAFE_INT64, MIN_SAFE_INT32, MIN_SAFE_INT64 } from '@directus/constants';
2
+ import { calculateDecimalLimit } from './decimal-limit.js';
3
+ export function numberInRange(value, info) {
4
+ switch (info.type) {
5
+ case 'bigInteger':
6
+ return value >= MIN_SAFE_INT64 && value <= MAX_SAFE_INT64;
7
+ case 'decimal': {
8
+ const { min, max } = calculateDecimalLimit(info.precision, info.scale);
9
+ return value >= min && value <= max;
10
+ }
11
+ case 'integer':
12
+ return value >= MIN_SAFE_INT32 && value <= MAX_SAFE_INT32;
13
+ case 'float':
14
+ // Not sure how to calculate the logical limits of float
15
+ // Let the database decide and error;
16
+ return true;
17
+ default:
18
+ return false;
19
+ }
20
+ }
@@ -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>;
@@ -0,0 +1,14 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_settings', (table) => {
3
+ table.boolean('public_registration').notNullable().defaultTo(false);
4
+ table.boolean('public_registration_verify_email').notNullable().defaultTo(true);
5
+ table.uuid('public_registration_role').nullable();
6
+ table.foreign('public_registration_role').references('directus_roles.id').onDelete('SET NULL');
7
+ table.json('public_registration_email_filter').nullable();
8
+ });
9
+ }
10
+ export async function down(knex) {
11
+ await knex.schema.alterTable('directus_settings', (table) => {
12
+ table.dropColumns('public_registration', 'public_registration_verify_email', 'public_registration_role', 'public_registration_email_filter');
13
+ });
14
+ }
@@ -6,7 +6,7 @@ import { applyFunctionToColumnName } from '../utils/apply-function-to-column-nam
6
6
  import applyQuery, { applyLimit, applySort, generateAlias } from '../utils/apply-query.js';
7
7
  import { getCollectionFromAlias } from '../utils/get-collection-from-alias.js';
8
8
  import { getColumn } from '../utils/get-column.js';
9
- import { stripFunction } from '../utils/strip-function.js';
9
+ import { parseFilterKey } from '../utils/parse-filter-key.js';
10
10
  import { getHelpers } from './helpers/index.js';
11
11
  import getDatabase from './index.js';
12
12
  /**
@@ -36,7 +36,7 @@ export default async function runAST(originalAST, schema, options) {
36
36
  return null;
37
37
  // Run the items through the special transforms
38
38
  const payloadService = new PayloadService(collection, { knex, schema });
39
- let items = await payloadService.processValues('read', rawItems);
39
+ let items = await payloadService.processValues('read', rawItems, query.alias ?? {});
40
40
  if (!items || (Array.isArray(items) && items.length === 0))
41
41
  return items;
42
42
  // Apply the `_in` filters to the nested collection batches
@@ -92,7 +92,7 @@ async function parseCurrentLevel(schema, collection, children, query) {
92
92
  const nestedCollectionNodes = [];
93
93
  for (const child of children) {
94
94
  if (child.type === 'field' || child.type === 'functionField') {
95
- const fieldName = stripFunction(child.name);
95
+ const { fieldName } = parseFilterKey(child.name);
96
96
  if (columnsInCollection.includes(fieldName)) {
97
97
  columnsToSelectInternal.push(child.fieldKey);
98
98
  }
@@ -134,7 +134,8 @@ function getColumnPreprocessor(knex, schema, table) {
134
134
  }
135
135
  let field;
136
136
  if (fieldNode.type === 'field' || fieldNode.type === 'functionField') {
137
- field = schema.collections[table].fields[stripFunction(fieldNode.name)];
137
+ const { fieldName } = parseFilterKey(fieldNode.name);
138
+ field = schema.collections[table].fields[fieldName];
138
139
  }
139
140
  else {
140
141
  field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];