@directus/api 23.0.0 → 23.1.1

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 (72) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/controllers/activity.js +30 -27
  3. package/dist/controllers/assets.js +1 -1
  4. package/dist/controllers/comments.d.ts +2 -0
  5. package/dist/controllers/comments.js +153 -0
  6. package/dist/controllers/permissions.js +1 -1
  7. package/dist/controllers/users.js +4 -8
  8. package/dist/controllers/versions.js +10 -5
  9. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  10. package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -2
  11. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  12. package/dist/database/helpers/schema/dialects/mssql.js +1 -1
  13. package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -1
  14. package/dist/database/helpers/schema/dialects/mysql.js +2 -2
  15. package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -1
  16. package/dist/database/helpers/schema/dialects/oracle.js +1 -1
  17. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  18. package/dist/database/helpers/schema/dialects/postgres.js +3 -3
  19. package/dist/database/helpers/schema/types.d.ts +1 -1
  20. package/dist/database/helpers/schema/types.js +1 -1
  21. package/dist/database/index.js +3 -0
  22. package/dist/database/migrations/20240806A-permissions-policies.d.ts +0 -3
  23. package/dist/database/migrations/20240806A-permissions-policies.js +8 -94
  24. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  25. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  26. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  27. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  28. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  29. package/dist/database/run-ast/lib/get-db-query.js +3 -3
  30. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  31. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  32. package/dist/database/run-ast/run-ast.js +8 -1
  33. package/dist/database/run-ast/utils/apply-case-when.js +1 -1
  34. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  35. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  36. package/dist/permissions/lib/fetch-permissions.d.ts +1 -1
  37. package/dist/permissions/lib/fetch-permissions.js +4 -1
  38. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  39. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  40. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  41. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  42. package/dist/permissions/utils/fetch-share-info.d.ts +12 -0
  43. package/dist/permissions/utils/fetch-share-info.js +9 -0
  44. package/dist/permissions/utils/get-permissions-for-share.d.ts +4 -0
  45. package/dist/permissions/utils/get-permissions-for-share.js +182 -0
  46. package/dist/permissions/utils/merge-permissions.d.ts +9 -0
  47. package/dist/permissions/utils/merge-permissions.js +118 -0
  48. package/dist/services/activity.d.ts +1 -7
  49. package/dist/services/activity.js +0 -103
  50. package/dist/services/assets.js +5 -4
  51. package/dist/services/authentication.js +1 -10
  52. package/dist/services/collections.js +6 -4
  53. package/dist/services/comments.d.ts +31 -0
  54. package/dist/services/comments.js +378 -0
  55. package/dist/services/graphql/index.js +17 -16
  56. package/dist/services/index.d.ts +1 -0
  57. package/dist/services/index.js +1 -0
  58. package/dist/services/items.js +3 -1
  59. package/dist/services/mail/index.d.ts +2 -1
  60. package/dist/services/mail/index.js +4 -1
  61. package/dist/services/payload.js +15 -14
  62. package/dist/services/shares.d.ts +2 -0
  63. package/dist/services/shares.js +11 -9
  64. package/dist/services/users.js +1 -0
  65. package/dist/services/versions.js +59 -44
  66. package/dist/types/auth.d.ts +0 -7
  67. package/dist/utils/apply-diff.js +5 -6
  68. package/dist/utils/get-accountability-for-token.js +0 -2
  69. package/dist/utils/get-service.js +3 -1
  70. package/dist/utils/sanitize-schema.d.ts +1 -1
  71. package/dist/utils/sanitize-schema.js +2 -0
  72. package/package.json +52 -52
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);
@@ -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;
@@ -72,7 +72,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
72
72
  router.get('/', validateBatch('read'), readHandler, respond);
73
73
  router.search('/', validateBatch('read'), readHandler, respond);
74
74
  router.get('/me', asyncHandler(async (req, res, next) => {
75
- if (!req.accountability?.user && !req.accountability?.role)
75
+ if (!req.accountability?.user && !req.accountability?.role && !req.accountability?.share)
76
76
  throw new ForbiddenError();
77
77
  const result = await fetchAccountabilityCollectionAccess(req.accountability, {
78
78
  schema: req.schema,
@@ -62,16 +62,12 @@ const readHandler = asyncHandler(async (req, res, next) => {
62
62
  router.get('/', validateBatch('read'), readHandler, respond);
63
63
  router.search('/', validateBatch('read'), readHandler, respond);
64
64
  router.get('/me', asyncHandler(async (req, res, next) => {
65
- if (req.accountability?.share_scope) {
66
- const user = {
67
- share: req.accountability?.share,
68
- role: {
69
- id: req.accountability.role,
70
- admin_access: false,
71
- app_access: false,
65
+ if (req.accountability?.share) {
66
+ res.locals['payload'] = {
67
+ data: {
68
+ share: req.accountability?.share,
72
69
  },
73
70
  };
74
- res.locals['payload'] = { data: user };
75
71
  return next();
76
72
  }
77
73
  if (!req.accountability?.user) {
@@ -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);
@@ -7,5 +7,5 @@ export declare class SchemaHelperCockroachDb extends SchemaHelper {
7
7
  constraintName(existingName: string): string;
8
8
  getDatabaseSize(): Promise<number | null>;
9
9
  preprocessBindings(queryParams: Sql): Sql;
10
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
10
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
11
11
  }
@@ -32,8 +32,8 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
32
32
  preprocessBindings(queryParams) {
33
33
  return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
34
34
  }
35
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
36
- if (hasMultiRelationalSort) {
35
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
36
+ if (hasRelationalSort) {
37
37
  /*
38
38
  Cockroach allows aliases to be used in the GROUP BY clause and only needs columns in the GROUP BY clause that
39
39
  are not functionally dependent on the primary key.
@@ -6,5 +6,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
6
6
  formatUUID(uuid: string): string;
7
7
  getDatabaseSize(): Promise<number | null>;
8
8
  preprocessBindings(queryParams: Sql): Sql;
9
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
9
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
10
10
  }
@@ -30,7 +30,7 @@ export class SchemaHelperMSSQL extends SchemaHelper {
30
30
  preprocessBindings(queryParams) {
31
31
  return preprocessBindings(queryParams, { format: (index) => `@p${index}` });
32
32
  }
33
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
33
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
34
34
  /*
35
35
  MSSQL requires all selected columns that are not aggregated over are to be present in the GROUP BY clause
36
36
 
@@ -3,5 +3,5 @@ import { SchemaHelper, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperMySQL extends SchemaHelper {
4
4
  applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
5
5
  getDatabaseSize(): Promise<number | null>;
6
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
6
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
7
  }
@@ -28,8 +28,8 @@ export class SchemaHelperMySQL extends SchemaHelper {
28
28
  return null;
29
29
  }
30
30
  }
31
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
32
- if (hasMultiRelationalSort) {
31
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
32
+ if (hasRelationalSort) {
33
33
  /*
34
34
  ** MySQL **
35
35
 
@@ -10,5 +10,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
10
10
  processFieldType(field: Field): Type;
11
11
  getDatabaseSize(): Promise<number | null>;
12
12
  preprocessBindings(queryParams: Sql): Sql;
13
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
13
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
14
14
  }
@@ -42,7 +42,7 @@ export class SchemaHelperOracle extends SchemaHelper {
42
42
  preprocessBindings(queryParams) {
43
43
  return preprocessBindings(queryParams, { format: (index) => `:${index + 1}` });
44
44
  }
45
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
45
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
46
46
  /*
47
47
  Oracle requires all selected columns that are not aggregated over to be present in the GROUP BY clause
48
48
  aliases can not be used before version 23c.
@@ -3,5 +3,5 @@ import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
3
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
4
4
  getDatabaseSize(): Promise<number | null>;
5
5
  preprocessBindings(queryParams: Sql): Sql;
6
- addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
6
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
7
7
  }
@@ -15,12 +15,12 @@ export class SchemaHelperPostgres extends SchemaHelper {
15
15
  preprocessBindings(queryParams) {
16
16
  return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
17
17
  }
18
- addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
19
- if (hasMultiRelationalSort) {
18
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
19
+ if (hasRelationalSort) {
20
20
  /*
21
21
  Postgres only requires selected columns that are not functionally dependent on the primary key to be
22
22
  included in the GROUP BY clause. Since the results are already grouped by the primary key, we don't need to
23
- always include the sort columns in the GROUP BY but only if there is a multi relational sort involved, eg.
23
+ always include the sort columns in the GROUP BY but only if there is a relational sort involved, eg.
24
24
  a sort column that comes from a related M2O relation.
25
25
 
26
26
  > When GROUP BY is present, or any aggregate functions are present, it is not valid for the SELECT list
@@ -36,5 +36,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
36
36
  */
37
37
  getDatabaseSize(): Promise<number | null>;
38
38
  preprocessBindings(queryParams: Sql): Sql;
39
- addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
39
+ addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
40
40
  }
@@ -97,7 +97,7 @@ export class SchemaHelper extends DatabaseHelper {
97
97
  preprocessBindings(queryParams) {
98
98
  return queryParams;
99
99
  }
100
- addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasMultiRelationalSort) {
100
+ addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasRelationalSort) {
101
101
  // no-op by default
102
102
  }
103
103
  }
@@ -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
  }
@@ -1,6 +1,3 @@
1
1
  import type { Knex } from 'knex';
2
- import type { Permission } from '@directus/types';
3
- export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): any[];
4
- export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): Omit<Permission, 'id' | 'system'>;
5
2
  export declare function up(knex: Knex): Promise<void>;
6
3
  export declare function down(knex: Knex): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { processChunk, toBoolean } from '@directus/utils';
2
- import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
2
+ import { omit } from 'lodash-es';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { useLogger } from '../../logger/index.js';
5
5
  import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
@@ -7,98 +7,7 @@ import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
7
7
  import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
8
8
  import { getSchema } from '../../utils/get-schema.js';
9
9
  import { getSchemaInspector } from '../index.js';
10
- // Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
11
- export function mergePermissions(strategy, ...permissions) {
12
- const allPermissions = flatten(permissions);
13
- const mergedPermissions = allPermissions
14
- .reduce((acc, val) => {
15
- const key = `${val.collection}__${val.action}`;
16
- const current = acc.get(key);
17
- acc.set(key, current ? mergePermission(strategy, current, val) : val);
18
- return acc;
19
- }, new Map())
20
- .values();
21
- return Array.from(mergedPermissions);
22
- }
23
- export function mergePermission(strategy, currentPerm, newPerm) {
24
- const logicalKey = `_${strategy}`;
25
- let { permissions, validation, fields, presets } = currentPerm;
26
- if (newPerm.permissions) {
27
- if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
28
- permissions = {
29
- [logicalKey]: [
30
- ...currentPerm.permissions[logicalKey],
31
- newPerm.permissions,
32
- ],
33
- };
34
- }
35
- else if (currentPerm.permissions) {
36
- // Empty {} supersedes other permissions in _OR merge
37
- if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
38
- permissions = {};
39
- }
40
- else {
41
- permissions = {
42
- [logicalKey]: [currentPerm.permissions, newPerm.permissions],
43
- };
44
- }
45
- }
46
- else {
47
- permissions = {
48
- [logicalKey]: [newPerm.permissions],
49
- };
50
- }
51
- }
52
- if (newPerm.validation) {
53
- if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
54
- validation = {
55
- [logicalKey]: [
56
- ...currentPerm.validation[logicalKey],
57
- newPerm.validation,
58
- ],
59
- };
60
- }
61
- else if (currentPerm.validation) {
62
- // Empty {} supersedes other validations in _OR merge
63
- if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
64
- validation = {};
65
- }
66
- else {
67
- validation = {
68
- [logicalKey]: [currentPerm.validation, newPerm.validation],
69
- };
70
- }
71
- }
72
- else {
73
- validation = {
74
- [logicalKey]: [newPerm.validation],
75
- };
76
- }
77
- }
78
- if (newPerm.fields) {
79
- if (Array.isArray(currentPerm.fields) && strategy === 'or') {
80
- fields = uniq([...currentPerm.fields, ...newPerm.fields]);
81
- }
82
- else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
83
- fields = intersection(currentPerm.fields, newPerm.fields);
84
- }
85
- else {
86
- fields = newPerm.fields;
87
- }
88
- if (fields.includes('*'))
89
- fields = ['*'];
90
- }
91
- if (newPerm.presets) {
92
- presets = merge({}, presets, newPerm.presets);
93
- }
94
- return omit({
95
- ...currentPerm,
96
- permissions,
97
- validation,
98
- fields,
99
- presets,
100
- }, ['id', 'system']);
101
- }
10
+ import { mergePermissions } from '../../permissions/utils/merge-permissions.js';
102
11
  async function fetchRoleAccess(roles, context) {
103
12
  const roleAccess = {
104
13
  admin_access: false,
@@ -286,7 +195,12 @@ export async function down(knex) {
286
195
  const policies = await fetchPolicies({ roles: roleTree, user: null, ip: null }, context);
287
196
  // fetch all of the policies permissions
288
197
  const rawPermissions = await fetchPermissions({
289
- accountability: { role: null, roles: roleTree, user: null, app: roleAccess?.app_access || false },
198
+ accountability: {
199
+ role: null,
200
+ roles: roleTree,
201
+ user: null,
202
+ app: roleAccess?.app_access || false,
203
+ },
290
204
  policies,
291
205
  bypassDynamicVariableProcessing: true,
292
206
  }, context);
@@ -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>;