@directus/api 12.1.3 → 13.1.0-beta.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/__utils__/snapshots.js +9 -0
- package/dist/app.js +2 -0
- package/dist/cli/utils/create-env/env-stub.liquid +2 -2
- package/dist/controllers/branches.d.ts +2 -0
- package/dist/controllers/branches.js +190 -0
- package/dist/database/migrations/20230721A-require-shares-fields.js +16 -0
- package/dist/database/migrations/20230823A-add-content-versioning.d.ts +3 -0
- package/dist/database/migrations/20230823A-add-content-versioning.js +26 -0
- package/dist/database/system-data/fields/branches.yaml +19 -0
- package/dist/database/system-data/fields/collections.yaml +19 -0
- package/dist/env.js +4 -2
- package/dist/flows.d.ts +1 -0
- package/dist/flows.js +17 -12
- package/dist/logger.js +1 -2
- package/dist/middleware/respond.js +20 -0
- package/dist/operations/exec/index.js +42 -33
- package/dist/operations/json-web-token/index.d.ts +10 -0
- package/dist/operations/json-web-token/index.js +35 -0
- package/dist/operations/notification/index.js +2 -2
- package/dist/services/branches.d.ts +25 -0
- package/dist/services/branches.js +205 -0
- package/dist/services/fields.js +2 -2
- package/dist/services/graphql/index.js +0 -10
- package/dist/services/server.js +0 -3
- package/dist/services/specifications.js +2 -1
- package/dist/types/collection.d.ts +1 -0
- package/dist/utils/get-default-value.d.ts +4 -1
- package/dist/utils/get-default-value.js +2 -2
- package/dist/utils/redact-object.d.ts +23 -0
- package/dist/utils/{redact.js → redact-object.js} +45 -26
- package/dist/utils/sanitize-query.js +13 -6
- package/dist/utils/validate-query.js +1 -0
- package/package.json +17 -17
- package/dist/utils/redact.d.ts +0 -15
|
@@ -13,6 +13,7 @@ export const snapshotBeforeCreateCollection = {
|
|
|
13
13
|
item_duplication_fields: null,
|
|
14
14
|
note: null,
|
|
15
15
|
singleton: false,
|
|
16
|
+
branches_enabled: false,
|
|
16
17
|
translations: {},
|
|
17
18
|
},
|
|
18
19
|
schema: {
|
|
@@ -86,6 +87,7 @@ export const snapshotCreateCollection = {
|
|
|
86
87
|
item_duplication_fields: null,
|
|
87
88
|
note: null,
|
|
88
89
|
singleton: false,
|
|
90
|
+
branches_enabled: false,
|
|
89
91
|
translations: {},
|
|
90
92
|
},
|
|
91
93
|
schema: {
|
|
@@ -105,6 +107,7 @@ export const snapshotCreateCollection = {
|
|
|
105
107
|
item_duplication_fields: null,
|
|
106
108
|
note: null,
|
|
107
109
|
singleton: false,
|
|
110
|
+
branches_enabled: false,
|
|
108
111
|
translations: {},
|
|
109
112
|
},
|
|
110
113
|
schema: {
|
|
@@ -124,6 +127,7 @@ export const snapshotCreateCollection = {
|
|
|
124
127
|
item_duplication_fields: null,
|
|
125
128
|
note: null,
|
|
126
129
|
singleton: false,
|
|
130
|
+
branches_enabled: false,
|
|
127
131
|
translations: {},
|
|
128
132
|
},
|
|
129
133
|
schema: {
|
|
@@ -287,6 +291,7 @@ export const snapshotCreateCollectionNotNested = {
|
|
|
287
291
|
item_duplication_fields: null,
|
|
288
292
|
note: null,
|
|
289
293
|
singleton: false,
|
|
294
|
+
branches_enabled: false,
|
|
290
295
|
translations: {},
|
|
291
296
|
},
|
|
292
297
|
schema: {
|
|
@@ -306,6 +311,7 @@ export const snapshotCreateCollectionNotNested = {
|
|
|
306
311
|
item_duplication_fields: null,
|
|
307
312
|
note: null,
|
|
308
313
|
singleton: false,
|
|
314
|
+
branches_enabled: false,
|
|
309
315
|
translations: {},
|
|
310
316
|
},
|
|
311
317
|
schema: {
|
|
@@ -424,6 +430,7 @@ export const snapshotBeforeDeleteCollection = {
|
|
|
424
430
|
item_duplication_fields: null,
|
|
425
431
|
note: null,
|
|
426
432
|
singleton: false,
|
|
433
|
+
branches_enabled: false,
|
|
427
434
|
translations: {},
|
|
428
435
|
},
|
|
429
436
|
schema: {
|
|
@@ -443,6 +450,7 @@ export const snapshotBeforeDeleteCollection = {
|
|
|
443
450
|
item_duplication_fields: null,
|
|
444
451
|
note: null,
|
|
445
452
|
singleton: false,
|
|
453
|
+
branches_enabled: false,
|
|
446
454
|
translations: {},
|
|
447
455
|
},
|
|
448
456
|
schema: {
|
|
@@ -462,6 +470,7 @@ export const snapshotBeforeDeleteCollection = {
|
|
|
462
470
|
item_duplication_fields: null,
|
|
463
471
|
note: null,
|
|
464
472
|
singleton: false,
|
|
473
|
+
branches_enabled: false,
|
|
465
474
|
translations: {},
|
|
466
475
|
},
|
|
467
476
|
schema: {
|
package/dist/app.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerAuthProviders } from './auth.js';
|
|
|
10
10
|
import activityRouter from './controllers/activity.js';
|
|
11
11
|
import assetsRouter from './controllers/assets.js';
|
|
12
12
|
import authRouter from './controllers/auth.js';
|
|
13
|
+
import branchesRouter from './controllers/branches.js';
|
|
13
14
|
import collectionsRouter from './controllers/collections.js';
|
|
14
15
|
import dashboardsRouter from './controllers/dashboards.js';
|
|
15
16
|
import extensionsRouter from './controllers/extensions.js';
|
|
@@ -199,6 +200,7 @@ export default async function createApp() {
|
|
|
199
200
|
app.use('/graphql', graphqlRouter);
|
|
200
201
|
app.use('/activity', activityRouter);
|
|
201
202
|
app.use('/assets', assetsRouter);
|
|
203
|
+
app.use('/branches', branchesRouter);
|
|
202
204
|
app.use('/collections', collectionsRouter);
|
|
203
205
|
app.use('/dashboards', dashboardsRouter);
|
|
204
206
|
app.use('/extensions', extensionsRouter);
|
|
@@ -148,8 +148,8 @@ CACHE_ENABLED=false
|
|
|
148
148
|
# How long the cache is persisted ["5m"]
|
|
149
149
|
# CACHE_TTL="30m"
|
|
150
150
|
|
|
151
|
-
# How to scope the cache data ["
|
|
152
|
-
# CACHE_NAMESPACE="
|
|
151
|
+
# How to scope the cache data ["system-cache"]
|
|
152
|
+
# CACHE_NAMESPACE="system-cache"
|
|
153
153
|
|
|
154
154
|
# Automatically purge the cache on create, update, and delete actions. [false]
|
|
155
155
|
# CACHE_AUTO_PURGE=true
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { isDirectusError } from '@directus/errors';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { assign } from 'lodash-es';
|
|
4
|
+
import { ErrorCode, InvalidPayloadError } from '../errors/index.js';
|
|
5
|
+
import { respond } from '../middleware/respond.js';
|
|
6
|
+
import useCollection from '../middleware/use-collection.js';
|
|
7
|
+
import { validateBatch } from '../middleware/validate-batch.js';
|
|
8
|
+
import { BranchesService } from '../services/branches.js';
|
|
9
|
+
import { MetaService } from '../services/meta.js';
|
|
10
|
+
import asyncHandler from '../utils/async-handler.js';
|
|
11
|
+
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
router.use(useCollection('directus_branches'));
|
|
14
|
+
router.post('/', asyncHandler(async (req, res, next) => {
|
|
15
|
+
const service = new BranchesService({
|
|
16
|
+
accountability: req.accountability,
|
|
17
|
+
schema: req.schema,
|
|
18
|
+
});
|
|
19
|
+
const savedKeys = [];
|
|
20
|
+
if (Array.isArray(req.body)) {
|
|
21
|
+
const keys = await service.createMany(req.body);
|
|
22
|
+
savedKeys.push(...keys);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const primaryKey = await service.createOne(req.body);
|
|
26
|
+
savedKeys.push(primaryKey);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
if (Array.isArray(req.body)) {
|
|
30
|
+
const records = await service.readMany(savedKeys, req.sanitizedQuery);
|
|
31
|
+
res.locals['payload'] = { data: records };
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const record = await service.readOne(savedKeys[0], req.sanitizedQuery);
|
|
35
|
+
res.locals['payload'] = { data: record };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isDirectusError(error, ErrorCode.Forbidden)) {
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return next();
|
|
45
|
+
}), respond);
|
|
46
|
+
const readHandler = asyncHandler(async (req, res, next) => {
|
|
47
|
+
const service = new BranchesService({
|
|
48
|
+
accountability: req.accountability,
|
|
49
|
+
schema: req.schema,
|
|
50
|
+
});
|
|
51
|
+
const metaService = new MetaService({
|
|
52
|
+
accountability: req.accountability,
|
|
53
|
+
schema: req.schema,
|
|
54
|
+
});
|
|
55
|
+
let result;
|
|
56
|
+
if (req.singleton) {
|
|
57
|
+
result = await service.readSingleton(req.sanitizedQuery);
|
|
58
|
+
}
|
|
59
|
+
else if (req.body.keys) {
|
|
60
|
+
result = await service.readMany(req.body.keys, req.sanitizedQuery);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
result = await service.readByQuery(req.sanitizedQuery);
|
|
64
|
+
}
|
|
65
|
+
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
|
|
66
|
+
res.locals['payload'] = { data: result, meta };
|
|
67
|
+
return next();
|
|
68
|
+
});
|
|
69
|
+
router.get('/', validateBatch('read'), readHandler, respond);
|
|
70
|
+
router.search('/', validateBatch('read'), readHandler, respond);
|
|
71
|
+
router.get('/:pk', asyncHandler(async (req, res, next) => {
|
|
72
|
+
const service = new BranchesService({
|
|
73
|
+
accountability: req.accountability,
|
|
74
|
+
schema: req.schema,
|
|
75
|
+
});
|
|
76
|
+
const record = await service.readOne(req.params['pk'], req.sanitizedQuery);
|
|
77
|
+
res.locals['payload'] = { data: record || null };
|
|
78
|
+
return next();
|
|
79
|
+
}), respond);
|
|
80
|
+
router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
|
|
81
|
+
const service = new BranchesService({
|
|
82
|
+
accountability: req.accountability,
|
|
83
|
+
schema: req.schema,
|
|
84
|
+
});
|
|
85
|
+
let keys = [];
|
|
86
|
+
if (Array.isArray(req.body)) {
|
|
87
|
+
keys = await service.updateBatch(req.body);
|
|
88
|
+
}
|
|
89
|
+
else if (req.body.keys) {
|
|
90
|
+
keys = await service.updateMany(req.body.keys, req.body.data);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
94
|
+
keys = await service.updateByQuery(sanitizedQuery, req.body.data);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const result = await service.readMany(keys, req.sanitizedQuery);
|
|
98
|
+
res.locals['payload'] = { data: result || null };
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (isDirectusError(error, ErrorCode.Forbidden)) {
|
|
102
|
+
return next();
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
return next();
|
|
107
|
+
}), respond);
|
|
108
|
+
router.patch('/:pk', asyncHandler(async (req, res, next) => {
|
|
109
|
+
const service = new BranchesService({
|
|
110
|
+
accountability: req.accountability,
|
|
111
|
+
schema: req.schema,
|
|
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 || null };
|
|
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 BranchesService({
|
|
128
|
+
accountability: req.accountability,
|
|
129
|
+
schema: req.schema,
|
|
130
|
+
});
|
|
131
|
+
if (Array.isArray(req.body)) {
|
|
132
|
+
await service.deleteMany(req.body);
|
|
133
|
+
}
|
|
134
|
+
else if (req.body.keys) {
|
|
135
|
+
await service.deleteMany(req.body.keys);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
139
|
+
await service.deleteByQuery(sanitizedQuery);
|
|
140
|
+
}
|
|
141
|
+
return next();
|
|
142
|
+
}), respond);
|
|
143
|
+
router.delete('/:pk', asyncHandler(async (req, _res, next) => {
|
|
144
|
+
const service = new BranchesService({
|
|
145
|
+
accountability: req.accountability,
|
|
146
|
+
schema: req.schema,
|
|
147
|
+
});
|
|
148
|
+
await service.deleteOne(req.params['pk']);
|
|
149
|
+
return next();
|
|
150
|
+
}), respond);
|
|
151
|
+
router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
|
|
152
|
+
const service = new BranchesService({
|
|
153
|
+
accountability: req.accountability,
|
|
154
|
+
schema: req.schema,
|
|
155
|
+
});
|
|
156
|
+
const branch = await service.readOne(req.params['pk']);
|
|
157
|
+
const { outdated, mainHash } = await service.verifyHash(branch['collection'], branch['item'], branch['hash']);
|
|
158
|
+
const commits = await service.getBranchCommits(branch['id']);
|
|
159
|
+
const current = assign({}, ...commits);
|
|
160
|
+
const fields = Object.keys(current);
|
|
161
|
+
const mainBranchItem = await service.getMainBranchItem(branch['collection'], branch['item'], fields.length > 0 ? { fields } : undefined);
|
|
162
|
+
res.locals['payload'] = { data: { outdated, mainHash, current, main: mainBranchItem } };
|
|
163
|
+
return next();
|
|
164
|
+
}), respond);
|
|
165
|
+
router.post('/:pk/commit', asyncHandler(async (req, res, next) => {
|
|
166
|
+
const service = new BranchesService({
|
|
167
|
+
accountability: req.accountability,
|
|
168
|
+
schema: req.schema,
|
|
169
|
+
});
|
|
170
|
+
const branch = await service.readOne(req.params['pk']);
|
|
171
|
+
const mainBranchItem = await service.getMainBranchItem(branch['collection'], branch['item']);
|
|
172
|
+
await service.commit(req.params['pk'], req.body);
|
|
173
|
+
const commits = await service.getBranchCommits(req.params['pk']);
|
|
174
|
+
const result = assign(mainBranchItem, ...commits);
|
|
175
|
+
res.locals['payload'] = { data: result || null };
|
|
176
|
+
return next();
|
|
177
|
+
}), respond);
|
|
178
|
+
router.post('/:pk/merge', asyncHandler(async (req, res, next) => {
|
|
179
|
+
if (typeof req.body.mainHash !== 'string') {
|
|
180
|
+
throw new InvalidPayloadError({ reason: `"mainHash" field is required` });
|
|
181
|
+
}
|
|
182
|
+
const service = new BranchesService({
|
|
183
|
+
accountability: req.accountability,
|
|
184
|
+
schema: req.schema,
|
|
185
|
+
});
|
|
186
|
+
const updatedItemKey = await service.merge(req.params['pk'], req.body.mainHash, req.body?.['fields']);
|
|
187
|
+
res.locals['payload'] = { data: updatedItemKey || null };
|
|
188
|
+
return next();
|
|
189
|
+
}), respond);
|
|
190
|
+
export default router;
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
export async function up(knex) {
|
|
2
2
|
await knex.schema.alterTable('directus_shares', (table) => {
|
|
3
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
4
|
+
// Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
|
|
5
|
+
table.dropForeign('collection', 'directus_shares_collection_foreign');
|
|
6
|
+
}
|
|
3
7
|
table.dropNullable('collection');
|
|
8
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
9
|
+
// Recreate foreign key constraint, from 20211211A-add-shares.ts
|
|
10
|
+
table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
|
|
11
|
+
}
|
|
4
12
|
table.dropNullable('item');
|
|
5
13
|
});
|
|
6
14
|
}
|
|
7
15
|
export async function down(knex) {
|
|
8
16
|
await knex.schema.alterTable('directus_shares', (table) => {
|
|
17
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
18
|
+
// Temporary drop foreign key constraint, see https://github.com/directus/directus/issues/19399
|
|
19
|
+
table.dropForeign('collection', 'directus_shares_collection_foreign');
|
|
20
|
+
}
|
|
9
21
|
table.setNullable('collection');
|
|
22
|
+
if (knex.client.constructor.name === 'Client_MySQL') {
|
|
23
|
+
// Recreate foreign key constraint, from 20211211A-add-shares.ts
|
|
24
|
+
table.foreign('collection').references('directus_collections.collection').onDelete('CASCADE');
|
|
25
|
+
}
|
|
10
26
|
table.setNullable('item');
|
|
11
27
|
});
|
|
12
28
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.createTable('directus_branches', (table) => {
|
|
3
|
+
table.uuid('id').primary().notNullable();
|
|
4
|
+
table.string('name').notNullable();
|
|
5
|
+
table.string('collection', 64).references('collection').inTable('directus_collections').onDelete('CASCADE');
|
|
6
|
+
table.string('item');
|
|
7
|
+
table.string('hash').notNullable();
|
|
8
|
+
table.timestamp('date_created').defaultTo(knex.fn.now());
|
|
9
|
+
table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
|
|
10
|
+
});
|
|
11
|
+
await knex.schema.alterTable('directus_collections', (table) => {
|
|
12
|
+
table.boolean('branches_enabled').notNullable().defaultTo(false);
|
|
13
|
+
});
|
|
14
|
+
await knex.schema.alterTable('directus_revisions', (table) => {
|
|
15
|
+
table.uuid('branch').references('id').inTable('directus_branches').onDelete('CASCADE');
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function down(knex) {
|
|
19
|
+
await knex.schema.dropTable('directus_branches');
|
|
20
|
+
await knex.schema.alterTable('directus_collections', (table) => {
|
|
21
|
+
table.dropColumn('branches_enabled');
|
|
22
|
+
});
|
|
23
|
+
await knex.schema.alterTable('directus_revisions', (table) => {
|
|
24
|
+
table.dropColumn('branch');
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
table: directus_branches
|
|
2
|
+
|
|
3
|
+
fields:
|
|
4
|
+
- field: id
|
|
5
|
+
special:
|
|
6
|
+
- uuid
|
|
7
|
+
readonly: true
|
|
8
|
+
hidden: true
|
|
9
|
+
- field: name
|
|
10
|
+
- field: collection
|
|
11
|
+
- field: item
|
|
12
|
+
- field: hash
|
|
13
|
+
- field: date_created
|
|
14
|
+
special:
|
|
15
|
+
- date-created
|
|
16
|
+
- cast-timestamp
|
|
17
|
+
- field: user_created
|
|
18
|
+
special:
|
|
19
|
+
- user-created
|
|
@@ -114,8 +114,27 @@ fields:
|
|
|
114
114
|
interface: system-display-template
|
|
115
115
|
options:
|
|
116
116
|
collectionField: collection
|
|
117
|
+
injectBranchField: true
|
|
117
118
|
width: full
|
|
118
119
|
|
|
120
|
+
- field: content_versioning_divider
|
|
121
|
+
special:
|
|
122
|
+
- alias
|
|
123
|
+
- no-data
|
|
124
|
+
interface: presentation-divider
|
|
125
|
+
options:
|
|
126
|
+
icon: update
|
|
127
|
+
title: $t:field_options.directus_collections.content_versioning_divider
|
|
128
|
+
width: full
|
|
129
|
+
|
|
130
|
+
- field: branches_enabled
|
|
131
|
+
interface: boolean
|
|
132
|
+
special:
|
|
133
|
+
- cast-boolean
|
|
134
|
+
options:
|
|
135
|
+
label: $t:field_options.directus_collections.enable_branches
|
|
136
|
+
width: half
|
|
137
|
+
|
|
119
138
|
- field: archive_divider
|
|
120
139
|
special:
|
|
121
140
|
- alias
|
package/dist/env.js
CHANGED
|
@@ -192,8 +192,9 @@ const allowedEnvironmentVars = [
|
|
|
192
192
|
'RELATIONAL_BATCH_SIZE',
|
|
193
193
|
'EXPORT_BATCH_SIZE',
|
|
194
194
|
// flows
|
|
195
|
-
'FLOWS_EXEC_ALLOWED_MODULES',
|
|
196
195
|
'FLOWS_ENV_ALLOW_LIST',
|
|
196
|
+
'FLOWS_RUN_SCRIPT_MAX_MEMORY',
|
|
197
|
+
'FLOWS_RUN_SCRIPT_TIMEOUT',
|
|
197
198
|
// websockets
|
|
198
199
|
'WEBSOCKETS_.+',
|
|
199
200
|
].map((name) => new RegExp(`^${name}$`));
|
|
@@ -282,8 +283,9 @@ const defaults = {
|
|
|
282
283
|
WEBSOCKETS_GRAPHQL_PATH: '/graphql',
|
|
283
284
|
WEBSOCKETS_HEARTBEAT_ENABLED: true,
|
|
284
285
|
WEBSOCKETS_HEARTBEAT_PERIOD: 30,
|
|
285
|
-
FLOWS_EXEC_ALLOWED_MODULES: false,
|
|
286
286
|
FLOWS_ENV_ALLOW_LIST: false,
|
|
287
|
+
FLOWS_RUN_SCRIPT_MAX_MEMORY: 32,
|
|
288
|
+
FLOWS_RUN_SCRIPT_TIMEOUT: 10000,
|
|
287
289
|
PRESSURE_LIMITER_ENABLED: true,
|
|
288
290
|
PRESSURE_LIMITER_SAMPLE_INTERVAL: 250,
|
|
289
291
|
PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION: 0.99,
|
package/dist/flows.d.ts
CHANGED
package/dist/flows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Action
|
|
2
|
-
import { applyOptionsData, isValidJSON, parseJSON, toArray } from '@directus/utils';
|
|
1
|
+
import { Action } from '@directus/constants';
|
|
2
|
+
import { applyOptionsData, getRedactedString, isValidJSON, parseJSON, toArray } from '@directus/utils';
|
|
3
3
|
import { omit, pick } from 'lodash-es';
|
|
4
4
|
import { get } from 'micromustache';
|
|
5
5
|
import getDatabase from './database/index.js';
|
|
@@ -16,7 +16,7 @@ import { constructFlowTree } from './utils/construct-flow-tree.js';
|
|
|
16
16
|
import { getSchema } from './utils/get-schema.js';
|
|
17
17
|
import { JobQueue } from './utils/job-queue.js';
|
|
18
18
|
import { mapValuesDeep } from './utils/map-values-deep.js';
|
|
19
|
-
import {
|
|
19
|
+
import { redactObject } from './utils/redact-object.js';
|
|
20
20
|
import { sanitizeError } from './utils/sanitize-error.js';
|
|
21
21
|
import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
|
|
22
22
|
let flowManager;
|
|
@@ -38,8 +38,10 @@ class FlowManager {
|
|
|
38
38
|
operationFlowHandlers = {};
|
|
39
39
|
webhookFlowHandlers = {};
|
|
40
40
|
reloadQueue;
|
|
41
|
+
envs;
|
|
41
42
|
constructor() {
|
|
42
43
|
this.reloadQueue = new JobQueue();
|
|
44
|
+
this.envs = env['FLOWS_ENV_ALLOW_LIST'] ? pick(env, toArray(env['FLOWS_ENV_ALLOW_LIST'])) : {};
|
|
43
45
|
const messenger = getMessenger();
|
|
44
46
|
messenger.subscribe('flows', (event) => {
|
|
45
47
|
if (event['type'] === 'reload') {
|
|
@@ -238,7 +240,7 @@ class FlowManager {
|
|
|
238
240
|
[TRIGGER_KEY]: data,
|
|
239
241
|
[LAST_KEY]: data,
|
|
240
242
|
[ACCOUNTABILITY_KEY]: context?.['accountability'] ?? null,
|
|
241
|
-
[ENV_KEY]:
|
|
243
|
+
[ENV_KEY]: this.envs,
|
|
242
244
|
};
|
|
243
245
|
let nextOperation = flow.operation;
|
|
244
246
|
let lastOperationStatus = 'unknown';
|
|
@@ -276,14 +278,17 @@ class FlowManager {
|
|
|
276
278
|
collection: 'directus_flows',
|
|
277
279
|
item: flow.id,
|
|
278
280
|
data: {
|
|
279
|
-
steps: steps,
|
|
280
|
-
data:
|
|
281
|
-
|
|
282
|
-
[
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
steps: steps.map((step) => redactObject(step, { values: this.envs }, getRedactedString)),
|
|
282
|
+
data: redactObject(omit(keyedData, '$accountability.permissions'), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
|
|
283
|
+
{
|
|
284
|
+
keys: [
|
|
285
|
+
['**', 'headers', 'authorization'],
|
|
286
|
+
['**', 'headers', 'cookie'],
|
|
287
|
+
['**', 'query', 'access_token'],
|
|
288
|
+
['**', 'payload', 'password'],
|
|
289
|
+
],
|
|
290
|
+
values: this.envs,
|
|
291
|
+
}, getRedactedString),
|
|
287
292
|
},
|
|
288
293
|
});
|
|
289
294
|
}
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { REDACTED_TEXT } from '@directus/
|
|
2
|
-
import { toArray } from '@directus/utils';
|
|
1
|
+
import { REDACTED_TEXT, toArray } from '@directus/utils';
|
|
3
2
|
import { merge } from 'lodash-es';
|
|
4
3
|
import { pino } from 'pino';
|
|
5
4
|
import { pinoHttp, stdSerializers } from 'pino-http';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { parse as parseBytesConfiguration } from 'bytes';
|
|
2
|
+
import { assign } from 'lodash-es';
|
|
2
3
|
import { getCache, setCacheValue } from '../cache.js';
|
|
3
4
|
import env from '../env.js';
|
|
4
5
|
import logger from '../logger.js';
|
|
6
|
+
import { BranchesService } from '../services/branches.js';
|
|
5
7
|
import { ExportService } from '../services/import-export.js';
|
|
6
8
|
import asyncHandler from '../utils/async-handler.js';
|
|
7
9
|
import { getCacheControlHeader } from '../utils/get-cache-headers.js';
|
|
@@ -39,6 +41,24 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
39
41
|
res.setHeader('Cache-Control', 'no-cache');
|
|
40
42
|
res.setHeader('Vary', 'Origin, Cache-Control');
|
|
41
43
|
}
|
|
44
|
+
if (req.sanitizedQuery.branch &&
|
|
45
|
+
req.collection &&
|
|
46
|
+
(req.singleton || req.params['pk']) &&
|
|
47
|
+
'data' in res.locals['payload']) {
|
|
48
|
+
const branchesService = new BranchesService({ accountability: req.accountability ?? null, schema: req.schema });
|
|
49
|
+
const filter = {
|
|
50
|
+
name: { _eq: req.sanitizedQuery.branch },
|
|
51
|
+
collection: { _eq: req.collection },
|
|
52
|
+
};
|
|
53
|
+
if (req.params['pk']) {
|
|
54
|
+
filter['item'] = { _eq: req.params['pk'] };
|
|
55
|
+
}
|
|
56
|
+
const branch = await branchesService.readByQuery({ filter });
|
|
57
|
+
if (branch[0]) {
|
|
58
|
+
const commits = await branchesService.getBranchCommits(branch[0]['id']);
|
|
59
|
+
assign(res.locals['payload'].data, ...commits);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
42
62
|
if (req.sanitizedQuery.export) {
|
|
43
63
|
const exportService = new ExportService({ accountability: req.accountability ?? null, schema: req.schema });
|
|
44
64
|
let filename = '';
|
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
import { defineOperationApi
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { defineOperationApi } from '@directus/utils';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const ivm = require('isolated-vm');
|
|
5
|
+
/**
|
|
6
|
+
* A helper for making the logs prettier.
|
|
7
|
+
* The logger prints arrays with their indices but this looks "bad" when you have only one argument.
|
|
8
|
+
*/
|
|
9
|
+
function unpackArgs(args) {
|
|
10
|
+
return args.length === 1 ? args[0] : args;
|
|
11
|
+
}
|
|
4
12
|
export default defineOperationApi({
|
|
5
13
|
id: 'exec',
|
|
6
|
-
handler: async ({ code }, { data, env }) => {
|
|
7
|
-
const allowedModules = env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [];
|
|
8
|
-
const allowedModulesBuiltIn = [];
|
|
9
|
-
const allowedModulesExternal = [];
|
|
14
|
+
handler: async ({ code }, { data, env, logger }) => {
|
|
10
15
|
const allowedEnv = data['$env'] ?? {};
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
16
|
+
const isolateSizeMb = env['FLOWS_RUN_SCRIPT_MAX_MEMORY'];
|
|
17
|
+
const scriptTimeoutMs = env['FLOWS_RUN_SCRIPT_TIMEOUT'];
|
|
18
|
+
const isolate = new ivm.Isolate({ memoryLimit: isolateSizeMb });
|
|
19
|
+
const context = isolate.createContextSync();
|
|
20
|
+
const jail = context.global;
|
|
21
|
+
jail.setSync('global', jail.derefInto());
|
|
22
|
+
jail.setSync('process', { env: allowedEnv }, { copy: true });
|
|
23
|
+
jail.setSync('module', { exports: null }, { copy: true });
|
|
24
|
+
jail.setSync('console', {
|
|
25
|
+
log: new ivm.Callback((...args) => logger.info(unpackArgs(args)), { sync: true }),
|
|
26
|
+
info: new ivm.Callback((...args) => logger.info(unpackArgs(args)), { sync: true }),
|
|
27
|
+
warn: new ivm.Callback((...args) => logger.warn(unpackArgs(args)), { sync: true }),
|
|
28
|
+
error: new ivm.Callback((...args) => logger.error(unpackArgs(args)), { sync: true }),
|
|
29
|
+
trace: new ivm.Callback((...args) => logger.trace(unpackArgs(args)), { sync: true }),
|
|
30
|
+
debug: new ivm.Callback((...args) => logger.debug(unpackArgs(args)), { sync: true }),
|
|
31
|
+
}, { copy: true });
|
|
32
|
+
// Run the operation once to define the module.exports function
|
|
33
|
+
await context.eval(code, { timeout: scriptTimeoutMs });
|
|
34
|
+
const inputData = new ivm.ExternalCopy({ data });
|
|
35
|
+
const resultRef = await context.evalClosure(`return module.exports($0.data)`, [inputData.copyInto()], {
|
|
36
|
+
result: { reference: true, promise: true },
|
|
37
|
+
timeout: scriptTimeoutMs,
|
|
38
|
+
});
|
|
39
|
+
const result = await resultRef.copy();
|
|
40
|
+
// Memory cleanup
|
|
41
|
+
resultRef.release();
|
|
42
|
+
inputData.release();
|
|
43
|
+
context.release();
|
|
44
|
+
isolate.dispose();
|
|
45
|
+
return result;
|
|
37
46
|
},
|
|
38
47
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
type Options = {
|
|
3
|
+
operation: string;
|
|
4
|
+
payload?: Record<string, any> | string;
|
|
5
|
+
token?: string;
|
|
6
|
+
secret?: jwt.Secret;
|
|
7
|
+
options?: any;
|
|
8
|
+
};
|
|
9
|
+
declare const _default: import("@directus/types").OperationApiConfig<Options>;
|
|
10
|
+
export default _default;
|