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