@directus/api 18.2.0 → 19.0.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 (40) hide show
  1. package/dist/app.js +0 -3
  2. package/dist/auth/drivers/ldap.js +1 -1
  3. package/dist/auth/drivers/local.js +1 -1
  4. package/dist/auth/drivers/oauth2.js +1 -1
  5. package/dist/auth/drivers/openid.js +1 -1
  6. package/dist/cache.js +1 -1
  7. package/dist/controllers/activity.js +1 -1
  8. package/dist/controllers/auth.js +7 -6
  9. package/dist/controllers/extensions.js +30 -0
  10. package/dist/controllers/fields.js +1 -3
  11. package/dist/controllers/webhooks.js +10 -74
  12. package/dist/database/migrations/20240122A-add-report-url-fields.d.ts +3 -0
  13. package/dist/database/migrations/20240122A-add-report-url-fields.js +14 -0
  14. package/dist/database/migrations/20240204A-marketplace.js +17 -5
  15. package/dist/database/migrations/20240305A-change-useragent-type.d.ts +3 -0
  16. package/dist/database/migrations/20240305A-change-useragent-type.js +19 -0
  17. package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +13 -0
  18. package/dist/database/migrations/20240311A-deprecate-webhooks.js +125 -0
  19. package/dist/extensions/manager.d.ts +1 -0
  20. package/dist/extensions/manager.js +4 -1
  21. package/dist/middleware/authenticate.js +1 -1
  22. package/dist/services/extensions.d.ts +3 -0
  23. package/dist/services/extensions.js +40 -9
  24. package/dist/services/fields.d.ts +2 -1
  25. package/dist/services/fields.js +33 -4
  26. package/dist/services/graphql/index.js +1 -1
  27. package/dist/services/relations.js +6 -0
  28. package/dist/services/webhooks.d.ts +7 -4
  29. package/dist/services/webhooks.js +15 -12
  30. package/dist/utils/get-ast-from-query.js +1 -1
  31. package/dist/utils/get-auth-providers.d.ts +3 -1
  32. package/dist/utils/get-auth-providers.js +15 -4
  33. package/dist/utils/get-schema.d.ts +1 -1
  34. package/dist/utils/get-schema.js +52 -29
  35. package/dist/websocket/controllers/base.d.ts +1 -3
  36. package/dist/websocket/controllers/base.js +12 -3
  37. package/license +1 -1
  38. package/package.json +36 -34
  39. package/dist/webhooks.d.ts +0 -4
  40. package/dist/webhooks.js +0 -80
package/dist/app.js CHANGED
@@ -60,7 +60,6 @@ import { getConfigFromEnv } from './utils/get-config-from-env.js';
60
60
  import { Url } from './utils/url.js';
61
61
  import { validateEnv } from './utils/validate-env.js';
62
62
  import { validateStorage } from './utils/validate-storage.js';
63
- import { init as initWebhooks } from './webhooks.js';
64
63
  const require = createRequire(import.meta.url);
65
64
  export default async function createApp() {
66
65
  const env = useEnv();
@@ -240,8 +239,6 @@ export default async function createApp() {
240
239
  app.use(notFoundHandler);
241
240
  app.use(errorHandler);
242
241
  await emitter.emitInit('routes.after', { app });
243
- // Register all webhooks
244
- await initWebhooks();
245
242
  initTelemetry();
246
243
  await emitter.emitInit('app.after', { app });
247
244
  return app;
@@ -299,7 +299,7 @@ export function createLDAPAuthRouter(provider) {
299
299
  ip: getIPFromReq(req),
300
300
  role: null,
301
301
  };
302
- const userAgent = req.get('user-agent');
302
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
303
303
  if (userAgent)
304
304
  accountability.userAgent = userAgent;
305
305
  const origin = req.get('origin');
@@ -51,7 +51,7 @@ export function createLocalAuthRouter(provider) {
51
51
  ip: getIPFromReq(req),
52
52
  role: null,
53
53
  };
54
- const userAgent = req.get('user-agent');
54
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
55
55
  if (userAgent)
56
56
  accountability.userAgent = userAgent;
57
57
  const origin = req.get('origin');
@@ -254,7 +254,7 @@ export function createOAuth2AuthRouter(providerName) {
254
254
  ip: getIPFromReq(req),
255
255
  role: null,
256
256
  };
257
- const userAgent = req.get('user-agent');
257
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
258
258
  if (userAgent)
259
259
  accountability.userAgent = userAgent;
260
260
  const origin = req.get('origin');
@@ -275,7 +275,7 @@ export function createOpenIDAuthRouter(providerName) {
275
275
  ip: getIPFromReq(req),
276
276
  role: null,
277
277
  };
278
- const userAgent = req.get('user-agent');
278
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
279
279
  if (userAgent)
280
280
  accountability.userAgent = userAgent;
281
281
  const origin = req.get('origin');
package/dist/cache.js CHANGED
@@ -80,7 +80,7 @@ export async function getSystemCache(key) {
80
80
  }
81
81
  export async function setSchemaCache(schema) {
82
82
  const { localSchemaCache, sharedSchemaCache } = getCache();
83
- const schemaHash = await getSimpleHash(JSON.stringify(schema));
83
+ const schemaHash = getSimpleHash(JSON.stringify(schema));
84
84
  await sharedSchemaCache.set('hash', schemaHash);
85
85
  await localSchemaCache.set('schema', schema);
86
86
  await localSchemaCache.set('hash', schemaHash);
@@ -70,7 +70,7 @@ router.post('/comment', asyncHandler(async (req, res, next) => {
70
70
  action: Action.COMMENT,
71
71
  user: req.accountability?.user,
72
72
  ip: getIPFromReq(req),
73
- user_agent: req.get('user-agent'),
73
+ user_agent: req.accountability?.userAgent,
74
74
  origin: req.get('origin'),
75
75
  });
76
76
  try {
@@ -74,7 +74,7 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
74
74
  ip: getIPFromReq(req),
75
75
  role: null,
76
76
  };
77
- const userAgent = req.get('user-agent');
77
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
78
78
  if (userAgent)
79
79
  accountability.userAgent = userAgent;
80
80
  const origin = req.get('origin');
@@ -114,7 +114,7 @@ router.post('/logout', asyncHandler(async (req, res, next) => {
114
114
  ip: getIPFromReq(req),
115
115
  role: null,
116
116
  };
117
- const userAgent = req.get('user-agent');
117
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
118
118
  if (userAgent)
119
119
  accountability.userAgent = userAgent;
120
120
  const origin = req.get('origin');
@@ -148,7 +148,7 @@ router.post('/password/request', asyncHandler(async (req, _res, next) => {
148
148
  ip: getIPFromReq(req),
149
149
  role: null,
150
150
  };
151
- const userAgent = req.get('user-agent');
151
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
152
152
  if (userAgent)
153
153
  accountability.userAgent = userAgent;
154
154
  const origin = req.get('origin');
@@ -180,7 +180,7 @@ router.post('/password/reset', asyncHandler(async (req, _res, next) => {
180
180
  ip: getIPFromReq(req),
181
181
  role: null,
182
182
  };
183
- const userAgent = req.get('user-agent');
183
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
184
184
  if (userAgent)
185
185
  accountability.userAgent = userAgent;
186
186
  const origin = req.get('origin');
@@ -190,9 +190,10 @@ router.post('/password/reset', asyncHandler(async (req, _res, next) => {
190
190
  await service.resetPassword(req.body.token, req.body.password);
191
191
  return next();
192
192
  }), respond);
193
- router.get('/', asyncHandler(async (_req, res, next) => {
193
+ router.get('/', asyncHandler(async (req, res, next) => {
194
+ const sessionOnly = 'sessionOnly' in req.query && (req.query['sessionOnly'] === '' || Boolean(req.query['sessionOnly']));
194
195
  res.locals['payload'] = {
195
- data: getAuthProviders(),
196
+ data: getAuthProviders({ sessionOnly }),
196
197
  disableDefault: env['AUTH_DISABLE_DEFAULT'],
197
198
  };
198
199
  return next();
@@ -101,6 +101,36 @@ router.post('/registry/install', asyncHandler(async (req, _res, next) => {
101
101
  await service.install(extension, version);
102
102
  return next();
103
103
  }), respond);
104
+ router.post('/registry/reinstall', asyncHandler(async (req, _res, next) => {
105
+ if (req.accountability && req.accountability.admin !== true) {
106
+ throw new ForbiddenError();
107
+ }
108
+ const { extension } = req.body;
109
+ if (!extension) {
110
+ throw new ForbiddenError();
111
+ }
112
+ const service = new ExtensionsService({
113
+ accountability: req.accountability,
114
+ schema: req.schema,
115
+ });
116
+ await service.reinstall(extension);
117
+ return next();
118
+ }), respond);
119
+ router.delete(`/registry/uninstall/:pk(${UUID_REGEX})`, asyncHandler(async (req, _res, next) => {
120
+ if (req.accountability && req.accountability.admin !== true) {
121
+ throw new ForbiddenError();
122
+ }
123
+ const pk = req.params['pk'];
124
+ if (typeof pk !== 'string') {
125
+ throw new ForbiddenError();
126
+ }
127
+ const service = new ExtensionsService({
128
+ accountability: req.accountability,
129
+ schema: req.schema,
130
+ });
131
+ await service.uninstall(pk);
132
+ return next();
133
+ }), respond);
104
134
  router.patch(`/:pk(${UUID_REGEX})`, asyncHandler(async (req, res, next) => {
105
135
  if (req.accountability && req.accountability.admin !== true) {
106
136
  throw new ForbiddenError();
@@ -85,9 +85,7 @@ router.patch('/:collection', validateCollection, asyncHandler(async (req, res, n
85
85
  if (Array.isArray(req.body) === false) {
86
86
  throw new InvalidPayloadError({ reason: 'Submitted body has to be an array' });
87
87
  }
88
- for (const field of req.body) {
89
- await service.updateField(req.params['collection'], field);
90
- }
88
+ await service.updateFields(req.params['collection'], req.body);
91
89
  try {
92
90
  const results = [];
93
91
  for (const field of req.body) {
@@ -1,6 +1,5 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, createError } from '@directus/errors';
2
2
  import express from 'express';
3
- import { ErrorCode } from '@directus/errors';
4
3
  import { respond } from '../middleware/respond.js';
5
4
  import useCollection from '../middleware/use-collection.js';
6
5
  import { validateBatch } from '../middleware/validate-batch.js';
@@ -10,37 +9,9 @@ import asyncHandler from '../utils/async-handler.js';
10
9
  import { sanitizeQuery } from '../utils/sanitize-query.js';
11
10
  const router = express.Router();
12
11
  router.use(useCollection('directus_webhooks'));
13
- router.post('/', asyncHandler(async (req, res, next) => {
14
- const service = new WebhooksService({
15
- accountability: req.accountability,
16
- schema: req.schema,
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 items = await service.readMany(savedKeys, req.sanitizedQuery);
30
- res.locals['payload'] = { data: items };
31
- }
32
- else {
33
- const item = await service.readOne(savedKeys[0], req.sanitizedQuery);
34
- res.locals['payload'] = { data: item };
35
- }
36
- }
37
- catch (error) {
38
- if (isDirectusError(error, ErrorCode.Forbidden)) {
39
- return next();
40
- }
41
- throw error;
42
- }
43
- return next();
12
+ router.post('/', asyncHandler(async (_req, _res, _next) => {
13
+ // Disallow creation of new Webhooks as part of the deprecation, see https://github.com/directus/directus/issues/15553
14
+ throw new (createError(ErrorCode.MethodNotAllowed, 'Webhooks are deprecated, use Flows instead', 405))();
44
15
  }), respond);
45
16
  const readHandler = asyncHandler(async (req, res, next) => {
46
17
  const service = new WebhooksService({
@@ -67,48 +38,13 @@ router.get('/:pk', asyncHandler(async (req, res, next) => {
67
38
  res.locals['payload'] = { data: record || null };
68
39
  return next();
69
40
  }), respond);
70
- router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
71
- const service = new WebhooksService({
72
- accountability: req.accountability,
73
- schema: req.schema,
74
- });
75
- let keys = [];
76
- if (req.body.keys) {
77
- keys = await service.updateMany(req.body.keys, req.body.data);
78
- }
79
- else {
80
- const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
81
- keys = await service.updateByQuery(sanitizedQuery, req.body.data);
82
- }
83
- try {
84
- const result = await service.readMany(keys, req.sanitizedQuery);
85
- res.locals['payload'] = { data: result };
86
- }
87
- catch (error) {
88
- if (isDirectusError(error, ErrorCode.Forbidden)) {
89
- return next();
90
- }
91
- throw error;
92
- }
93
- return next();
41
+ router.patch('/', validateBatch('update'), asyncHandler(async (_req, _res, _next) => {
42
+ // Disallow patching of Webhooks as part of the deprecation, see https://github.com/directus/directus/issues/15553
43
+ throw new (createError(ErrorCode.MethodNotAllowed, 'Webhooks are deprecated, use Flows instead', 405))();
94
44
  }), respond);
95
- router.patch('/:pk', asyncHandler(async (req, res, next) => {
96
- const service = new WebhooksService({
97
- accountability: req.accountability,
98
- schema: req.schema,
99
- });
100
- const primaryKey = await service.updateOne(req.params['pk'], req.body);
101
- try {
102
- const item = await service.readOne(primaryKey, req.sanitizedQuery);
103
- res.locals['payload'] = { data: item || null };
104
- }
105
- catch (error) {
106
- if (isDirectusError(error, ErrorCode.Forbidden)) {
107
- return next();
108
- }
109
- throw error;
110
- }
111
- return next();
45
+ router.patch('/:pk', asyncHandler(async (_req, _res, _next) => {
46
+ // Disallow patching of Webhooks as part of the deprecation, see https://github.com/directus/directus/issues/15553
47
+ throw new (createError(ErrorCode.MethodNotAllowed, 'Webhooks are deprecated, use Flows instead', 405))();
112
48
  }), respond);
113
49
  router.delete('/', asyncHandler(async (req, _res, next) => {
114
50
  const service = new WebhooksService({
@@ -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,14 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_settings', (table) => {
3
+ table.string('report_error_url').nullable();
4
+ table.string('report_bug_url').nullable();
5
+ table.string('report_feature_url').nullable();
6
+ });
7
+ }
8
+ export async function down(knex) {
9
+ await knex.schema.alterTable('directus_settings', (table) => {
10
+ table.dropColumn('report_error_url');
11
+ table.dropColumn('report_bug_url');
12
+ table.dropColumn('report_feature_url');
13
+ });
14
+ }
@@ -32,7 +32,7 @@ export async function up(knex) {
32
32
  source = 'local';
33
33
  }
34
34
  await knex('directus_extensions').update({ id, source, folder: name }).where({ name });
35
- idMap.set(name, id);
35
+ idMap.set(name, { id, source });
36
36
  }
37
37
  }
38
38
  for (const { name } of installedExtensions) {
@@ -44,16 +44,28 @@ export async function up(knex) {
44
44
  continue;
45
45
  const isScopedModuleBundleChild = name.startsWith('@') && splittedName.length > 2;
46
46
  const bundleParentName = isScopedModuleBundleParent || isScopedModuleBundleChild ? splittedName.slice(0, 2).join('/') : splittedName[0];
47
- const bundleParentId = idMap.get(bundleParentName);
48
- if (!bundleParentId)
47
+ const bundleParent = idMap.get(bundleParentName);
48
+ if (!bundleParent)
49
49
  continue;
50
50
  await knex('directus_extensions')
51
- .update({ bundle: bundleParentId, folder: name.substring(bundleParentName.length + 1) })
51
+ .update({
52
+ bundle: bundleParent.id,
53
+ folder: name.substring(bundleParentName.length + 1),
54
+ source: bundleParent.source,
55
+ })
52
56
  .where({ folder: name });
53
57
  }
58
+ await knex.schema.alterTable('directus_extensions', (table) => {
59
+ table.uuid('id').alter().notNullable();
60
+ });
61
+ await knex.transaction(async (trx) => {
62
+ await trx.schema.alterTable('directus_extensions', (table) => {
63
+ table.dropPrimary();
64
+ table.primary(['id']);
65
+ });
66
+ });
54
67
  await knex.schema.alterTable('directus_extensions', (table) => {
55
68
  table.dropColumn('name');
56
- table.uuid('id').alter().primary().notNullable();
57
69
  table.string('source').alter().notNullable();
58
70
  table.string('folder').alter().notNullable();
59
71
  });
@@ -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,19 @@
1
+ import { getHelpers } from '../helpers/index.js';
2
+ export async function up(knex) {
3
+ const helper = getHelpers(knex).schema;
4
+ await Promise.all([
5
+ helper.changeToType('directus_activity', 'user_agent', 'text'),
6
+ helper.changeToType('directus_sessions', 'user_agent', 'text'),
7
+ ]);
8
+ }
9
+ export async function down(knex) {
10
+ const helper = await getHelpers(knex).schema;
11
+ const opts = {
12
+ nullable: false,
13
+ length: 255,
14
+ };
15
+ await Promise.all([
16
+ helper.changeToType('directus_activity', 'user_agent', 'string', opts),
17
+ helper.changeToType('directus_sessions', 'user_agent', 'string', opts),
18
+ ]);
19
+ }
@@ -0,0 +1,13 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * 0. Identify and persist which webhooks were active before deprecation
4
+ * 1. Migrate existing webhooks over to identically behaving Flows
5
+ * 2. Disable existing webhooks
6
+ * 3. Dont drop webhooks yet
7
+ */
8
+ export declare function up(knex: Knex): Promise<void>;
9
+ /**
10
+ * Dont completely drop Webhooks yet.
11
+ * Lets preserve the data and drop them in the next release to be extra safe with users data.
12
+ */
13
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,125 @@
1
+ import { parseJSON, toArray } from '@directus/utils';
2
+ import { randomUUID } from 'node:crypto';
3
+ // To avoid typos
4
+ const TABLE_WEBHOOKS = 'directus_webhooks';
5
+ const TABLE_FLOWS = 'directus_flows';
6
+ const TABLE_OPERATIONS = 'directus_operations';
7
+ const NEW_COLUMN_WAS_ACTIVE = 'was_active_before_deprecation';
8
+ const NEW_COLUMN_FLOW = 'migrated_flow';
9
+ /**
10
+ * 0. Identify and persist which webhooks were active before deprecation
11
+ * 1. Migrate existing webhooks over to identically behaving Flows
12
+ * 2. Disable existing webhooks
13
+ * 3. Dont drop webhooks yet
14
+ */
15
+ export async function up(knex) {
16
+ // 0. Identify and persist which webhooks were active before deprecation
17
+ await knex.schema.alterTable(TABLE_WEBHOOKS, (table) => {
18
+ table.boolean(NEW_COLUMN_WAS_ACTIVE).notNullable().defaultTo(false);
19
+ table.uuid(NEW_COLUMN_FLOW).nullable();
20
+ table.foreign(NEW_COLUMN_FLOW).references(`${TABLE_FLOWS}.id`).onDelete('SET NULL');
21
+ });
22
+ await knex(TABLE_WEBHOOKS)
23
+ .update({ [NEW_COLUMN_WAS_ACTIVE]: true })
24
+ .where({ status: 'active' });
25
+ const webhooks = await knex.select('*').from(TABLE_WEBHOOKS);
26
+ // 1. Migrate existing webhooks over to identically behaving Flows
27
+ await knex.transaction(async (trx) => {
28
+ for (const webhook of webhooks) {
29
+ const flowId = randomUUID();
30
+ const operationIdRunScript = randomUUID();
31
+ const operationIdWebhook = randomUUID();
32
+ const newFlow = {
33
+ id: flowId,
34
+ name: webhook.name,
35
+ icon: 'webhook',
36
+ // color: null,
37
+ description: 'Auto-generated by Directus Migration\n\nWebhooks were deprecated and have been moved into Flows for you automatically.',
38
+ status: webhook.status, // Only activate already active webhooks!
39
+ trigger: 'event',
40
+ // accountability: "all",
41
+ options: {
42
+ type: 'action',
43
+ scope: toArray(webhook.actions)
44
+ .filter((action) => action.trim() !== '')
45
+ .map((scope) => `items.${scope}`),
46
+ collections: toArray(webhook.collections).filter((collection) => collection.trim() !== ''),
47
+ },
48
+ operation: null, // Fill this in later --> `operationIdRunScript`
49
+ // date_created: Default Date,
50
+ // user_created: null,
51
+ };
52
+ const operationRunScript = {
53
+ id: operationIdRunScript,
54
+ name: 'Transforming Payload for Webhook',
55
+ key: 'payload-transform',
56
+ type: 'exec',
57
+ position_x: 19,
58
+ position_y: 1,
59
+ options: {
60
+ code: '/**\n * Webhook data slightly differs from Flow trigger data.\n * This flow converts the new Flow format into the older Webhook shape\n * in order to not break existing logic of consumers.\n */ \nmodule.exports = async function(data) {\n const crudOperation = data.$trigger.event.split(".").at(-1);\n const keyOrKeys = crudOperation === "create" ? "key" : "keys";\n return {\n event: `items.${crudOperation}`,\n accountability: { user: data.$accountability.user, role: data.$accountability.role },\n payload: data.$trigger.payload,\n [keyOrKeys]: data.$trigger?.[keyOrKeys],\n collection: data.$trigger.collection,\n };\n}',
61
+ },
62
+ resolve: operationIdWebhook,
63
+ reject: null,
64
+ flow: flowId,
65
+ // date_created: "",
66
+ // user_created: "",
67
+ };
68
+ const operationWebhook = {
69
+ id: operationIdWebhook,
70
+ name: 'Webhook',
71
+ key: 'webhook',
72
+ type: 'request',
73
+ position_x: 37,
74
+ position_y: 1,
75
+ options: {
76
+ url: webhook.url,
77
+ body: webhook.data ? '{{$last}}' : undefined,
78
+ method: webhook.method,
79
+ headers: typeof webhook.headers === 'string' ? parseJSON(webhook.headers) : webhook.headers,
80
+ },
81
+ resolve: null,
82
+ reject: null,
83
+ flow: flowId,
84
+ // date_created: "",
85
+ // user_created: "",
86
+ };
87
+ await trx(TABLE_FLOWS).insert(newFlow);
88
+ // Only need to transform the payload if the webhook enabled transmitting it
89
+ if (webhook.data && webhook.method !== 'GET') {
90
+ // Order is important due to IDs
91
+ await trx(TABLE_OPERATIONS).insert(operationWebhook);
92
+ await trx(TABLE_OPERATIONS).insert(operationRunScript);
93
+ await trx(TABLE_FLOWS).update({ operation: operationIdRunScript }).where({ id: flowId });
94
+ }
95
+ else {
96
+ operationWebhook.position_x = 19; // Reset it because the Run-Script will be missing
97
+ await trx(TABLE_OPERATIONS).insert(operationWebhook);
98
+ await trx(TABLE_FLOWS).update({ operation: operationIdWebhook }).where({ id: flowId });
99
+ }
100
+ // Persist new Flow/s so that we can retroactively remove them on potential down-migrations
101
+ await trx(TABLE_WEBHOOKS)
102
+ .update({ [NEW_COLUMN_FLOW]: flowId })
103
+ .where({ id: webhook.id });
104
+ // 2. Disable existing webhooks
105
+ await trx(TABLE_WEBHOOKS).update({ status: 'inactive' });
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * Dont completely drop Webhooks yet.
111
+ * Lets preserve the data and drop them in the next release to be extra safe with users data.
112
+ */
113
+ export async function down(knex) {
114
+ // Remove Flows created by the up-migration
115
+ const migratedFlowIds = (await knex(TABLE_WEBHOOKS).select(NEW_COLUMN_FLOW).whereNotNull(NEW_COLUMN_FLOW)).map((col) => col[NEW_COLUMN_FLOW]);
116
+ await knex(TABLE_FLOWS).delete().whereIn('id', migratedFlowIds);
117
+ // Restore previously activated webhooks
118
+ await knex(TABLE_WEBHOOKS)
119
+ .update({ status: 'active' })
120
+ .where({ [NEW_COLUMN_WAS_ACTIVE]: true });
121
+ // Cleanup of up-migration
122
+ await knex.schema.alterTable(TABLE_WEBHOOKS, (table) => {
123
+ table.dropColumns(NEW_COLUMN_WAS_ACTIVE, NEW_COLUMN_FLOW);
124
+ });
125
+ }
@@ -81,6 +81,7 @@ export declare class ExtensionManager {
81
81
  */
82
82
  install(versionId: string): Promise<void>;
83
83
  uninstall(folder: string): Promise<void>;
84
+ broadcastReloadNotification(): Promise<void>;
84
85
  /**
85
86
  * Load all extensions from disk and register them in their respective places
86
87
  */
@@ -171,11 +171,14 @@ export class ExtensionManager {
171
171
  async install(versionId) {
172
172
  await this.installationManager.install(versionId);
173
173
  await this.reload({ forceSync: true });
174
- await this.messenger.publish(this.reloadChannel, { origin: this.processId });
174
+ await this.broadcastReloadNotification();
175
175
  }
176
176
  async uninstall(folder) {
177
177
  await this.installationManager.uninstall(folder);
178
178
  await this.reload({ forceSync: true });
179
+ await this.broadcastReloadNotification();
180
+ }
181
+ async broadcastReloadNotification() {
179
182
  await this.messenger.publish(this.reloadChannel, { origin: this.processId });
180
183
  }
181
184
  /**
@@ -15,7 +15,7 @@ export const handler = async (req, _res, next) => {
15
15
  app: false,
16
16
  ip: getIPFromReq(req),
17
17
  };
18
- const userAgent = req.get('user-agent');
18
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
19
19
  if (userAgent)
20
20
  defaultAccountability.userAgent = userAgent;
21
21
  const origin = req.get('origin');
@@ -15,7 +15,10 @@ export declare class ExtensionsService {
15
15
  extensionsItemService: ItemsService<ExtensionSettings>;
16
16
  extensionsManager: ExtensionManager;
17
17
  constructor(options: AbstractServiceOptions);
18
+ private preInstall;
18
19
  install(extensionId: string, versionId: string): Promise<void>;
20
+ uninstall(id: string): Promise<void>;
21
+ reinstall(id: string): Promise<void>;
19
22
  readAll(): Promise<ApiOutput[]>;
20
23
  readOne(id: string): Promise<ApiOutput>;
21
24
  updateOne(id: string, data: DeepPartial<ApiOutput>): Promise<ApiOutput>;
@@ -29,7 +29,7 @@ export class ExtensionsService {
29
29
  accountability: this.accountability,
30
30
  });
31
31
  }
32
- async install(extensionId, versionId) {
32
+ async preInstall(extensionId, versionId) {
33
33
  const env = useEnv();
34
34
  const describeOptions = {};
35
35
  if (typeof env['MARKETPLACE_REGISTRY'] === 'string') {
@@ -54,6 +54,10 @@ export class ExtensionsService {
54
54
  throw new LimitExceededError();
55
55
  }
56
56
  }
57
+ return { extension, version };
58
+ }
59
+ async install(extensionId, versionId) {
60
+ const { extension, version } = await this.preInstall(extensionId, versionId);
57
61
  await this.extensionsItemService.createOne({
58
62
  id: extensionId,
59
63
  enabled: true,
@@ -71,6 +75,38 @@ export class ExtensionsService {
71
75
  }
72
76
  await this.extensionsManager.install(versionId);
73
77
  }
78
+ async uninstall(id) {
79
+ const settings = await this.extensionsItemService.readOne(id);
80
+ if (settings.source !== 'registry') {
81
+ throw new InvalidPayloadError({
82
+ reason: 'Cannot uninstall extensions that were not installed via marketplace',
83
+ });
84
+ }
85
+ if (settings.bundle !== null) {
86
+ throw new InvalidPayloadError({
87
+ reason: 'Cannot uninstall sub extensions of bundles separately',
88
+ });
89
+ }
90
+ await this.deleteOne(id);
91
+ await this.extensionsManager.uninstall(settings.folder);
92
+ }
93
+ async reinstall(id) {
94
+ const settings = await this.extensionsItemService.readOne(id);
95
+ if (settings.source !== 'registry') {
96
+ throw new InvalidPayloadError({
97
+ reason: 'Cannot reinstall extensions that were not installed via marketplace',
98
+ });
99
+ }
100
+ if (settings.bundle !== null) {
101
+ throw new InvalidPayloadError({
102
+ reason: 'Cannot reinstall sub extensions of bundles separately',
103
+ });
104
+ }
105
+ const extensionId = settings.id;
106
+ const versionId = settings.folder;
107
+ await this.preInstall(extensionId, versionId);
108
+ await this.extensionsManager.install(versionId);
109
+ }
74
110
  async readAll() {
75
111
  const settings = await this.extensionsItemService.readByQuery({ limit: -1 });
76
112
  const regular = settings.filter(({ bundle }) => bundle === null);
@@ -133,19 +169,14 @@ export class ExtensionsService {
133
169
  }
134
170
  return extension;
135
171
  });
136
- this.extensionsManager.reload();
172
+ this.extensionsManager.reload().then(() => {
173
+ this.extensionsManager.broadcastReloadNotification();
174
+ });
137
175
  return result;
138
176
  }
139
177
  async deleteOne(id) {
140
- const settings = await this.extensionsItemService.readOne(id);
141
- if (settings.source !== 'registry') {
142
- throw new InvalidPayloadError({
143
- reason: 'Cannot uninstall extensions that were not installed from the marketplace registry',
144
- });
145
- }
146
178
  await this.extensionsItemService.deleteOne(id);
147
179
  await this.extensionsItemService.deleteByQuery({ filter: { bundle: { _eq: id } } });
148
- await this.extensionsManager.uninstall(settings.folder);
149
180
  }
150
181
  /**
151
182
  * Sync a bundles enabled status
@@ -3,9 +3,9 @@ import type { Accountability, Field, RawField, SchemaOverview, Type } from '@dir
3
3
  import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Helpers } from '../database/helpers/index.js';
6
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
6
7
  import { ItemsService } from './items.js';
7
8
  import { PayloadService } from './payload.js';
8
- import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
9
9
  export declare class FieldsService {
10
10
  knex: Knex;
11
11
  helpers: Helpers;
@@ -26,6 +26,7 @@ export declare class FieldsService {
26
26
  }, table?: Knex.CreateTableBuilder, // allows collection creation to
27
27
  opts?: MutationOptions): Promise<void>;
28
28
  updateField(collection: string, field: RawField, opts?: MutationOptions): Promise<string>;
29
+ updateFields(collection: string, fields: RawField[], opts?: MutationOptions): Promise<string[]>;
29
30
  deleteField(collection: string, field: string, opts?: MutationOptions): Promise<void>;
30
31
  addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, alter?: Column | null): void;
31
32
  }