@directus/api 23.0.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.
- package/dist/app.js +2 -0
- package/dist/controllers/activity.js +30 -27
- package/dist/controllers/assets.js +1 -1
- package/dist/controllers/comments.d.ts +2 -0
- package/dist/controllers/comments.js +153 -0
- package/dist/controllers/versions.js +10 -5
- package/dist/database/index.js +3 -0
- package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
- package/dist/database/migrations/20240909A-separate-comments.js +65 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
- package/dist/database/run-ast/lib/get-db-query.js +2 -2
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
- package/dist/database/run-ast/run-ast.js +8 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
- package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
- package/dist/permissions/modules/validate-access/validate-access.js +14 -1
- package/dist/services/activity.d.ts +1 -7
- package/dist/services/activity.js +0 -103
- package/dist/services/assets.js +5 -4
- package/dist/services/collections.js +6 -4
- package/dist/services/comments.d.ts +31 -0
- package/dist/services/comments.js +374 -0
- package/dist/services/graphql/index.js +17 -16
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +3 -1
- package/dist/services/mail/index.d.ts +2 -1
- package/dist/services/mail/index.js +4 -1
- package/dist/services/payload.js +15 -14
- package/dist/services/users.js +1 -0
- package/dist/services/versions.js +59 -44
- package/dist/utils/apply-diff.js +5 -6
- package/dist/utils/get-service.js +3 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/sanitize-schema.js +2 -0
- package/package.json +51 -51
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
111
|
+
const primaryKey = await commentsService.updateOne(req.params['pk'], req.body);
|
|
103
112
|
try {
|
|
104
|
-
const record = await
|
|
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
|
|
127
|
+
const commentsService = new CommentsService({
|
|
119
128
|
accountability: req.accountability,
|
|
120
129
|
schema: req.schema,
|
|
130
|
+
serviceOrigin: 'activity',
|
|
121
131
|
});
|
|
122
|
-
|
|
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,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
|
-
|
|
158
|
-
|
|
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
|
|
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);
|
package/dist/database/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_versions', (table) => {
|
|
3
|
+
table.json('delta');
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export async function down(knex) {
|
|
7
|
+
await knex.schema.alterTable('directus_versions', (table) => {
|
|
8
|
+
table.dropColumn('delta');
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { Filter, Permission, Query
|
|
1
|
+
import type { Filter, Permission, Query } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
|
+
import type { Context } from '../../../permissions/types.js';
|
|
3
4
|
import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
|
|
4
|
-
export
|
|
5
|
+
export type DBQueryOptions = {
|
|
6
|
+
table: string;
|
|
7
|
+
fieldNodes: (FieldNode | FunctionFieldNode)[];
|
|
8
|
+
o2mNodes: O2MNode[];
|
|
9
|
+
query: Query;
|
|
10
|
+
cases: Filter[];
|
|
11
|
+
permissions: Permission[];
|
|
12
|
+
permissionsOnly?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }: DBQueryOptions, { knex, schema }: Context): Knex.QueryBuilder;
|
|
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
|
|
|
9
9
|
import { getNodeAlias } from '../utils/get-field-alias.js';
|
|
10
10
|
import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
|
|
11
11
|
import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
|
|
12
|
-
export function getDBQuery(
|
|
12
|
+
export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }, { knex, schema }) {
|
|
13
13
|
const aliasMap = Object.create(null);
|
|
14
14
|
const env = useEnv();
|
|
15
|
-
const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
|
|
15
|
+
const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly);
|
|
16
16
|
const queryCopy = cloneDeep(query);
|
|
17
17
|
const helpers = getHelpers(knex);
|
|
18
18
|
const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Accountability, PermissionsAction, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
import type { AST } from '../../../types/ast.js';
|
|
4
|
+
type FetchPermittedAstRootFieldsOptions = {
|
|
5
|
+
schema: SchemaOverview;
|
|
6
|
+
accountability: Accountability;
|
|
7
|
+
knex: Knex;
|
|
8
|
+
action: PermissionsAction;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
|
|
12
|
+
* same way as `runAst` but only returns flags (1/null) instead of actual field values.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchPermittedAstRootFields(originalAST: AST, { schema, accountability, knex, action }: FetchPermittedAstRootFieldsOptions): Promise<any>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash-es';
|
|
2
|
+
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
|
|
3
|
+
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
|
|
4
|
+
import { getDBQuery } from '../lib/get-db-query.js';
|
|
5
|
+
import { parseCurrentLevel } from '../lib/parse-current-level.js';
|
|
6
|
+
/**
|
|
7
|
+
* Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
|
|
8
|
+
* same way as `runAst` but only returns flags (1/null) instead of actual field values.
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchPermittedAstRootFields(originalAST, { schema, accountability, knex, action }) {
|
|
11
|
+
const ast = cloneDeep(originalAST);
|
|
12
|
+
const { name: collection, children, cases, query } = ast;
|
|
13
|
+
// Retrieve the database columns to select in the current AST
|
|
14
|
+
const { fieldNodes } = await parseCurrentLevel(schema, collection, children, query);
|
|
15
|
+
let permissions = [];
|
|
16
|
+
if (accountability && !accountability.admin) {
|
|
17
|
+
const policies = await fetchPolicies(accountability, { schema, knex });
|
|
18
|
+
permissions = await fetchPermissions({ action, accountability, policies }, { schema, knex });
|
|
19
|
+
}
|
|
20
|
+
return getDBQuery({
|
|
21
|
+
table: collection,
|
|
22
|
+
fieldNodes,
|
|
23
|
+
o2mNodes: [],
|
|
24
|
+
query,
|
|
25
|
+
cases,
|
|
26
|
+
permissions,
|
|
27
|
+
permissionsOnly: true,
|
|
28
|
+
}, { schema, knex });
|
|
29
|
+
}
|
|
@@ -36,7 +36,14 @@ export async function runAst(originalAST, schema, accountability, options) {
|
|
|
36
36
|
permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
|
|
37
37
|
}
|
|
38
38
|
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
|
|
39
|
-
const dbQuery = getDBQuery(
|
|
39
|
+
const dbQuery = getDBQuery({
|
|
40
|
+
table: collection,
|
|
41
|
+
fieldNodes,
|
|
42
|
+
o2mNodes,
|
|
43
|
+
query,
|
|
44
|
+
cases,
|
|
45
|
+
permissions,
|
|
46
|
+
}, { schema, knex });
|
|
40
47
|
const rawItems = await dbQuery;
|
|
41
48
|
if (!rawItems)
|
|
42
49
|
return null;
|
|
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
|
|
|
6
6
|
/** Don't assign an alias to the column but instead return the column as is */
|
|
7
7
|
noAlias?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
9
|
+
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap, permissionsOnly?: boolean): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
10
10
|
export {};
|
|
@@ -4,7 +4,7 @@ import { parseFilterKey } from '../../../utils/parse-filter-key.js';
|
|
|
4
4
|
import { getHelpers } from '../../helpers/index.js';
|
|
5
5
|
import { applyCaseWhen } from './apply-case-when.js';
|
|
6
6
|
import { getNodeAlias } from './get-field-alias.js';
|
|
7
|
-
export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap) {
|
|
7
|
+
export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly) {
|
|
8
8
|
const helpers = getHelpers(knex);
|
|
9
9
|
return function (fieldNode, options) {
|
|
10
10
|
// Don't assign an alias to the column expression if the field has a whenCase
|
|
@@ -22,7 +22,15 @@ export function getColumnPreprocessor(knex, schema, table, cases, permissions, a
|
|
|
22
22
|
field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];
|
|
23
23
|
}
|
|
24
24
|
let column;
|
|
25
|
-
if (
|
|
25
|
+
if (permissionsOnly) {
|
|
26
|
+
if (noAlias) {
|
|
27
|
+
column = knex.raw(1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
column = knex.raw('1 as ??', [alias]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (field?.type?.startsWith('geometry')) {
|
|
26
34
|
column = helpers.st.asText(table, field.field, rawColumnAlias);
|
|
27
35
|
}
|
|
28
36
|
else if (fieldNode.type === 'functionField') {
|
|
@@ -5,5 +5,6 @@ export interface ValidateItemAccessOptions {
|
|
|
5
5
|
action: PermissionsAction;
|
|
6
6
|
collection: string;
|
|
7
7
|
primaryKeys: PrimaryKey[];
|
|
8
|
+
fields?: string[];
|
|
8
9
|
}
|
|
9
|
-
export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<
|
|
10
|
+
export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<any>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runAst } from '../../../../database/run-ast/run-ast.js';
|
|
1
|
+
import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
|
|
3
2
|
import { processAst } from '../../process-ast/process-ast.js';
|
|
4
3
|
export async function validateItemAccess(options, context) {
|
|
5
4
|
const primaryKeyField = context.schema.collections[options.collection]?.primary;
|
|
@@ -8,17 +7,14 @@ export async function validateItemAccess(options, context) {
|
|
|
8
7
|
}
|
|
9
8
|
// When we're looking up access to specific items, we have to read them from the database to
|
|
10
9
|
// make sure you are allowed to access them.
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const ast = {
|
|
11
|
+
type: 'root',
|
|
12
|
+
name: options.collection,
|
|
13
|
+
query: { limit: options.primaryKeys.length },
|
|
14
|
+
// Act as if every field was a "normal" field
|
|
15
|
+
children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [] })) ?? [],
|
|
16
|
+
cases: [],
|
|
16
17
|
};
|
|
17
|
-
const ast = await getAstFromQuery({
|
|
18
|
-
accountability: options.accountability,
|
|
19
|
-
query,
|
|
20
|
-
collection: options.collection,
|
|
21
|
-
}, context);
|
|
22
18
|
await processAst({ ast, ...options }, context);
|
|
23
19
|
// Inject the filter after the permissions have been processed, as to not require access to the primary key
|
|
24
20
|
ast.query.filter = {
|
|
@@ -26,8 +22,17 @@ export async function validateItemAccess(options, context) {
|
|
|
26
22
|
_in: options.primaryKeys,
|
|
27
23
|
},
|
|
28
24
|
};
|
|
29
|
-
const items = await
|
|
25
|
+
const items = await fetchPermittedAstRootFields(ast, {
|
|
26
|
+
schema: context.schema,
|
|
27
|
+
accountability: options.accountability,
|
|
28
|
+
knex: context.knex,
|
|
29
|
+
action: options.action,
|
|
30
|
+
});
|
|
30
31
|
if (items && items.length === options.primaryKeys.length) {
|
|
32
|
+
const { fields } = options;
|
|
33
|
+
if (fields) {
|
|
34
|
+
return items.every((item) => fields.every((field) => item[field] === 1));
|
|
35
|
+
}
|
|
31
36
|
return true;
|
|
32
37
|
}
|
|
33
38
|
return false;
|