@directus/api 18.2.1 → 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.
- package/dist/app.js +0 -3
- package/dist/auth/drivers/ldap.js +1 -1
- package/dist/auth/drivers/local.js +1 -1
- package/dist/auth/drivers/oauth2.js +1 -1
- package/dist/auth/drivers/openid.js +1 -1
- package/dist/cache.js +1 -1
- package/dist/controllers/activity.js +1 -1
- package/dist/controllers/auth.js +7 -6
- package/dist/controllers/extensions.js +30 -0
- package/dist/controllers/fields.js +1 -3
- package/dist/controllers/webhooks.js +10 -74
- package/dist/database/migrations/20240122A-add-report-url-fields.d.ts +3 -0
- package/dist/database/migrations/20240122A-add-report-url-fields.js +14 -0
- package/dist/database/migrations/20240204A-marketplace.js +17 -5
- package/dist/database/migrations/20240305A-change-useragent-type.d.ts +3 -0
- package/dist/database/migrations/20240305A-change-useragent-type.js +19 -0
- package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +13 -0
- package/dist/database/migrations/20240311A-deprecate-webhooks.js +125 -0
- package/dist/extensions/manager.d.ts +1 -0
- package/dist/extensions/manager.js +4 -1
- package/dist/middleware/authenticate.js +1 -1
- package/dist/services/extensions.d.ts +3 -0
- package/dist/services/extensions.js +40 -9
- package/dist/services/fields.d.ts +2 -1
- package/dist/services/fields.js +33 -4
- package/dist/services/relations.js +6 -0
- package/dist/services/webhooks.d.ts +7 -4
- package/dist/services/webhooks.js +15 -12
- package/dist/utils/get-ast-from-query.js +1 -1
- package/dist/utils/get-auth-providers.d.ts +3 -1
- package/dist/utils/get-auth-providers.js +15 -4
- package/dist/utils/get-schema.d.ts +1 -1
- package/dist/utils/get-schema.js +52 -29
- package/dist/websocket/controllers/base.d.ts +1 -3
- package/dist/websocket/controllers/base.js +12 -3
- package/license +1 -1
- package/package.json +35 -33
- package/dist/webhooks.d.ts +0 -4
- 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 =
|
|
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.
|
|
73
|
+
user_agent: req.accountability?.userAgent,
|
|
74
74
|
origin: req.get('origin'),
|
|
75
75
|
});
|
|
76
76
|
try {
|
package/dist/controllers/auth.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 {
|
|
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 (
|
|
14
|
-
|
|
15
|
-
|
|
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 (
|
|
71
|
-
|
|
72
|
-
|
|
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 (
|
|
96
|
-
|
|
97
|
-
|
|
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,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
|
|
48
|
-
if (!
|
|
47
|
+
const bundleParent = idMap.get(bundleParentName);
|
|
48
|
+
if (!bundleParent)
|
|
49
49
|
continue;
|
|
50
50
|
await knex('directus_extensions')
|
|
51
|
-
.update({
|
|
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,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.
|
|
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
|
|
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
|
}
|