@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.
Files changed (34) hide show
  1. package/dist/__utils__/snapshots.js +9 -0
  2. package/dist/app.js +2 -0
  3. package/dist/cli/utils/create-env/env-stub.liquid +2 -2
  4. package/dist/controllers/branches.d.ts +2 -0
  5. package/dist/controllers/branches.js +190 -0
  6. package/dist/database/migrations/20230721A-require-shares-fields.js +16 -0
  7. package/dist/database/migrations/20230823A-add-content-versioning.d.ts +3 -0
  8. package/dist/database/migrations/20230823A-add-content-versioning.js +26 -0
  9. package/dist/database/system-data/fields/branches.yaml +19 -0
  10. package/dist/database/system-data/fields/collections.yaml +19 -0
  11. package/dist/env.js +4 -2
  12. package/dist/flows.d.ts +1 -0
  13. package/dist/flows.js +17 -12
  14. package/dist/logger.js +1 -2
  15. package/dist/middleware/respond.js +20 -0
  16. package/dist/operations/exec/index.js +42 -33
  17. package/dist/operations/json-web-token/index.d.ts +10 -0
  18. package/dist/operations/json-web-token/index.js +35 -0
  19. package/dist/operations/notification/index.js +2 -2
  20. package/dist/services/branches.d.ts +25 -0
  21. package/dist/services/branches.js +205 -0
  22. package/dist/services/fields.js +2 -2
  23. package/dist/services/graphql/index.js +0 -10
  24. package/dist/services/server.js +0 -3
  25. package/dist/services/specifications.js +2 -1
  26. package/dist/types/collection.d.ts +1 -0
  27. package/dist/utils/get-default-value.d.ts +4 -1
  28. package/dist/utils/get-default-value.js +2 -2
  29. package/dist/utils/redact-object.d.ts +23 -0
  30. package/dist/utils/{redact.js → redact-object.js} +45 -26
  31. package/dist/utils/sanitize-query.js +13 -6
  32. package/dist/utils/validate-query.js +1 -0
  33. package/package.json +17 -17
  34. 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 ["directus-cache"]
152
- # CACHE_NAMESPACE="directus-cache"
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,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
@@ -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,3 @@
1
+ import type { Knex } from 'knex';
2
+ export declare function up(knex: Knex): Promise<void>;
3
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,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
@@ -7,6 +7,7 @@ declare class FlowManager {
7
7
  private operationFlowHandlers;
8
8
  private webhookFlowHandlers;
9
9
  private reloadQueue;
10
+ private envs;
10
11
  constructor();
11
12
  initialize(): Promise<void>;
12
13
  reload(): Promise<void>;
package/dist/flows.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Action, REDACTED_TEXT } from '@directus/constants';
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 { redact } from './utils/redact.js';
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]: pick(env, env['FLOWS_ENV_ALLOW_LIST'] ? toArray(env['FLOWS_ENV_ALLOW_LIST']) : []),
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: redact(omit(keyedData, '$accountability.permissions'), // Permissions is a ton of data, and is just a copy of what's in the directus_permissions table
281
- [
282
- ['**', 'headers', 'authorization'],
283
- ['**', 'headers', 'cookie'],
284
- ['**', 'query', 'access_token'],
285
- ['**', 'payload', 'password'],
286
- ], REDACTED_TEXT),
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/constants';
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, toArray } from '@directus/utils';
2
- import { isBuiltin } from 'node:module';
3
- import { NodeVM, VMScript } from 'vm2';
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 opts = {
12
- eval: false,
13
- wasm: false,
14
- env: allowedEnv,
15
- };
16
- for (const module of allowedModules) {
17
- if (isBuiltin(module)) {
18
- allowedModulesBuiltIn.push(module);
19
- }
20
- else {
21
- allowedModulesExternal.push(module);
22
- }
23
- }
24
- if (allowedModules.length > 0) {
25
- opts.require = {
26
- builtin: allowedModulesBuiltIn,
27
- external: {
28
- modules: allowedModulesExternal,
29
- transitive: false,
30
- },
31
- };
32
- }
33
- const vm = new NodeVM(opts);
34
- const script = new VMScript(code).compile();
35
- const fn = await vm.run(script);
36
- return await fn(data);
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;