@directus/api 23.1.2 → 23.2.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 (52) hide show
  1. package/dist/app.js +7 -4
  2. package/dist/auth/drivers/openid.js +1 -1
  3. package/dist/controllers/activity.js +2 -88
  4. package/dist/controllers/comments.js +0 -7
  5. package/dist/controllers/items.js +1 -1
  6. package/dist/controllers/tus.d.ts +0 -1
  7. package/dist/controllers/tus.js +0 -16
  8. package/dist/controllers/versions.js +1 -8
  9. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  10. package/dist/database/helpers/schema/dialects/cockroachdb.js +3 -3
  11. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  12. package/dist/database/helpers/schema/dialects/mssql.js +3 -3
  13. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  14. package/dist/database/helpers/schema/dialects/oracle.js +8 -3
  15. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  16. package/dist/database/helpers/schema/dialects/postgres.js +3 -3
  17. package/dist/database/helpers/schema/types.d.ts +2 -1
  18. package/dist/database/helpers/schema/types.js +4 -1
  19. package/dist/database/helpers/schema/utils/{preprocess-bindings.d.ts → prep-query-params.d.ts} +2 -2
  20. package/dist/database/helpers/schema/utils/{preprocess-bindings.js → prep-query-params.js} +1 -1
  21. package/dist/database/index.js +8 -2
  22. package/dist/database/migrations/20240909A-separate-comments.js +1 -6
  23. package/dist/database/migrations/20240924A-migrate-legacy-comments.d.ts +3 -0
  24. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +59 -0
  25. package/dist/database/migrations/20240924B-populate-versioning-deltas.d.ts +3 -0
  26. package/dist/database/migrations/20240924B-populate-versioning-deltas.js +32 -0
  27. package/dist/database/run-ast/utils/apply-parent-filters.js +4 -0
  28. package/dist/database/run-ast/utils/with-preprocess-bindings.js +4 -3
  29. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +2 -1
  30. package/dist/schedules/retention.d.ts +14 -0
  31. package/dist/schedules/retention.js +96 -0
  32. package/dist/{telemetry/lib/init-telemetry.d.ts → schedules/telemetry.d.ts} +2 -2
  33. package/dist/{telemetry/lib/init-telemetry.js → schedules/telemetry.js} +6 -6
  34. package/dist/schedules/tus.d.ts +6 -0
  35. package/dist/schedules/tus.js +23 -0
  36. package/dist/services/assets.js +4 -3
  37. package/dist/services/comments.d.ts +4 -22
  38. package/dist/services/comments.js +16 -252
  39. package/dist/services/graphql/index.d.ts +1 -2
  40. package/dist/services/graphql/index.js +1 -75
  41. package/dist/services/users.js +1 -1
  42. package/dist/services/versions.d.ts +0 -1
  43. package/dist/services/versions.js +9 -29
  44. package/dist/storage/register-locations.js +1 -0
  45. package/dist/telemetry/index.d.ts +0 -1
  46. package/dist/telemetry/index.js +0 -1
  47. package/dist/utils/apply-diff.js +15 -3
  48. package/dist/utils/get-service.js +1 -1
  49. package/dist/utils/get-snapshot-diff.js +17 -1
  50. package/dist/websocket/controllers/base.js +2 -1
  51. package/dist/websocket/controllers/graphql.js +2 -1
  52. package/package.json +19 -19
package/dist/app.js CHANGED
@@ -38,11 +38,14 @@ import serverRouter from './controllers/server.js';
38
38
  import settingsRouter from './controllers/settings.js';
39
39
  import sharesRouter from './controllers/shares.js';
40
40
  import translationsRouter from './controllers/translations.js';
41
- import { default as tusRouter, scheduleTusCleanup } from './controllers/tus.js';
41
+ import tusRouter from './controllers/tus.js';
42
42
  import usersRouter from './controllers/users.js';
43
43
  import utilsRouter from './controllers/utils.js';
44
44
  import versionsRouter from './controllers/versions.js';
45
45
  import webhooksRouter from './controllers/webhooks.js';
46
+ import retentionSchedule from './schedules/retention.js';
47
+ import telemetrySchedule from './schedules/telemetry.js';
48
+ import tusSchedule from './schedules/tus.js';
46
49
  import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations, } from './database/index.js';
47
50
  import emitter from './emitter.js';
48
51
  import { getExtensionManager } from './extensions/index.js';
@@ -57,7 +60,6 @@ import rateLimiterGlobal from './middleware/rate-limiter-global.js';
57
60
  import rateLimiter from './middleware/rate-limiter-ip.js';
58
61
  import sanitizeQuery from './middleware/sanitize-query.js';
59
62
  import schema from './middleware/schema.js';
60
- import { initTelemetry } from './telemetry/index.js';
61
63
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
62
64
  import { Url } from './utils/url.js';
63
65
  import { validateStorage } from './utils/validate-storage.js';
@@ -246,8 +248,9 @@ export default async function createApp() {
246
248
  app.use(notFoundHandler);
247
249
  app.use(errorHandler);
248
250
  await emitter.emitInit('routes.after', { app });
249
- initTelemetry();
250
- scheduleTusCleanup();
251
+ await retentionSchedule();
252
+ await telemetrySchedule();
253
+ await tusSchedule();
251
254
  await emitter.emitInit('app.after', { app });
252
255
  return app;
253
256
  }
@@ -259,7 +259,7 @@ export function createOpenIDAuthRouter(providerName) {
259
259
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
260
260
  }
261
261
  const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, getSecret(), {
262
- expiresIn: '5m',
262
+ expiresIn: (env[`AUTH_${providerName.toUpperCase()}_LOGIN_TIMEOUT`] ?? '5m'),
263
263
  issuer: 'directus',
264
264
  });
265
265
  res.cookie(`openid.${providerName}`, token, {
@@ -1,11 +1,8 @@
1
- import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
2
1
  import express from 'express';
3
- import Joi from 'joi';
4
2
  import { respond } from '../middleware/respond.js';
5
3
  import useCollection from '../middleware/use-collection.js';
6
4
  import { validateBatch } from '../middleware/validate-batch.js';
7
5
  import { ActivityService } from '../services/activity.js';
8
- import { CommentsService } from '../services/comments.js';
9
6
  import { MetaService } from '../services/meta.js';
10
7
  import asyncHandler from '../utils/async-handler.js';
11
8
  const router = express.Router();
@@ -20,7 +17,6 @@ const readHandler = asyncHandler(async (req, res, next) => {
20
17
  schema: req.schema,
21
18
  });
22
19
  let result;
23
- let isComment;
24
20
  if (req.singleton) {
25
21
  result = await service.readSingleton(req.sanitizedQuery);
26
22
  }
@@ -28,24 +24,9 @@ const readHandler = asyncHandler(async (req, res, next) => {
28
24
  result = await service.readMany(req.body.keys, req.sanitizedQuery);
29
25
  }
30
26
  else {
31
- const sanitizedFilter = req.sanitizedQuery.filter;
32
- if (sanitizedFilter &&
33
- '_and' in sanitizedFilter &&
34
- Array.isArray(sanitizedFilter['_and']) &&
35
- sanitizedFilter['_and'].find((andItem) => 'action' in andItem && '_eq' in andItem['action'] && andItem['action']['_eq'] === 'comment')) {
36
- const commentsService = new CommentsService({
37
- accountability: req.accountability,
38
- schema: req.schema,
39
- serviceOrigin: 'activity',
40
- });
41
- result = await commentsService.readByQuery(req.sanitizedQuery);
42
- isComment = true;
43
- }
44
- else {
45
- result = await service.readByQuery(req.sanitizedQuery);
46
- }
27
+ result = await service.readByQuery(req.sanitizedQuery);
47
28
  }
48
- const meta = await metaService.getMetaForQuery(isComment ? 'directus_comments' : 'directus_activity', req.sanitizedQuery);
29
+ const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
49
30
  res.locals['payload'] = {
50
31
  data: result,
51
32
  meta,
@@ -65,71 +46,4 @@ router.get('/:pk', asyncHandler(async (req, res, next) => {
65
46
  };
66
47
  return next();
67
48
  }), respond);
68
- const createCommentSchema = Joi.object({
69
- comment: Joi.string().required(),
70
- collection: Joi.string().required(),
71
- item: [Joi.number().required(), Joi.string().required()],
72
- });
73
- router.post('/comment', asyncHandler(async (req, res, next) => {
74
- const service = new CommentsService({
75
- accountability: req.accountability,
76
- schema: req.schema,
77
- serviceOrigin: 'activity',
78
- });
79
- const { error } = createCommentSchema.validate(req.body);
80
- if (error) {
81
- throw new InvalidPayloadError({ reason: error.message });
82
- }
83
- const primaryKey = await service.createOne(req.body);
84
- try {
85
- const record = await service.readOne(primaryKey, req.sanitizedQuery);
86
- res.locals['payload'] = {
87
- data: record || null,
88
- };
89
- }
90
- catch (error) {
91
- if (isDirectusError(error, ErrorCode.Forbidden)) {
92
- return next();
93
- }
94
- throw error;
95
- }
96
- return next();
97
- }), respond);
98
- const updateCommentSchema = Joi.object({
99
- comment: Joi.string().required(),
100
- });
101
- router.patch('/comment/:pk', asyncHandler(async (req, res, next) => {
102
- const commentsService = new CommentsService({
103
- accountability: req.accountability,
104
- schema: req.schema,
105
- serviceOrigin: 'activity',
106
- });
107
- const { error } = updateCommentSchema.validate(req.body);
108
- if (error) {
109
- throw new InvalidPayloadError({ reason: error.message });
110
- }
111
- const primaryKey = await commentsService.updateOne(req.params['pk'], req.body);
112
- try {
113
- const record = await commentsService.readOne(primaryKey, req.sanitizedQuery);
114
- res.locals['payload'] = {
115
- data: record || null,
116
- };
117
- }
118
- catch (error) {
119
- if (isDirectusError(error, ErrorCode.Forbidden)) {
120
- return next();
121
- }
122
- throw error;
123
- }
124
- return next();
125
- }), respond);
126
- router.delete('/comment/:pk', asyncHandler(async (req, _res, next) => {
127
- const commentsService = new CommentsService({
128
- accountability: req.accountability,
129
- schema: req.schema,
130
- serviceOrigin: 'activity',
131
- });
132
- await commentsService.deleteOne(req.params['pk']);
133
- return next();
134
- }), respond);
135
49
  export default router;
@@ -13,7 +13,6 @@ router.post('/', asyncHandler(async (req, res, next) => {
13
13
  const service = new CommentsService({
14
14
  accountability: req.accountability,
15
15
  schema: req.schema,
16
- serviceOrigin: 'comments',
17
16
  });
18
17
  const savedKeys = [];
19
18
  if (Array.isArray(req.body)) {
@@ -46,7 +45,6 @@ const readHandler = asyncHandler(async (req, res, next) => {
46
45
  const service = new CommentsService({
47
46
  accountability: req.accountability,
48
47
  schema: req.schema,
49
- serviceOrigin: 'comments',
50
48
  });
51
49
  const metaService = new MetaService({
52
50
  accountability: req.accountability,
@@ -69,7 +67,6 @@ router.get('/:pk', asyncHandler(async (req, res, next) => {
69
67
  const service = new CommentsService({
70
68
  accountability: req.accountability,
71
69
  schema: req.schema,
72
- serviceOrigin: 'comments',
73
70
  });
74
71
  const record = await service.readOne(req.params['pk'], req.sanitizedQuery);
75
72
  res.locals['payload'] = { data: record || null };
@@ -79,7 +76,6 @@ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) =
79
76
  const service = new CommentsService({
80
77
  accountability: req.accountability,
81
78
  schema: req.schema,
82
- serviceOrigin: 'comments',
83
79
  });
84
80
  let keys = [];
85
81
  if (Array.isArray(req.body)) {
@@ -108,7 +104,6 @@ router.patch('/:pk', asyncHandler(async (req, res, next) => {
108
104
  const service = new CommentsService({
109
105
  accountability: req.accountability,
110
106
  schema: req.schema,
111
- serviceOrigin: 'comments',
112
107
  });
113
108
  const primaryKey = await service.updateOne(req.params['pk'], req.body);
114
109
  try {
@@ -127,7 +122,6 @@ router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next)
127
122
  const service = new CommentsService({
128
123
  accountability: req.accountability,
129
124
  schema: req.schema,
130
- serviceOrigin: 'comments',
131
125
  });
132
126
  if (Array.isArray(req.body)) {
133
127
  await service.deleteMany(req.body);
@@ -145,7 +139,6 @@ router.delete('/:pk', asyncHandler(async (req, _res, next) => {
145
139
  const service = new CommentsService({
146
140
  accountability: req.accountability,
147
141
  schema: req.schema,
148
- serviceOrigin: 'comments',
149
142
  });
150
143
  await service.deleteOne(req.params['pk']);
151
144
  return next();
@@ -75,7 +75,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
75
75
  };
76
76
  return next();
77
77
  });
78
- router.search('/:collection', collectionExists, validateBatch('read'), readHandler, respond);
78
+ router.search('/:collection', collectionExists, validateBatch('read'), readHandler, mergeContentVersions, respond);
79
79
  router.get('/:collection', collectionExists, readHandler, mergeContentVersions, respond);
80
80
  router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, next) => {
81
81
  if (isSystemCollection(req.params['collection']))
@@ -1,3 +1,2 @@
1
- export declare function scheduleTusCleanup(): void;
2
1
  declare const router: import("express-serve-static-core").Router;
3
2
  export default router;
@@ -1,11 +1,8 @@
1
1
  import { Router } from 'express';
2
- import { RESUMABLE_UPLOADS } from '../constants.js';
3
2
  import getDatabase from '../database/index.js';
4
3
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
5
4
  import { createTusServer } from '../services/tus/index.js';
6
5
  import asyncHandler from '../utils/async-handler.js';
7
- import { getSchema } from '../utils/get-schema.js';
8
- import { scheduleSynchronizedJob, validateCron } from '../utils/schedule.js';
9
6
  const mapAction = (method) => {
10
7
  switch (method) {
11
8
  case 'POST':
@@ -40,19 +37,6 @@ const handler = asyncHandler(async (req, res) => {
40
37
  await tusServer.handle(req, res);
41
38
  cleanupServer();
42
39
  });
43
- export function scheduleTusCleanup() {
44
- if (!RESUMABLE_UPLOADS.ENABLED)
45
- return;
46
- if (validateCron(RESUMABLE_UPLOADS.SCHEDULE)) {
47
- scheduleSynchronizedJob('tus-cleanup', RESUMABLE_UPLOADS.SCHEDULE, async () => {
48
- const [tusServer, cleanupServer] = await createTusServer({
49
- schema: await getSchema(),
50
- });
51
- await tusServer.cleanUpExpiredUploads();
52
- cleanupServer();
53
- });
54
- }
55
- }
56
40
  const router = Router();
57
41
  router.post('/', checkFileAccess, handler);
58
42
  router.patch('/:id', checkFileAccess, handler);
@@ -154,14 +154,7 @@ router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
154
154
  });
155
155
  const version = await service.readOne(req.params['pk']);
156
156
  const { outdated, mainHash } = await service.verifyHash(version['collection'], version['item'], version['hash']);
157
- let current;
158
- if (version['delta']) {
159
- current = version['delta'];
160
- }
161
- else {
162
- const saves = await service.getVersionSavesById(version['id']);
163
- current = assign({}, ...saves);
164
- }
157
+ const current = assign({}, version['delta']);
165
158
  const main = await service.getMainItem(version['collection'], version['item']);
166
159
  res.locals['payload'] = { data: { outdated, mainHash, current, main } };
167
160
  return next();
@@ -6,6 +6,6 @@ export declare class SchemaHelperCockroachDb extends SchemaHelper {
6
6
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
7
7
  constraintName(existingName: string): string;
8
8
  getDatabaseSize(): Promise<number | null>;
9
- preprocessBindings(queryParams: Sql): Sql;
9
+ prepQueryParams(queryParams: Sql): Sql;
10
10
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
11
11
  }
@@ -1,7 +1,7 @@
1
1
  import {} from 'knex';
2
2
  import { SchemaHelper } from '../types.js';
3
3
  import { useEnv } from '@directus/env';
4
- import { preprocessBindings } from '../utils/preprocess-bindings.js';
4
+ import { prepQueryParams } from '../utils/prep-query-params.js';
5
5
  const env = useEnv();
6
6
  export class SchemaHelperCockroachDb extends SchemaHelper {
7
7
  async changeToType(table, column, type, options = {}) {
@@ -29,8 +29,8 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
29
29
  return null;
30
30
  }
31
31
  }
32
- preprocessBindings(queryParams) {
33
- return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
32
+ prepQueryParams(queryParams) {
33
+ return prepQueryParams(queryParams, { format: (index) => `$${index + 1}` });
34
34
  }
35
35
  addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
36
36
  if (hasRelationalSort) {
@@ -5,6 +5,6 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
5
5
  applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
6
6
  formatUUID(uuid: string): string;
7
7
  getDatabaseSize(): Promise<number | null>;
8
- preprocessBindings(queryParams: Sql): Sql;
8
+ prepQueryParams(queryParams: Sql): Sql;
9
9
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
10
10
  }
@@ -1,5 +1,5 @@
1
1
  import { SchemaHelper } from '../types.js';
2
- import { preprocessBindings } from '../utils/preprocess-bindings.js';
2
+ import { prepQueryParams } from '../utils/prep-query-params.js';
3
3
  export class SchemaHelperMSSQL extends SchemaHelper {
4
4
  applyLimit(rootQuery, limit) {
5
5
  // The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries,
@@ -27,8 +27,8 @@ export class SchemaHelperMSSQL extends SchemaHelper {
27
27
  return null;
28
28
  }
29
29
  }
30
- preprocessBindings(queryParams) {
31
- return preprocessBindings(queryParams, { format: (index) => `@p${index}` });
30
+ prepQueryParams(queryParams) {
31
+ return prepQueryParams(queryParams, { format: (index) => `@p${index}` });
32
32
  }
33
33
  addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
34
34
  /*
@@ -9,6 +9,7 @@ export declare class SchemaHelperOracle extends SchemaHelper {
9
9
  preRelationChange(relation: Partial<Relation>): void;
10
10
  processFieldType(field: Field): Type;
11
11
  getDatabaseSize(): Promise<number | null>;
12
- preprocessBindings(queryParams: Sql): Sql;
12
+ prepQueryParams(queryParams: Sql): Sql;
13
+ prepBindings(bindings: Knex.Value[]): any;
13
14
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
14
15
  }
@@ -1,5 +1,5 @@
1
1
  import { SchemaHelper } from '../types.js';
2
- import { preprocessBindings } from '../utils/preprocess-bindings.js';
2
+ import { prepQueryParams } from '../utils/prep-query-params.js';
3
3
  export class SchemaHelperOracle extends SchemaHelper {
4
4
  async changeToType(table, column, type, options = {}) {
5
5
  await this.changeToTypeByCopy(table, column, type, options);
@@ -39,8 +39,13 @@ export class SchemaHelperOracle extends SchemaHelper {
39
39
  return null;
40
40
  }
41
41
  }
42
- preprocessBindings(queryParams) {
43
- return preprocessBindings(queryParams, { format: (index) => `:${index + 1}` });
42
+ prepQueryParams(queryParams) {
43
+ return prepQueryParams(queryParams, { format: (index) => `:${index + 1}` });
44
+ }
45
+ prepBindings(bindings) {
46
+ // Create an object with keys 1, 2, 3, ... and the bindings as values
47
+ // This will use the "named" binding syntax in the oracledb driver instead of the positional binding
48
+ return Object.fromEntries(bindings.map((binding, index) => [index + 1, binding]));
44
49
  }
45
50
  addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
46
51
  /*
@@ -2,6 +2,6 @@ import type { Knex } from 'knex';
2
2
  import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
3
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
4
4
  getDatabaseSize(): Promise<number | null>;
5
- preprocessBindings(queryParams: Sql): Sql;
5
+ prepQueryParams(queryParams: Sql): Sql;
6
6
  addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { SchemaHelper } from '../types.js';
3
- import { preprocessBindings } from '../utils/preprocess-bindings.js';
3
+ import { prepQueryParams } from '../utils/prep-query-params.js';
4
4
  const env = useEnv();
5
5
  export class SchemaHelperPostgres extends SchemaHelper {
6
6
  async getDatabaseSize() {
@@ -12,8 +12,8 @@ export class SchemaHelperPostgres extends SchemaHelper {
12
12
  return null;
13
13
  }
14
14
  }
15
- preprocessBindings(queryParams) {
16
- return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
15
+ prepQueryParams(queryParams) {
16
+ return prepQueryParams(queryParams, { format: (index) => `$${index + 1}` });
17
17
  }
18
18
  addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
19
19
  if (hasRelationalSort) {
@@ -35,6 +35,7 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
35
35
  * @returns Size of the database in bytes
36
36
  */
37
37
  getDatabaseSize(): Promise<number | null>;
38
- preprocessBindings(queryParams: Sql): Sql;
38
+ prepQueryParams(queryParams: Sql): Sql;
39
+ prepBindings(bindings: Knex.Value[]): any;
39
40
  addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
40
41
  }
@@ -94,9 +94,12 @@ export class SchemaHelper extends DatabaseHelper {
94
94
  async getDatabaseSize() {
95
95
  return null;
96
96
  }
97
- preprocessBindings(queryParams) {
97
+ prepQueryParams(queryParams) {
98
98
  return queryParams;
99
99
  }
100
+ prepBindings(bindings) {
101
+ return bindings;
102
+ }
100
103
  addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasRelationalSort) {
101
104
  // no-op by default
102
105
  }
@@ -1,12 +1,12 @@
1
1
  import type { Knex } from 'knex';
2
2
  import type { Sql } from '../types.js';
3
- export type PreprocessBindingsOptions = {
3
+ export type PrepQueryParamsOptions = {
4
4
  format(index: number): string;
5
5
  };
6
6
  /**
7
7
  * Preprocess a SQL query, such that repeated binding values are bound to the same binding index.
8
8
  **/
9
- export declare function preprocessBindings(queryParams: (Partial<Sql> & Pick<Sql, 'sql'>) | string, options: PreprocessBindingsOptions): {
9
+ export declare function prepQueryParams(queryParams: (Partial<Sql> & Pick<Sql, 'sql'>) | string, options: PrepQueryParamsOptions): {
10
10
  sql: string;
11
11
  bindings: Knex.Value[];
12
12
  };
@@ -2,7 +2,7 @@ import { isString } from 'lodash-es';
2
2
  /**
3
3
  * Preprocess a SQL query, such that repeated binding values are bound to the same binding index.
4
4
  **/
5
- export function preprocessBindings(queryParams, options) {
5
+ export function prepQueryParams(queryParams, options) {
6
6
  const query = { bindings: [], ...(isString(queryParams) ? { sql: queryParams } : queryParams) };
7
7
  // bindingIndices[i] is the index of the first occurrence of query.bindings[i]
8
8
  const bindingIndices = new Map();
@@ -3,7 +3,7 @@ import { createInspector } from '@directus/schema';
3
3
  import { isObject } from '@directus/utils';
4
4
  import fse from 'fs-extra';
5
5
  import knex from 'knex';
6
- import { merge } from 'lodash-es';
6
+ import { isArray, merge } from 'lodash-es';
7
7
  import { dirname } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import path from 'path';
@@ -139,7 +139,13 @@ export function getDatabase() {
139
139
  delta = performance.now() - time;
140
140
  times.delete(queryInfo.__knexUid);
141
141
  }
142
- logger.trace(`[${delta ? delta.toFixed(3) : '?'}ms] ${queryInfo.sql} [${(queryInfo.bindings ?? []).join(', ')}]`);
142
+ // eslint-disable-next-line no-nested-ternary
143
+ const bindings = queryInfo.bindings
144
+ ? isArray(queryInfo.bindings)
145
+ ? queryInfo.bindings
146
+ : Object.values(queryInfo.bindings)
147
+ : [];
148
+ logger.trace(`[${delta ? delta.toFixed(3) : '?'}ms] ${queryInfo.sql} [${bindings.join(', ')}]`);
143
149
  })
144
150
  .on('query-error', (_, queryInfo) => {
145
151
  times.delete(queryInfo.__knexUid);
@@ -2,12 +2,7 @@ import { Action } from '@directus/constants';
2
2
  export async function up(knex) {
3
3
  await knex.schema.createTable('directus_comments', (table) => {
4
4
  table.uuid('id').primary().notNullable();
5
- table
6
- .string('collection', 64)
7
- .notNullable()
8
- .references('collection')
9
- .inTable('directus_collections')
10
- .onDelete('CASCADE');
5
+ table.string('collection', 64).notNullable();
11
6
  table.string('item').notNullable();
12
7
  table.text('comment').notNullable();
13
8
  table.timestamp('date_created').defaultTo(knex.fn.now());
@@ -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,59 @@
1
+ import { Action } from '@directus/constants';
2
+ import { randomUUID } from 'node:crypto';
3
+ export async function up(knex) {
4
+ // remove foreign key constraint for projects already migrated to retentions-p1
5
+ try {
6
+ await knex.schema.alterTable('directus_comments', (table) => {
7
+ table.dropForeign('collection');
8
+ });
9
+ }
10
+ catch {
11
+ // ignore
12
+ }
13
+ const rowsLimit = 50;
14
+ let hasMore = true;
15
+ while (hasMore) {
16
+ const legacyComments = await knex
17
+ .select('*')
18
+ .from('directus_activity')
19
+ .where('action', '=', Action.COMMENT)
20
+ .limit(rowsLimit);
21
+ if (legacyComments.length === 0) {
22
+ hasMore = false;
23
+ break;
24
+ }
25
+ await knex.transaction(async (trx) => {
26
+ for (const legacyComment of legacyComments) {
27
+ let primaryKey;
28
+ // Migrate legacy comment
29
+ if (legacyComment['action'] === Action.COMMENT) {
30
+ primaryKey = randomUUID();
31
+ await trx('directus_comments').insert({
32
+ id: primaryKey,
33
+ collection: legacyComment.collection,
34
+ item: legacyComment.item,
35
+ comment: legacyComment.comment,
36
+ user_created: legacyComment.user,
37
+ date_created: legacyComment.timestamp,
38
+ });
39
+ await trx('directus_activity')
40
+ .update({
41
+ action: Action.CREATE,
42
+ collection: 'directus_comments',
43
+ item: primaryKey,
44
+ comment: null,
45
+ })
46
+ .where('id', '=', legacyComment.id);
47
+ }
48
+ }
49
+ });
50
+ }
51
+ await knex.schema.alterTable('directus_activity', (table) => {
52
+ table.dropColumn('comment');
53
+ });
54
+ }
55
+ export async function down(knex) {
56
+ await knex.schema.alterTable('directus_activity', (table) => {
57
+ table.text('comment');
58
+ });
59
+ }
@@ -0,0 +1,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { parseJSON } from '@directus/utils';
2
+ import { assign } from 'lodash-es';
3
+ export async function up(knex) {
4
+ const rowsLimit = 50;
5
+ let hasMore = true;
6
+ while (hasMore) {
7
+ const missingDeltaVersions = await knex.select('id').from('directus_versions').whereNull('delta').limit(rowsLimit);
8
+ if (missingDeltaVersions.length === 0) {
9
+ hasMore = false;
10
+ break;
11
+ }
12
+ await knex.transaction(async (trx) => {
13
+ for (const missingDeltaVersion of missingDeltaVersions) {
14
+ const revisions = await trx
15
+ .select('delta')
16
+ .from('directus_revisions')
17
+ .where('version', '=', missingDeltaVersion.id)
18
+ .orderBy('id');
19
+ const deltas = revisions.map((revision) => parseJSON(revision.delta));
20
+ const consolidatedDelta = assign({}, ...deltas);
21
+ await trx('directus_versions')
22
+ .update({
23
+ delta: JSON.stringify(consolidatedDelta),
24
+ })
25
+ .where('id', '=', missingDeltaVersion.id);
26
+ }
27
+ });
28
+ }
29
+ }
30
+ export async function down() {
31
+ // No down migration required
32
+ }
@@ -33,6 +33,10 @@ export function applyParentFilters(schema, nestedCollectionNodes, parentItem) {
33
33
  const foreignField = nestedNode.relation.field;
34
34
  const foreignIds = uniq(parentItems.map((res) => res[nestedNode.parentKey])).filter((id) => !isNil(id));
35
35
  merge(nestedNode, { query: { filter: { [foreignField]: { _in: foreignIds } } } });
36
+ if (nestedNode.relation.meta?.junction_field) {
37
+ const junctionField = nestedNode.relation.meta.junction_field;
38
+ merge(nestedNode, { query: { filter: { [junctionField]: { _nnull: true } } } });
39
+ }
36
40
  }
37
41
  else if (nestedNode.type === 'a2o') {
38
42
  const keysPerCollection = {};
@@ -4,9 +4,10 @@ export function withPreprocessBindings(knex, dbQuery) {
4
4
  dbQuery.client = new Proxy(dbQuery.client, {
5
5
  get(target, prop, receiver) {
6
6
  if (prop === 'query') {
7
- return (connection, queryParam) => {
8
- return Reflect.get(target, prop, receiver).bind(target)(connection, schemaHelper.preprocessBindings(queryParam));
9
- };
7
+ return (connection, queryParams) => Reflect.get(target, prop, receiver).bind(dbQuery.client)(connection, schemaHelper.prepQueryParams(queryParams));
8
+ }
9
+ if (prop === 'prepBindings') {
10
+ return (bindings) => schemaHelper.prepBindings(Reflect.get(target, prop, receiver).bind(dbQuery.client)(bindings));
10
11
  }
11
12
  return Reflect.get(target, prop, receiver);
12
13
  },
@@ -1,3 +1,4 @@
1
+ import { toBoolean } from '@directus/utils';
1
2
  import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
2
3
  import { processAst } from '../../process-ast/process-ast.js';
3
4
  export async function validateItemAccess(options, context) {
@@ -31,7 +32,7 @@ export async function validateItemAccess(options, context) {
31
32
  if (items && items.length === options.primaryKeys.length) {
32
33
  const { fields } = options;
33
34
  if (fields) {
34
- return items.every((item) => fields.every((field) => item[field] === 1));
35
+ return items.every((item) => fields.every((field) => toBoolean(item[field])));
35
36
  }
36
37
  return true;
37
38
  }