@directus/api 25.0.1 → 26.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 (115) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/auth/drivers/oauth2.d.ts +2 -0
  3. package/dist/auth/drivers/oauth2.js +40 -2
  4. package/dist/auth/drivers/openid.js +8 -1
  5. package/dist/controllers/access.js +2 -2
  6. package/dist/controllers/comments.js +2 -2
  7. package/dist/controllers/dashboards.js +2 -2
  8. package/dist/controllers/files.js +2 -2
  9. package/dist/controllers/flows.js +2 -2
  10. package/dist/controllers/folders.js +2 -2
  11. package/dist/controllers/items.js +2 -2
  12. package/dist/controllers/notifications.js +2 -2
  13. package/dist/controllers/operations.js +2 -2
  14. package/dist/controllers/panels.js +2 -2
  15. package/dist/controllers/permissions.js +2 -2
  16. package/dist/controllers/policies.js +2 -2
  17. package/dist/controllers/presets.js +2 -2
  18. package/dist/controllers/roles.js +2 -2
  19. package/dist/controllers/shares.js +2 -2
  20. package/dist/controllers/translations.js +2 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/controllers/utils.js +8 -3
  23. package/dist/controllers/versions.js +2 -2
  24. package/dist/controllers/webhooks.js +1 -1
  25. package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
  26. package/dist/database/helpers/capabilities/dialects/default.js +3 -0
  27. package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
  28. package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
  29. package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
  30. package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
  31. package/dist/database/helpers/capabilities/index.d.ts +7 -0
  32. package/dist/database/helpers/capabilities/index.js +7 -0
  33. package/dist/database/helpers/capabilities/types.d.ts +11 -0
  34. package/dist/database/helpers/capabilities/types.js +15 -0
  35. package/dist/database/helpers/index.d.ts +2 -0
  36. package/dist/database/helpers/index.js +2 -0
  37. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
  38. package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
  39. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
  40. package/dist/database/helpers/schema/dialects/postgres.js +0 -4
  41. package/dist/database/index.js +1 -1
  42. package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
  43. package/dist/database/migrations/20250224A-visual-editor.js +35 -0
  44. package/dist/database/run-ast/lib/get-db-query.js +16 -4
  45. package/dist/logger/index.js +3 -3
  46. package/dist/middleware/sanitize-query.js +17 -7
  47. package/dist/middleware/validate-batch.js +1 -1
  48. package/dist/operations/item-delete/index.js +1 -1
  49. package/dist/operations/item-read/index.js +1 -1
  50. package/dist/operations/item-update/index.js +1 -1
  51. package/dist/permissions/lib/fetch-permissions.js +6 -4
  52. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
  53. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
  54. package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
  55. package/dist/permissions/modules/process-payload/process-payload.js +13 -4
  56. package/dist/permissions/types.d.ts +2 -1
  57. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
  58. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
  59. package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
  60. package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +11 -12
  61. package/dist/rate-limiter.js +1 -1
  62. package/dist/services/assets.js +12 -2
  63. package/dist/services/authentication.js +2 -2
  64. package/dist/services/collections.js +8 -2
  65. package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
  66. package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
  67. package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
  68. package/dist/services/graphql/resolvers/get-field-type.js +51 -0
  69. package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
  70. package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
  71. package/dist/services/graphql/resolvers/mutation.js +1 -1
  72. package/dist/services/graphql/resolvers/query.js +4 -4
  73. package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
  74. package/dist/services/graphql/resolvers/system-admin.js +207 -199
  75. package/dist/services/graphql/resolvers/system.d.ts +1 -7
  76. package/dist/services/graphql/resolvers/system.js +12 -113
  77. package/dist/services/graphql/schema/index.js +1 -1
  78. package/dist/services/graphql/schema/parse-query.d.ts +2 -2
  79. package/dist/services/graphql/schema/parse-query.js +6 -6
  80. package/dist/services/graphql/schema/read.d.ts +2 -2
  81. package/dist/services/graphql/schema/read.js +86 -2
  82. package/dist/services/graphql/schema-cache.d.ts +2 -2
  83. package/dist/services/graphql/schema-cache.js +1 -3
  84. package/dist/services/graphql/subscription.d.ts +3 -3
  85. package/dist/services/graphql/subscription.js +3 -3
  86. package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
  87. package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
  88. package/dist/services/items.d.ts +1 -0
  89. package/dist/services/items.js +30 -16
  90. package/dist/services/payload.d.ts +1 -0
  91. package/dist/services/payload.js +32 -17
  92. package/dist/services/shares.js +1 -1
  93. package/dist/services/specifications.js +10 -5
  94. package/dist/services/tus/lockers.d.ts +1 -1
  95. package/dist/services/tus/lockers.js +6 -5
  96. package/dist/services/tus/server.js +24 -0
  97. package/dist/services/users.js +1 -0
  98. package/dist/types/services.d.ts +2 -0
  99. package/dist/utils/apply-query.d.ts +1 -0
  100. package/dist/utils/apply-query.js +42 -31
  101. package/dist/utils/generate-hash.js +1 -1
  102. package/dist/utils/get-config-from-env.d.ts +6 -1
  103. package/dist/utils/get-config-from-env.js +16 -11
  104. package/dist/utils/get-graphql-type.js +3 -1
  105. package/dist/utils/is-login-redirect-allowed.js +2 -0
  106. package/dist/utils/redact-object.js +5 -1
  107. package/dist/utils/sanitize-query.d.ts +5 -2
  108. package/dist/utils/sanitize-query.js +34 -9
  109. package/dist/websocket/controllers/base.d.ts +2 -2
  110. package/dist/websocket/handlers/items.js +4 -4
  111. package/dist/websocket/handlers/subscribe.js +2 -2
  112. package/dist/websocket/messages.d.ts +7 -7
  113. package/dist/websocket/messages.js +1 -1
  114. package/package.json +60 -60
  115. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
package/dist/app.js CHANGED
@@ -94,7 +94,7 @@ export default async function createApp() {
94
94
  const app = express();
95
95
  app.disable('x-powered-by');
96
96
  app.set('trust proxy', env['IP_TRUST_PROXY']);
97
- app.set('query parser', (str) => qs.parse(str, { depth: 10 }));
97
+ app.set('query parser', (str) => qs.parse(str, { depth: Number(env['QUERYSTRING_MAX_PARSE_DEPTH']) }));
98
98
  if (env['PRESSURE_LIMITER_ENABLED']) {
99
99
  const sampleInterval = Number(env['PRESSURE_LIMITER_SAMPLE_INTERVAL']);
100
100
  if (Number.isNaN(sampleInterval) === true || Number.isFinite(sampleInterval) === false) {
@@ -134,7 +134,7 @@ export default async function createApp() {
134
134
  },
135
135
  }, getConfigFromEnv('CONTENT_SECURITY_POLICY_'))));
136
136
  if (env['HSTS_ENABLED']) {
137
- app.use(helmet.hsts(getConfigFromEnv('HSTS_', ['HSTS_ENABLED'])));
137
+ app.use(helmet.hsts(getConfigFromEnv('HSTS_', { omitPrefix: 'HSTS_ENABLED' })));
138
138
  }
139
139
  await emitter.emitInit('app.before', { app });
140
140
  await emitter.emitInit('middlewares.before', { app });
@@ -203,9 +203,9 @@ export default async function createApp() {
203
203
  }
204
204
  app.get('/server/ping', (_req, res) => res.send('pong'));
205
205
  app.use(authenticate);
206
+ app.use(schema);
206
207
  app.use(sanitizeQuery);
207
208
  app.use(cache);
208
- app.use(schema);
209
209
  await emitter.emitInit('middlewares.after', { app });
210
210
  await emitter.emitInit('routes.before', { app });
211
211
  app.use('/auth', authRouter);
@@ -2,12 +2,14 @@ import { Router } from 'express';
2
2
  import type { Client } from 'openid-client';
3
3
  import { UsersService } from '../../services/users.js';
4
4
  import type { AuthDriverOptions, User } from '../../types/index.js';
5
+ import type { RoleMap } from '../../types/rolemap.js';
5
6
  import { LocalAuthDriver } from './local.js';
6
7
  export declare class OAuth2AuthDriver extends LocalAuthDriver {
7
8
  client: Client;
8
9
  redirectUrl: string;
9
10
  usersService: UsersService;
10
11
  config: Record<string, any>;
12
+ roleMap: RoleMap;
11
13
  constructor(options: AuthDriverOptions, config: Record<string, any>);
12
14
  generateCodeVerifier(): string;
13
15
  generateAuthUrl(codeVerifier: string, prompt?: boolean): string;
@@ -27,6 +27,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
27
27
  redirectUrl;
28
28
  usersService;
29
29
  config;
30
+ roleMap;
30
31
  constructor(options, config) {
31
32
  super(options, config);
32
33
  const env = useEnv();
@@ -40,13 +41,31 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
40
41
  this.redirectUrl = redirectUrl.toString();
41
42
  this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
42
43
  this.config = additionalConfig;
44
+ this.roleMap = {};
45
+ const roleMapping = this.config['roleMapping'];
46
+ if (roleMapping) {
47
+ this.roleMap = roleMapping;
48
+ }
49
+ // role mapping will fail on login if AUTH_<provider>_ROLE_MAPPING is an array instead of an object.
50
+ // This happens if the 'json:' prefix is missing from the variable declaration. To save the user from exhaustive debugging, we'll try to fail early here.
51
+ if (roleMapping instanceof Array) {
52
+ logger.error("[OAuth2] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.");
53
+ throw new InvalidProviderError();
54
+ }
43
55
  const issuer = new Issuer({
44
56
  authorization_endpoint: authorizeUrl,
45
57
  token_endpoint: accessUrl,
46
58
  userinfo_endpoint: profileUrl,
47
59
  issuer: additionalConfig['provider'],
48
60
  });
49
- const clientOptionsOverrides = getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_CLIENT_`, [`AUTH_${config['provider'].toUpperCase()}_CLIENT_ID`, `AUTH_${config['provider'].toUpperCase()}_CLIENT_SECRET`], 'underscore');
61
+ // extract client overrides/options excluding CLIENT_ID and CLIENT_SECRET as they are passed directly
62
+ const clientOptionsOverrides = getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_CLIENT_`, {
63
+ omitKey: [
64
+ `AUTH_${config['provider'].toUpperCase()}_CLIENT_ID`,
65
+ `AUTH_${config['provider'].toUpperCase()}_CLIENT_SECRET`,
66
+ ],
67
+ type: 'underscore',
68
+ });
50
69
  this.client = new issuer.Client({
51
70
  client_id: clientId,
52
71
  client_secret: clientSecret,
@@ -105,6 +124,21 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
105
124
  catch (e) {
106
125
  throw handleError(e);
107
126
  }
127
+ let role = this.config['defaultRoleId'];
128
+ const groupClaimName = this.config['groupClaimName'] ?? 'groups';
129
+ const groups = userInfo[groupClaimName];
130
+ if (Array.isArray(groups)) {
131
+ for (const key in this.roleMap) {
132
+ if (groups.includes(key)) {
133
+ // Overwrite default role if user is member of a group specified in roleMap
134
+ role = this.roleMap[key];
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ else {
140
+ logger.debug(`[OAuth2] Configured group claim with name "${groupClaimName}" does not exist or is empty.`);
141
+ }
108
142
  // Flatten response to support dot indexes
109
143
  userInfo = flatten(userInfo);
110
144
  const { provider, emailKey, identifierKey, allowPublicRegistration, syncUserInfo } = this.config;
@@ -121,7 +155,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
121
155
  last_name: userInfo[this.config['lastNameKey']],
122
156
  email: email,
123
157
  external_identifier: identifier,
124
- role: this.config['defaultRoleId'],
158
+ role: role,
125
159
  auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
126
160
  };
127
161
  const userId = await this.fetchUserId(identifier);
@@ -131,6 +165,10 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
131
165
  let emitPayload = {
132
166
  auth_data: userPayload.auth_data,
133
167
  };
168
+ // Make sure a user's role gets updated if their oauth group or role mapping changes
169
+ if (this.config['roleMapping']) {
170
+ emitPayload['role'] = role;
171
+ }
134
172
  if (syncUserInfo) {
135
173
  emitPayload = {
136
174
  ...emitPayload,
@@ -38,7 +38,14 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
38
38
  throw new InvalidProviderConfigError({ provider: additionalConfig['provider'] });
39
39
  }
40
40
  const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', additionalConfig['provider'], 'callback');
41
- const clientOptionsOverrides = getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_CLIENT_`, [`AUTH_${config['provider'].toUpperCase()}_CLIENT_ID`, `AUTH_${config['provider'].toUpperCase()}_CLIENT_SECRET`], 'underscore');
41
+ // extract client overrides/options excluding CLIENT_ID and CLIENT_SECRET as they are passed directly
42
+ const clientOptionsOverrides = getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_CLIENT_`, {
43
+ omitKey: [
44
+ `AUTH_${config['provider'].toUpperCase()}_CLIENT_ID`,
45
+ `AUTH_${config['provider'].toUpperCase()}_CLIENT_SECRET`,
46
+ ],
47
+ type: 'underscore',
48
+ });
42
49
  this.redirectUrl = redirectUrl.toString();
43
50
  this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
44
51
  this.config = additionalConfig;
@@ -87,7 +87,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
87
87
  keys = await service.updateMany(req.body.keys, req.body.data);
88
88
  }
89
89
  else {
90
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
90
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
91
91
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
92
92
  }
93
93
  try {
@@ -132,7 +132,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
132
132
  await service.deleteMany(req.body.keys);
133
133
  }
134
134
  else {
135
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
135
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
136
136
  await service.deleteByQuery(sanitizedQuery);
137
137
  }
138
138
  return next();
@@ -85,7 +85,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
85
85
  keys = await service.updateMany(req.body.keys, req.body.data);
86
86
  }
87
87
  else {
88
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
88
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
89
89
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
90
90
  }
91
91
  try {
@@ -130,7 +130,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
130
130
  await service.deleteMany(req.body.keys);
131
131
  }
132
132
  else {
133
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
133
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
134
134
  await service.deleteByQuery(sanitizedQuery);
135
135
  }
136
136
  return next();
@@ -79,7 +79,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
79
79
  keys = await service.updateMany(req.body.keys, req.body.data);
80
80
  }
81
81
  else {
82
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
82
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
83
83
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
84
84
  }
85
85
  try {
@@ -124,7 +124,7 @@ router.delete('/', asyncHandler(async (req, _res, next) => {
124
124
  await service.deleteMany(req.body.keys);
125
125
  }
126
126
  else {
127
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
127
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
128
128
  await service.deleteByQuery(sanitizedQuery);
129
129
  }
130
130
  return next();
@@ -220,7 +220,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
220
220
  keys = await service.updateMany(req.body.keys, req.body.data);
221
221
  }
222
222
  else {
223
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
223
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
224
224
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
225
225
  }
226
226
  try {
@@ -265,7 +265,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
265
265
  await service.deleteMany(req.body.keys);
266
266
  }
267
267
  else {
268
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
268
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
269
269
  await service.deleteByQuery(sanitizedQuery);
270
270
  }
271
271
  return next();
@@ -101,7 +101,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
101
101
  keys = await service.updateMany(req.body.keys, req.body.data);
102
102
  }
103
103
  else {
104
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
104
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
105
105
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
106
106
  }
107
107
  try {
@@ -146,7 +146,7 @@ router.delete('/', asyncHandler(async (req, _res, next) => {
146
146
  await service.deleteMany(req.body.keys);
147
147
  }
148
148
  else {
149
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
149
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
150
150
  await service.deleteByQuery(sanitizedQuery);
151
151
  }
152
152
  return next();
@@ -88,7 +88,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
88
88
  keys = await service.updateMany(req.body.keys, req.body.data);
89
89
  }
90
90
  else {
91
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
91
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
92
92
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
93
93
  }
94
94
  try {
@@ -133,7 +133,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
133
133
  await service.deleteMany(req.body.keys);
134
134
  }
135
135
  else {
136
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
136
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
137
137
  await service.deleteByQuery(sanitizedQuery);
138
138
  }
139
139
  return next();
@@ -111,7 +111,7 @@ router.patch('/:collection', collectionExists, validateBatch('update'), asyncHan
111
111
  keys = await service.updateMany(req.body.keys, req.body.data);
112
112
  }
113
113
  else {
114
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
114
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
115
115
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
116
116
  }
117
117
  try {
@@ -163,7 +163,7 @@ router.delete('/:collection', collectionExists, validateBatch('delete'), asyncHa
163
163
  await service.deleteMany(req.body.keys);
164
164
  }
165
165
  else {
166
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
166
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
167
167
  await service.deleteByQuery(sanitizedQuery);
168
168
  }
169
169
  return next();
@@ -88,7 +88,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
88
88
  keys = await service.updateMany(req.body.keys, req.body.data);
89
89
  }
90
90
  else {
91
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
91
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
92
92
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
93
93
  }
94
94
  try {
@@ -133,7 +133,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
133
133
  await service.deleteMany(req.body.keys);
134
134
  }
135
135
  else {
136
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
136
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
137
137
  await service.deleteByQuery(sanitizedQuery);
138
138
  }
139
139
  return next();
@@ -79,7 +79,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
79
79
  keys = await service.updateMany(req.body.keys, req.body.data);
80
80
  }
81
81
  else {
82
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
82
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
83
83
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
84
84
  }
85
85
  try {
@@ -124,7 +124,7 @@ router.delete('/', asyncHandler(async (req, _res, next) => {
124
124
  await service.deleteMany(req.body.keys);
125
125
  }
126
126
  else {
127
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
127
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
128
128
  await service.deleteByQuery(sanitizedQuery);
129
129
  }
130
130
  return next();
@@ -79,7 +79,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
79
79
  keys = await service.updateMany(req.body.keys, req.body.data);
80
80
  }
81
81
  else {
82
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
82
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
83
83
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
84
84
  }
85
85
  try {
@@ -124,7 +124,7 @@ router.delete('/', asyncHandler(async (req, _res, next) => {
124
124
  await service.deleteMany(req.body.keys);
125
125
  }
126
126
  else {
127
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
127
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
128
128
  await service.deleteByQuery(sanitizedQuery);
129
129
  }
130
130
  return next();
@@ -105,7 +105,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
105
105
  keys = await service.updateMany(req.body.keys, req.body.data);
106
106
  }
107
107
  else {
108
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
108
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
109
109
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
110
110
  }
111
111
  try {
@@ -150,7 +150,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
150
150
  await service.deleteMany(req.body.keys);
151
151
  }
152
152
  else {
153
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
153
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
154
154
  await service.deleteByQuery(sanitizedQuery);
155
155
  }
156
156
  return next();
@@ -108,7 +108,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
108
108
  keys = await service.updateMany(req.body.keys, req.body.data);
109
109
  }
110
110
  else {
111
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
111
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
112
112
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
113
113
  }
114
114
  try {
@@ -153,7 +153,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
153
153
  await service.deleteMany(req.body.keys);
154
154
  }
155
155
  else {
156
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
156
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
157
157
  await service.deleteByQuery(sanitizedQuery);
158
158
  }
159
159
  return next();
@@ -88,7 +88,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
88
88
  keys = await service.updateMany(req.body.keys, req.body.data);
89
89
  }
90
90
  else {
91
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
91
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
92
92
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
93
93
  }
94
94
  try {
@@ -133,7 +133,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
133
133
  await service.deleteMany(req.body.keys);
134
134
  }
135
135
  else {
136
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
136
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
137
137
  await service.deleteByQuery(sanitizedQuery);
138
138
  }
139
139
  return next();
@@ -100,7 +100,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
100
100
  keys = await service.updateMany(req.body.keys, req.body.data);
101
101
  }
102
102
  else {
103
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
103
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
104
104
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
105
105
  }
106
106
  try {
@@ -145,7 +145,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
145
145
  await service.deleteMany(req.body.keys);
146
146
  }
147
147
  else {
148
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
148
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
149
149
  await service.deleteByQuery(sanitizedQuery);
150
150
  }
151
151
  return next();
@@ -168,7 +168,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
168
168
  keys = await service.updateMany(req.body.keys, req.body.data);
169
169
  }
170
170
  else {
171
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
171
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
172
172
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
173
173
  }
174
174
  try {
@@ -213,7 +213,7 @@ router.delete('/', asyncHandler(async (req, _res, next) => {
213
213
  await service.deleteMany(req.body.keys);
214
214
  }
215
215
  else {
216
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
216
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
217
217
  await service.deleteByQuery(sanitizedQuery);
218
218
  }
219
219
  return next();
@@ -88,7 +88,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
88
88
  keys = await service.updateMany(req.body.keys, req.body.data);
89
89
  }
90
90
  else {
91
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
91
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
92
92
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
93
93
  }
94
94
  try {
@@ -133,7 +133,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
133
133
  await service.deleteMany(req.body.keys);
134
134
  }
135
135
  else {
136
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
136
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
137
137
  await service.deleteByQuery(sanitizedQuery);
138
138
  }
139
139
  return next();
@@ -138,7 +138,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
138
138
  keys = await service.updateMany(req.body.keys, req.body.data);
139
139
  }
140
140
  else {
141
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
141
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
142
142
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
143
143
  }
144
144
  try {
@@ -183,7 +183,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
183
183
  await service.deleteMany(req.body.keys);
184
184
  }
185
185
  else {
186
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
186
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
187
187
  await service.deleteByQuery(sanitizedQuery);
188
188
  }
189
189
  return next();
@@ -36,8 +36,13 @@ router.post('/hash/verify', asyncHandler(async (req, res) => {
36
36
  if (!req.body?.hash) {
37
37
  throw new InvalidPayloadError({ reason: `"hash" is required` });
38
38
  }
39
- const result = await argon2.verify(req.body.hash, req.body.string);
40
- return res.json({ data: result });
39
+ try {
40
+ const result = await argon2.verify(req.body.hash, req.body.string);
41
+ return res.json({ data: result });
42
+ }
43
+ catch {
44
+ throw new InvalidPayloadError({ reason: `Invalid "hash" or "string"` });
45
+ }
41
46
  }));
42
47
  const SortSchema = Joi.object({
43
48
  item: Joi.alternatives(Joi.string(), Joi.number()).required(),
@@ -104,7 +109,7 @@ router.post('/export/:collection', collectionExists, asyncHandler(async (req, _r
104
109
  accountability: req.accountability,
105
110
  schema: req.schema,
106
111
  });
107
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability ?? null);
112
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability ?? null);
108
113
  // We're not awaiting this, as it's supposed to run async in the background
109
114
  service.exportToFile(req.params['collection'], sanitizedQuery, req.body.format, {
110
115
  file: req.body.file,
@@ -89,7 +89,7 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
89
89
  keys = await service.updateMany(req.body.keys, req.body.data);
90
90
  }
91
91
  else {
92
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
92
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
93
93
  keys = await service.updateByQuery(sanitizedQuery, req.body.data);
94
94
  }
95
95
  try {
@@ -134,7 +134,7 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
134
134
  await service.deleteMany(req.body.keys);
135
135
  }
136
136
  else {
137
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
137
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
138
138
  await service.deleteByQuery(sanitizedQuery);
139
139
  }
140
140
  return next();
@@ -58,7 +58,7 @@ router.delete('/', asyncHandler(async (req, _res, next) => {
58
58
  await service.deleteMany(req.body.keys);
59
59
  }
60
60
  else {
61
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
61
+ const sanitizedQuery = await sanitizeQuery(req.body.query, req.schema, req.accountability);
62
62
  await service.deleteByQuery(sanitizedQuery);
63
63
  }
64
64
  return next();
@@ -0,0 +1,3 @@
1
+ import { CapabilitiesHelper } from '../types.js';
2
+ export declare class CapabilitiesHelperDefault extends CapabilitiesHelper {
3
+ }
@@ -0,0 +1,3 @@
1
+ import { CapabilitiesHelper } from '../types.js';
2
+ export class CapabilitiesHelperDefault extends CapabilitiesHelper {
3
+ }
@@ -0,0 +1,4 @@
1
+ import { CapabilitiesHelper } from '../types.js';
2
+ export declare class CapabilitiesHelperMySQL extends CapabilitiesHelper {
3
+ supportsColumnPositionInGroupBy(): boolean;
4
+ }
@@ -0,0 +1,9 @@
1
+ import { CapabilitiesHelper } from '../types.js';
2
+ export class CapabilitiesHelperMySQL extends CapabilitiesHelper {
3
+ supportsColumnPositionInGroupBy() {
4
+ // Supported in MySQL https://dev.mysql.com/doc/refman/8.4/en/select.html#id756773
5
+ // > Columns selected for output can be referred to in ORDER BY and GROUP BY clauses using column names,
6
+ // column aliases, or column positions. Column positions are integers and begin with 1:
7
+ return true;
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ import { CapabilitiesHelper } from '../types.js';
2
+ export declare class CapabilitiesHelperPostgres extends CapabilitiesHelper {
3
+ supportsColumnPositionInGroupBy(): boolean;
4
+ supportsDeduplicationOfParameters(): boolean;
5
+ }
@@ -0,0 +1,14 @@
1
+ import { CapabilitiesHelper } from '../types.js';
2
+ export class CapabilitiesHelperPostgres extends CapabilitiesHelper {
3
+ supportsColumnPositionInGroupBy() {
4
+ // Supported in Postgres https://www.postgresql.org/docs/8.3/sql-select.html#SQL-GROUPBY
5
+ // Supported in CockroachDB (tested manually)
6
+ return true;
7
+ }
8
+ supportsDeduplicationOfParameters() {
9
+ // Postgres infers the type from the context in which the parameter is first referenced.
10
+ // This causes issues when the same parameter is used in different contexts with different types.
11
+ // See https://www.postgresql.org/docs/current/sql-prepare.html
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ export { CapabilitiesHelperPostgres as postgres } from './dialects/postgres.js';
2
+ export { CapabilitiesHelperPostgres as redshift } from './dialects/postgres.js';
3
+ export { CapabilitiesHelperPostgres as cockroachdb } from './dialects/postgres.js';
4
+ export { CapabilitiesHelperDefault as oracle } from './dialects/default.js';
5
+ export { CapabilitiesHelperMySQL as mysql } from './dialects/mysql.js';
6
+ export { CapabilitiesHelperDefault as mssql } from './dialects/default.js';
7
+ export { CapabilitiesHelperDefault as sqlite } from './dialects/default.js';
@@ -0,0 +1,7 @@
1
+ export { CapabilitiesHelperPostgres as postgres } from './dialects/postgres.js';
2
+ export { CapabilitiesHelperPostgres as redshift } from './dialects/postgres.js';
3
+ export { CapabilitiesHelperPostgres as cockroachdb } from './dialects/postgres.js';
4
+ export { CapabilitiesHelperDefault as oracle } from './dialects/default.js';
5
+ export { CapabilitiesHelperMySQL as mysql } from './dialects/mysql.js';
6
+ export { CapabilitiesHelperDefault as mssql } from './dialects/default.js';
7
+ export { CapabilitiesHelperDefault as sqlite } from './dialects/default.js';
@@ -0,0 +1,11 @@
1
+ import { DatabaseHelper } from '../types.js';
2
+ export declare class CapabilitiesHelper extends DatabaseHelper {
3
+ supportsColumnPositionInGroupBy(): boolean;
4
+ /**
5
+ * Indicates if the values within the list of parameters can be safely deduplicated.
6
+ * This is useful for databases that do not automatically cast the value for cases when a parameter is referenced multiple times in the query,
7
+ * but the targeting type is different. For example when referencing a parameter which a UUID, postgres cannot use the same parameter reference
8
+ * to compare it against column of type UUID and at the same time against a column of type a string.
9
+ */
10
+ supportsDeduplicationOfParameters(): boolean;
11
+ }
@@ -0,0 +1,15 @@
1
+ import { DatabaseHelper } from '../types.js';
2
+ export class CapabilitiesHelper extends DatabaseHelper {
3
+ supportsColumnPositionInGroupBy() {
4
+ return false;
5
+ }
6
+ /**
7
+ * Indicates if the values within the list of parameters can be safely deduplicated.
8
+ * This is useful for databases that do not automatically cast the value for cases when a parameter is referenced multiple times in the query,
9
+ * but the targeting type is different. For example when referencing a parameter which a UUID, postgres cannot use the same parameter reference
10
+ * to compare it against column of type UUID and at the same time against a column of type a string.
11
+ */
12
+ supportsDeduplicationOfParameters() {
13
+ return true;
14
+ }
15
+ }
@@ -1,5 +1,6 @@
1
1
  import type { SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
+ import * as capabilitiesHelpers from './capabilities/index.js';
3
4
  import * as dateHelpers from './date/index.js';
4
5
  import * as fnHelpers from './fn/index.js';
5
6
  import * as geometryHelpers from './geometry/index.js';
@@ -12,6 +13,7 @@ export declare function getHelpers(database: Knex): {
12
13
  schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
13
14
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
14
15
  number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
16
+ capabilities: capabilitiesHelpers.postgres | capabilitiesHelpers.oracle | capabilitiesHelpers.mysql;
15
17
  };
16
18
  export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
17
19
  export type Helpers = ReturnType<typeof getHelpers>;