@directus/api 22.2.0 → 23.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 (101) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/drivers/ldap.js +14 -3
  3. package/dist/auth/drivers/oauth2.js +13 -2
  4. package/dist/auth/drivers/openid.js +13 -2
  5. package/dist/cache.js +4 -4
  6. package/dist/cli/commands/init/questions.d.ts +5 -5
  7. package/dist/cli/commands/schema/apply.d.ts +1 -0
  8. package/dist/cli/commands/schema/apply.js +20 -1
  9. package/dist/cli/index.js +1 -0
  10. package/dist/cli/utils/create-env/env-stub.liquid +1 -4
  11. package/dist/controllers/activity.js +30 -27
  12. package/dist/controllers/assets.js +1 -1
  13. package/dist/controllers/comments.d.ts +2 -0
  14. package/dist/controllers/comments.js +153 -0
  15. package/dist/controllers/versions.js +10 -5
  16. package/dist/database/index.js +3 -0
  17. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
  18. package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
  19. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  20. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  21. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  22. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  23. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  24. package/dist/database/run-ast/lib/get-db-query.js +2 -2
  25. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  26. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  27. package/dist/database/run-ast/run-ast.js +8 -1
  28. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  29. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  30. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
  31. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
  32. package/dist/logger/index.d.ts +2 -3
  33. package/dist/logger/logs-stream.d.ts +0 -1
  34. package/dist/mailer.js +0 -6
  35. package/dist/middleware/authenticate.d.ts +1 -3
  36. package/dist/middleware/error-handler.d.ts +0 -1
  37. package/dist/middleware/validate-batch.d.ts +1 -4
  38. package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
  39. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
  40. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  41. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  42. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  43. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  44. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
  45. package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
  46. package/dist/permissions/utils/process-permissions.d.ts +11 -1
  47. package/dist/permissions/utils/process-permissions.js +6 -4
  48. package/dist/request/agent-with-ip-validation.d.ts +0 -1
  49. package/dist/server.d.ts +0 -3
  50. package/dist/services/activity.d.ts +1 -7
  51. package/dist/services/activity.js +0 -103
  52. package/dist/services/assets.d.ts +0 -1
  53. package/dist/services/assets.js +5 -4
  54. package/dist/services/collections.js +6 -4
  55. package/dist/services/comments.d.ts +31 -0
  56. package/dist/services/comments.js +374 -0
  57. package/dist/services/fields.js +0 -6
  58. package/dist/services/files/utils/get-metadata.d.ts +0 -1
  59. package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
  60. package/dist/services/files.d.ts +0 -1
  61. package/dist/services/graphql/index.js +17 -16
  62. package/dist/services/import-export.d.ts +0 -1
  63. package/dist/services/index.d.ts +1 -0
  64. package/dist/services/index.js +1 -0
  65. package/dist/services/items.js +3 -1
  66. package/dist/services/mail/index.d.ts +2 -1
  67. package/dist/services/mail/index.js +4 -1
  68. package/dist/services/payload.js +15 -14
  69. package/dist/services/tus/data-store.d.ts +0 -1
  70. package/dist/services/users.js +3 -2
  71. package/dist/services/versions.js +59 -44
  72. package/dist/types/graphql.d.ts +0 -1
  73. package/dist/utils/apply-diff.js +5 -6
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/compress.d.ts +0 -1
  76. package/dist/utils/delete-from-require-cache.js +1 -1
  77. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
  78. package/dist/utils/generate-hash.js +2 -2
  79. package/dist/utils/get-address.d.ts +0 -3
  80. package/dist/utils/get-cache-headers.d.ts +0 -1
  81. package/dist/utils/get-cache-key.d.ts +0 -1
  82. package/dist/utils/get-column.d.ts +1 -1
  83. package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
  84. package/dist/utils/get-ip-from-req.d.ts +0 -1
  85. package/dist/utils/get-service.js +3 -1
  86. package/dist/utils/get-snapshot.js +1 -1
  87. package/dist/utils/sanitize-query.js +1 -1
  88. package/dist/utils/sanitize-schema.d.ts +1 -1
  89. package/dist/utils/sanitize-schema.js +2 -0
  90. package/dist/utils/should-skip-cache.d.ts +0 -1
  91. package/dist/websocket/authenticate.js +1 -1
  92. package/dist/websocket/controllers/base.d.ts +1 -10
  93. package/dist/websocket/controllers/base.js +15 -21
  94. package/dist/websocket/controllers/graphql.d.ts +0 -3
  95. package/dist/websocket/controllers/index.d.ts +0 -3
  96. package/dist/websocket/controllers/logs.d.ts +4 -5
  97. package/dist/websocket/controllers/logs.js +7 -3
  98. package/dist/websocket/controllers/rest.d.ts +0 -3
  99. package/dist/websocket/controllers/rest.js +1 -1
  100. package/dist/websocket/types.d.ts +0 -6
  101. package/package.json +70 -71
package/dist/app.js CHANGED
@@ -14,6 +14,7 @@ import accessRouter from './controllers/access.js';
14
14
  import assetsRouter from './controllers/assets.js';
15
15
  import authRouter from './controllers/auth.js';
16
16
  import collectionsRouter from './controllers/collections.js';
17
+ import commentsRouter from './controllers/comments.js';
17
18
  import dashboardsRouter from './controllers/dashboards.js';
18
19
  import extensionsRouter from './controllers/extensions.js';
19
20
  import fieldsRouter from './controllers/fields.js';
@@ -209,6 +210,7 @@ export default async function createApp() {
209
210
  app.use('/access', accessRouter);
210
211
  app.use('/assets', assetsRouter);
211
212
  app.use('/collections', collectionsRouter);
213
+ app.use('/comments', commentsRouter);
212
214
  app.use('/dashboards', dashboardsRouter);
213
215
  app.use('/extensions', extensionsRouter);
214
216
  app.use('/fields', fieldsRouter);
@@ -172,7 +172,7 @@ export class LDAPAuthDriver extends AuthDriver {
172
172
  }
173
173
  const logger = useLogger();
174
174
  await this.validateBindClient();
175
- const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId } = this.config;
175
+ const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId, syncUserInfo } = this.config;
176
176
  const userInfo = await this.fetchUserInfo(userDn, new ldap.EqualityFilter({
177
177
  attribute: userAttribute ?? 'cn',
178
178
  value: payload['identifier'],
@@ -201,11 +201,22 @@ export class LDAPAuthDriver extends AuthDriver {
201
201
  if (userId) {
202
202
  // Run hook so the end user has the chance to augment the
203
203
  // user that is about to be updated
204
- let updatedUserPayload = await emitter.emitFilter(`auth.update`, {}, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
204
+ let emitPayload = {};
205
205
  // Only sync roles if the AD groups are configured
206
206
  if (groupDn) {
207
- updatedUserPayload = { role: userRole?.id ?? defaultRoleId ?? null, ...updatedUserPayload };
207
+ emitPayload = {
208
+ role: userRole?.id ?? defaultRoleId ?? null,
209
+ };
208
210
  }
211
+ if (syncUserInfo) {
212
+ emitPayload = {
213
+ ...emitPayload,
214
+ first_name: userInfo.firstName,
215
+ last_name: userInfo.lastName,
216
+ email: userInfo.email,
217
+ };
218
+ }
219
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
209
220
  // Update user to update properties that might have changed
210
221
  await this.usersService.updateOne(userId, updatedUserPayload);
211
222
  return userId;
@@ -106,7 +106,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
106
106
  }
107
107
  // Flatten response to support dot indexes
108
108
  userInfo = flatten(userInfo);
109
- const { provider, emailKey, identifierKey, allowPublicRegistration } = this.config;
109
+ const { provider, emailKey, identifierKey, allowPublicRegistration, syncUserInfo } = this.config;
110
110
  const email = userInfo[emailKey ?? 'email'] ? String(userInfo[emailKey ?? 'email']) : undefined;
111
111
  // Fallback to email if explicit identifier not found
112
112
  const identifier = userInfo[identifierKey] ? String(userInfo[identifierKey]) : email;
@@ -127,7 +127,18 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
127
127
  if (userId) {
128
128
  // Run hook so the end user has the chance to augment the
129
129
  // user that is about to be updated
130
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
130
+ let emitPayload = {
131
+ auth_data: userPayload.auth_data,
132
+ };
133
+ if (syncUserInfo) {
134
+ emitPayload = {
135
+ ...emitPayload,
136
+ first_name: userPayload.first_name,
137
+ last_name: userPayload.last_name,
138
+ email: userPayload.email,
139
+ };
140
+ }
141
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
131
142
  identifier,
132
143
  provider: this.config['provider'],
133
144
  providerPayload: { accessToken: tokenSet.access_token, userInfo },
@@ -125,7 +125,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
125
125
  }
126
126
  // Flatten response to support dot indexes
127
127
  userInfo = flatten(userInfo);
128
- const { provider, identifierKey, allowPublicRegistration, requireVerifiedEmail } = this.config;
128
+ const { provider, identifierKey, allowPublicRegistration, requireVerifiedEmail, syncUserInfo } = this.config;
129
129
  const email = userInfo['email'] ? String(userInfo['email']) : undefined;
130
130
  // Fallback to email if explicit identifier not found
131
131
  const identifier = userInfo[identifierKey ?? 'sub'] ? String(userInfo[identifierKey ?? 'sub']) : email;
@@ -146,7 +146,18 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
146
146
  if (userId) {
147
147
  // Run hook so the end user has the chance to augment the
148
148
  // user that is about to be updated
149
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
149
+ let emitPayload = {
150
+ auth_data: userPayload.auth_data,
151
+ };
152
+ if (syncUserInfo) {
153
+ emitPayload = {
154
+ ...emitPayload,
155
+ first_name: userPayload.first_name,
156
+ last_name: userPayload.last_name,
157
+ email: userPayload.email,
158
+ };
159
+ }
160
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
150
161
  identifier,
151
162
  provider: this.config['provider'],
152
163
  providerPayload: { accessToken: tokenSet.access_token, userInfo },
package/dist/cache.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { useEnv } from '@directus/env';
2
- import Keyv from 'keyv';
2
+ import Keyv, {} from 'keyv';
3
3
  import { useBus } from './bus/index.js';
4
4
  import { useLogger } from './logger/index.js';
5
+ import { clearCache as clearPermissionCache } from './permissions/cache.js';
5
6
  import { redisConfigAvailable } from './redis/index.js';
6
7
  import { compress, decompress } from './utils/compress.js';
7
8
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
8
9
  import { getMilliseconds } from './utils/get-milliseconds.js';
9
10
  import { validateEnv } from './utils/validate-env.js';
10
- import { clearCache as clearPermissionCache } from './permissions/cache.js';
11
11
  import { createRequire } from 'node:module';
12
12
  const logger = useLogger();
13
13
  const env = useEnv();
@@ -106,10 +106,10 @@ function getKeyvInstance(store, ttl, namespaceSuffix) {
106
106
  function getConfig(store = 'memory', ttl, namespaceSuffix = '') {
107
107
  const config = {
108
108
  namespace: `${env['CACHE_NAMESPACE']}${namespaceSuffix}`,
109
- ttl,
109
+ ...(ttl && { ttl }),
110
110
  };
111
111
  if (store === 'redis') {
112
- const KeyvRedis = require('@keyv/redis');
112
+ const { default: KeyvRedis } = require('@keyv/redis');
113
113
  config.store = new KeyvRedis(env['REDIS'] || getConfigFromEnv('REDIS'), { useRedisSets: false });
114
114
  }
115
115
  return config;
@@ -4,18 +4,18 @@ export declare const databaseQuestions: {
4
4
  filepath: string;
5
5
  }) => Record<string, string>)[];
6
6
  mysql2: (({ client }: {
7
- client: Exclude<Driver, 'sqlite3'>;
7
+ client: Exclude<Driver, "sqlite3">;
8
8
  }) => Record<string, any>)[];
9
9
  pg: (({ client }: {
10
- client: Exclude<Driver, 'sqlite3'>;
10
+ client: Exclude<Driver, "sqlite3">;
11
11
  }) => Record<string, any>)[];
12
12
  cockroachdb: (({ client }: {
13
- client: Exclude<Driver, 'sqlite3'>;
13
+ client: Exclude<Driver, "sqlite3">;
14
14
  }) => Record<string, any>)[];
15
15
  oracledb: (({ client }: {
16
- client: Exclude<Driver, 'sqlite3'>;
16
+ client: Exclude<Driver, "sqlite3">;
17
17
  }) => Record<string, any>)[];
18
18
  mssql: (({ client }: {
19
- client: Exclude<Driver, 'sqlite3'>;
19
+ client: Exclude<Driver, "sqlite3">;
20
20
  }) => Record<string, any>)[];
21
21
  };
@@ -1,4 +1,5 @@
1
1
  export declare function apply(snapshotPath: string, options?: {
2
2
  yes: boolean;
3
3
  dryRun: boolean;
4
+ ignoreRules: string;
4
5
  }): Promise<void>;
@@ -11,6 +11,22 @@ import { isNestedMetaUpdate } from '../../../utils/apply-diff.js';
11
11
  import { applySnapshot } from '../../../utils/apply-snapshot.js';
12
12
  import { getSnapshotDiff } from '../../../utils/get-snapshot-diff.js';
13
13
  import { getSnapshot } from '../../../utils/get-snapshot.js';
14
+ function filterSnapshotDiff(snapshot, filters) {
15
+ const filterSet = new Set(filters);
16
+ function shouldKeep(item) {
17
+ if (filterSet.has(item.collection))
18
+ return false;
19
+ if (item.field && filterSet.has(`${item.collection}.${item.field}`))
20
+ return false;
21
+ return true;
22
+ }
23
+ const filteredDiff = {
24
+ collections: snapshot.collections.filter((item) => shouldKeep(item)),
25
+ fields: snapshot.fields.filter((item) => shouldKeep(item)),
26
+ relations: snapshot.relations.filter((item) => shouldKeep(item)),
27
+ };
28
+ return filteredDiff;
29
+ }
14
30
  export async function apply(snapshotPath, options) {
15
31
  const logger = useLogger();
16
32
  const filename = path.resolve(process.cwd(), snapshotPath);
@@ -31,7 +47,10 @@ export async function apply(snapshotPath, options) {
31
47
  snapshot = parseJSON(fileContents);
32
48
  }
33
49
  const currentSnapshot = await getSnapshot({ database });
34
- const snapshotDiff = getSnapshotDiff(currentSnapshot, snapshot);
50
+ let snapshotDiff = getSnapshotDiff(currentSnapshot, snapshot);
51
+ if (options?.ignoreRules) {
52
+ snapshotDiff = filterSnapshotDiff(snapshotDiff, options.ignoreRules.split(','));
53
+ }
35
54
  if (snapshotDiff.collections.length === 0 &&
36
55
  snapshotDiff.fields.length === 0 &&
37
56
  snapshotDiff.relations.length === 0) {
package/dist/cli/index.js CHANGED
@@ -81,6 +81,7 @@ export async function createCli() {
81
81
  .description('Apply a snapshot file to the current database')
82
82
  .option('-y, --yes', `Assume "yes" as answer to all prompts and run non-interactively`)
83
83
  .option('-d, --dry-run', 'Plan and log changes to be applied', false)
84
+ .option('--ignoreRules <value>', `Comma-separated list of collections and or fields to ignore. Format: "products.title,reviews" this will ignore applying changes to the title field in the products collection and the entire reviews collection`)
84
85
  .argument('<path>', 'Path to snapshot file')
85
86
  .action(apply);
86
87
  await emitter.emitInit('cli.after', { program });
@@ -313,7 +313,7 @@ EXTENSIONS_AUTO_RELOAD=false
313
313
  EMAIL_FROM="no-reply@example.com"
314
314
 
315
315
  # What to use to send emails. One of
316
- # sendmail, smtp, mailgun, sendgrid, ses.
316
+ # sendmail, smtp, mailgun, ses.
317
317
  EMAIL_TRANSPORT="sendmail"
318
318
  EMAIL_SENDMAIL_NEW_LINE="unix"
319
319
  EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
@@ -340,6 +340,3 @@ EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
340
340
  ## Email (Mailgun Transport)
341
341
  # EMAIL_MAILGUN_API_KEY="key-1234123412341234"
342
342
  # EMAIL_MAILGUN_DOMAIN="a domain name from https://app.mailgun.com/app/sending/domains"
343
-
344
- ## Email (SendGrid Transport)
345
- # EMAIL_SENDGRID_API_KEY="key-1234123412341234"
@@ -1,15 +1,13 @@
1
- import { Action } from '@directus/constants';
2
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
2
  import express from 'express';
4
3
  import Joi from 'joi';
5
- import { ErrorCode, ForbiddenError, InvalidPayloadError } from '@directus/errors';
6
4
  import { respond } from '../middleware/respond.js';
7
5
  import useCollection from '../middleware/use-collection.js';
8
6
  import { validateBatch } from '../middleware/validate-batch.js';
9
7
  import { ActivityService } from '../services/activity.js';
8
+ import { CommentsService } from '../services/comments.js';
10
9
  import { MetaService } from '../services/meta.js';
11
10
  import asyncHandler from '../utils/async-handler.js';
12
- import { getIPFromReq } from '../utils/get-ip-from-req.js';
13
11
  const router = express.Router();
14
12
  router.use(useCollection('directus_activity'));
15
13
  const readHandler = asyncHandler(async (req, res, next) => {
@@ -22,6 +20,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
22
20
  schema: req.schema,
23
21
  });
24
22
  let result;
23
+ let isComment;
25
24
  if (req.singleton) {
26
25
  result = await service.readSingleton(req.sanitizedQuery);
27
26
  }
@@ -29,9 +28,24 @@ const readHandler = asyncHandler(async (req, res, next) => {
29
28
  result = await service.readMany(req.body.keys, req.sanitizedQuery);
30
29
  }
31
30
  else {
32
- result = await service.readByQuery(req.sanitizedQuery);
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
+ }
33
47
  }
34
- const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
48
+ const meta = await metaService.getMetaForQuery(isComment ? 'directus_comments' : 'directus_activity', req.sanitizedQuery);
35
49
  res.locals['payload'] = {
36
50
  data: result,
37
51
  meta,
@@ -57,22 +71,16 @@ const createCommentSchema = Joi.object({
57
71
  item: [Joi.number().required(), Joi.string().required()],
58
72
  });
59
73
  router.post('/comment', asyncHandler(async (req, res, next) => {
60
- const service = new ActivityService({
74
+ const service = new CommentsService({
61
75
  accountability: req.accountability,
62
76
  schema: req.schema,
77
+ serviceOrigin: 'activity',
63
78
  });
64
79
  const { error } = createCommentSchema.validate(req.body);
65
80
  if (error) {
66
81
  throw new InvalidPayloadError({ reason: error.message });
67
82
  }
68
- const primaryKey = await service.createOne({
69
- ...req.body,
70
- action: Action.COMMENT,
71
- user: req.accountability?.user,
72
- ip: getIPFromReq(req),
73
- user_agent: req.accountability?.userAgent,
74
- origin: req.get('origin'),
75
- });
83
+ const primaryKey = await service.createOne(req.body);
76
84
  try {
77
85
  const record = await service.readOne(primaryKey, req.sanitizedQuery);
78
86
  res.locals['payload'] = {
@@ -91,17 +99,18 @@ const updateCommentSchema = Joi.object({
91
99
  comment: Joi.string().required(),
92
100
  });
93
101
  router.patch('/comment/:pk', asyncHandler(async (req, res, next) => {
94
- const service = new ActivityService({
102
+ const commentsService = new CommentsService({
95
103
  accountability: req.accountability,
96
104
  schema: req.schema,
105
+ serviceOrigin: 'activity',
97
106
  });
98
107
  const { error } = updateCommentSchema.validate(req.body);
99
108
  if (error) {
100
109
  throw new InvalidPayloadError({ reason: error.message });
101
110
  }
102
- const primaryKey = await service.updateOne(req.params['pk'], req.body);
111
+ const primaryKey = await commentsService.updateOne(req.params['pk'], req.body);
103
112
  try {
104
- const record = await service.readOne(primaryKey, req.sanitizedQuery);
113
+ const record = await commentsService.readOne(primaryKey, req.sanitizedQuery);
105
114
  res.locals['payload'] = {
106
115
  data: record || null,
107
116
  };
@@ -115,18 +124,12 @@ router.patch('/comment/:pk', asyncHandler(async (req, res, next) => {
115
124
  return next();
116
125
  }), respond);
117
126
  router.delete('/comment/:pk', asyncHandler(async (req, _res, next) => {
118
- const service = new ActivityService({
127
+ const commentsService = new CommentsService({
119
128
  accountability: req.accountability,
120
129
  schema: req.schema,
130
+ serviceOrigin: 'activity',
121
131
  });
122
- const adminService = new ActivityService({
123
- schema: req.schema,
124
- });
125
- const item = await adminService.readOne(req.params['pk'], { fields: ['action'] });
126
- if (!item || item['action'] !== Action.COMMENT) {
127
- throw new ForbiddenError();
128
- }
129
- await service.deleteOne(req.params['pk']);
132
+ await commentsService.deleteOne(req.params['pk']);
130
133
  return next();
131
134
  }), respond);
132
135
  export default router;
@@ -104,7 +104,7 @@ asyncHandler(async (req, res, next) => {
104
104
  return helmet.contentSecurityPolicy(merge({
105
105
  useDefaults: false,
106
106
  directives: {
107
- defaultSrc: ['none'],
107
+ defaultSrc: [`'none'`],
108
108
  },
109
109
  }, getConfigFromEnv('ASSETS_CONTENT_SECURITY_POLICY')))(req, res, next);
110
110
  }),
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -0,0 +1,153 @@
1
+ import { ErrorCode, isDirectusError } from '@directus/errors';
2
+ import express from 'express';
3
+ import { respond } from '../middleware/respond.js';
4
+ import useCollection from '../middleware/use-collection.js';
5
+ import { validateBatch } from '../middleware/validate-batch.js';
6
+ import { CommentsService } from '../services/comments.js';
7
+ import { MetaService } from '../services/meta.js';
8
+ import asyncHandler from '../utils/async-handler.js';
9
+ import { sanitizeQuery } from '../utils/sanitize-query.js';
10
+ const router = express.Router();
11
+ router.use(useCollection('directus_comments'));
12
+ router.post('/', asyncHandler(async (req, res, next) => {
13
+ const service = new CommentsService({
14
+ accountability: req.accountability,
15
+ schema: req.schema,
16
+ serviceOrigin: 'comments',
17
+ });
18
+ const savedKeys = [];
19
+ if (Array.isArray(req.body)) {
20
+ const keys = await service.createMany(req.body);
21
+ savedKeys.push(...keys);
22
+ }
23
+ else {
24
+ const key = await service.createOne(req.body);
25
+ savedKeys.push(key);
26
+ }
27
+ try {
28
+ if (Array.isArray(req.body)) {
29
+ const records = await service.readMany(savedKeys, req.sanitizedQuery);
30
+ res.locals['payload'] = { data: records };
31
+ }
32
+ else {
33
+ const record = await service.readOne(savedKeys[0], req.sanitizedQuery);
34
+ res.locals['payload'] = { data: record };
35
+ }
36
+ }
37
+ catch (error) {
38
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
39
+ return next();
40
+ }
41
+ throw error;
42
+ }
43
+ return next();
44
+ }), respond);
45
+ const readHandler = asyncHandler(async (req, res, next) => {
46
+ const service = new CommentsService({
47
+ accountability: req.accountability,
48
+ schema: req.schema,
49
+ serviceOrigin: 'comments',
50
+ });
51
+ const metaService = new MetaService({
52
+ accountability: req.accountability,
53
+ schema: req.schema,
54
+ });
55
+ let result;
56
+ if (req.body.keys) {
57
+ result = await service.readMany(req.body.keys, req.sanitizedQuery);
58
+ }
59
+ else {
60
+ result = await service.readByQuery(req.sanitizedQuery);
61
+ }
62
+ const meta = await metaService.getMetaForQuery('directus_comments', req.sanitizedQuery);
63
+ res.locals['payload'] = { data: result, meta };
64
+ return next();
65
+ });
66
+ router.get('/', validateBatch('read'), readHandler, respond);
67
+ router.search('/', validateBatch('read'), readHandler, respond);
68
+ router.get('/:pk', asyncHandler(async (req, res, next) => {
69
+ const service = new CommentsService({
70
+ accountability: req.accountability,
71
+ schema: req.schema,
72
+ serviceOrigin: 'comments',
73
+ });
74
+ const record = await service.readOne(req.params['pk'], req.sanitizedQuery);
75
+ res.locals['payload'] = { data: record || null };
76
+ return next();
77
+ }), respond);
78
+ router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
79
+ const service = new CommentsService({
80
+ accountability: req.accountability,
81
+ schema: req.schema,
82
+ serviceOrigin: 'comments',
83
+ });
84
+ let keys = [];
85
+ if (Array.isArray(req.body)) {
86
+ keys = await service.updateBatch(req.body);
87
+ }
88
+ else if (req.body.keys) {
89
+ keys = await service.updateMany(req.body.keys, req.body.data);
90
+ }
91
+ else {
92
+ const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
93
+ keys = await service.updateByQuery(sanitizedQuery, req.body.data);
94
+ }
95
+ try {
96
+ const result = await service.readMany(keys, req.sanitizedQuery);
97
+ res.locals['payload'] = { data: result };
98
+ }
99
+ catch (error) {
100
+ if (isDirectusError(error, ErrorCode.Forbidden)) {
101
+ return next();
102
+ }
103
+ throw error;
104
+ }
105
+ return next();
106
+ }), respond);
107
+ router.patch('/:pk', asyncHandler(async (req, res, next) => {
108
+ const service = new CommentsService({
109
+ accountability: req.accountability,
110
+ schema: req.schema,
111
+ serviceOrigin: 'comments',
112
+ });
113
+ const primaryKey = await service.updateOne(req.params['pk'], req.body);
114
+ try {
115
+ const record = await service.readOne(primaryKey, req.sanitizedQuery);
116
+ res.locals['payload'] = { data: record };
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('/', validateBatch('delete'), asyncHandler(async (req, _res, next) => {
127
+ const service = new CommentsService({
128
+ accountability: req.accountability,
129
+ schema: req.schema,
130
+ serviceOrigin: 'comments',
131
+ });
132
+ if (Array.isArray(req.body)) {
133
+ await service.deleteMany(req.body);
134
+ }
135
+ else if (req.body.keys) {
136
+ await service.deleteMany(req.body.keys);
137
+ }
138
+ else {
139
+ const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
140
+ await service.deleteByQuery(sanitizedQuery);
141
+ }
142
+ return next();
143
+ }), respond);
144
+ router.delete('/:pk', asyncHandler(async (req, _res, next) => {
145
+ const service = new CommentsService({
146
+ accountability: req.accountability,
147
+ schema: req.schema,
148
+ serviceOrigin: 'comments',
149
+ });
150
+ await service.deleteOne(req.params['pk']);
151
+ return next();
152
+ }), respond);
153
+ export default router;
@@ -154,8 +154,14 @@ router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
154
154
  });
155
155
  const version = await service.readOne(req.params['pk']);
156
156
  const { outdated, mainHash } = await service.verifyHash(version['collection'], version['item'], version['hash']);
157
- const saves = await service.getVersionSavesById(version['id']);
158
- const current = assign({}, ...saves);
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
+ }
159
165
  const main = await service.getMainItem(version['collection'], version['item']);
160
166
  res.locals['payload'] = { data: { outdated, mainHash, current, main } };
161
167
  return next();
@@ -167,9 +173,8 @@ router.post('/:pk/save', asyncHandler(async (req, res, next) => {
167
173
  });
168
174
  const version = await service.readOne(req.params['pk']);
169
175
  const mainItem = await service.getMainItem(version['collection'], version['item']);
170
- await service.save(req.params['pk'], req.body);
171
- const saves = await service.getVersionSavesById(req.params['pk']);
172
- const result = assign(mainItem, ...saves);
176
+ const updatedVersion = await service.save(req.params['pk'], req.body);
177
+ const result = assign(mainItem, updatedVersion);
173
178
  res.locals['payload'] = { data: result || null };
174
179
  return next();
175
180
  }), respond);
@@ -140,6 +140,9 @@ export function getDatabase() {
140
140
  times.delete(queryInfo.__knexUid);
141
141
  }
142
142
  logger.trace(`[${delta ? delta.toFixed(3) : '?'}ms] ${queryInfo.sql} [${(queryInfo.bindings ?? []).join(', ')}]`);
143
+ })
144
+ .on('query-error', (_, queryInfo) => {
145
+ times.delete(queryInfo.__knexUid);
143
146
  });
144
147
  return database;
145
148
  }
@@ -43,7 +43,7 @@ export async function up(knex) {
43
43
  .update({ [constraint.many_field]: null })
44
44
  .whereIn(currentPrimaryKeyField, ids);
45
45
  }
46
- catch (err) {
46
+ catch {
47
47
  logger.error(`${constraint.many_collection}.${constraint.many_field} contains illegal foreign keys which couldn't be set to NULL. Please fix these references and rerun this migration to complete the upgrade.`);
48
48
  if (ids.length < 25) {
49
49
  logger.error(`Items with illegal foreign keys: ${ids.join(', ')}`);
@@ -198,7 +198,7 @@ export async function up(knex) {
198
198
  table.dropForeign('role', foreignConstraint);
199
199
  });
200
200
  }
201
- catch (err) {
201
+ catch {
202
202
  logger.warn('Failed to drop foreign key constraint on `role` column in `directus_permissions` table');
203
203
  }
204
204
  await knex('directus_permissions')
@@ -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,65 @@
1
+ import { Action } from '@directus/constants';
2
+ export async function up(knex) {
3
+ await knex.schema.createTable('directus_comments', (table) => {
4
+ table.uuid('id').primary().notNullable();
5
+ table
6
+ .string('collection', 64)
7
+ .notNullable()
8
+ .references('collection')
9
+ .inTable('directus_collections')
10
+ .onDelete('CASCADE');
11
+ table.string('item').notNullable();
12
+ table.text('comment').notNullable();
13
+ table.timestamp('date_created').defaultTo(knex.fn.now());
14
+ table.timestamp('date_updated').defaultTo(knex.fn.now());
15
+ table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
16
+ // Cannot have two constraints from/to the same table, handled on API side
17
+ table.uuid('user_updated').references('id').inTable('directus_users');
18
+ });
19
+ }
20
+ export async function down(knex) {
21
+ const rowsLimit = 50;
22
+ let hasMore = true;
23
+ while (hasMore) {
24
+ const comments = await knex
25
+ .select('id', 'collection', 'item', 'comment', 'date_created', 'user_created')
26
+ .from('directus_comments')
27
+ .limit(rowsLimit);
28
+ if (comments.length === 0) {
29
+ hasMore = false;
30
+ break;
31
+ }
32
+ await knex.transaction(async (trx) => {
33
+ for (const comment of comments) {
34
+ const migratedRecords = await trx('directus_activity')
35
+ .select('id')
36
+ .where('collection', '=', 'directus_comments')
37
+ .andWhere('item', '=', comment.id)
38
+ .andWhere('action', '=', Action.CREATE)
39
+ .limit(1);
40
+ if (migratedRecords[0]) {
41
+ await trx('directus_activity')
42
+ .update({
43
+ action: Action.COMMENT,
44
+ collection: comment.collection,
45
+ item: comment.item,
46
+ comment: comment.comment,
47
+ })
48
+ .where('id', '=', migratedRecords[0].id);
49
+ }
50
+ else {
51
+ await trx('directus_activity').insert({
52
+ action: Action.COMMENT,
53
+ collection: comment.collection,
54
+ item: comment.item,
55
+ comment: comment.comment,
56
+ user: comment.user_created,
57
+ timestamp: comment.date_created,
58
+ });
59
+ }
60
+ await trx('directus_comments').where('id', '=', comment.id).delete();
61
+ }
62
+ });
63
+ }
64
+ await knex.schema.dropTable('directus_comments');
65
+ }
@@ -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>;