@directus/api 17.1.0 → 18.1.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 +8 -2
- package/dist/auth/drivers/ldap.js +14 -16
- package/dist/auth/drivers/local.js +16 -10
- package/dist/auth/drivers/oauth2.js +16 -11
- package/dist/auth/drivers/openid.js +16 -11
- package/dist/auth/drivers/saml.js +27 -12
- package/dist/cli/commands/init/index.js +3 -3
- package/dist/cli/commands/security/key.js +2 -2
- package/dist/cli/utils/create-env/env-stub.liquid +19 -4
- package/dist/cli/utils/create-env/index.js +2 -2
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +11 -4
- package/dist/controllers/auth.js +54 -19
- package/dist/controllers/extensions.js +102 -5
- package/dist/controllers/items.js +3 -2
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/shares.js +19 -4
- package/dist/database/migrations/20220429A-add-flows.js +3 -3
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +2 -2
- package/dist/database/migrations/20240204A-marketplace.d.ts +3 -0
- package/dist/database/migrations/20240204A-marketplace.js +88 -0
- package/dist/database/migrations/run.js +3 -2
- package/dist/extensions/lib/get-extensions-settings.d.ts +6 -2
- package/dist/extensions/lib/get-extensions-settings.js +70 -22
- package/dist/extensions/lib/get-extensions.d.ts +5 -1
- package/dist/extensions/lib/get-extensions.js +7 -31
- package/dist/extensions/lib/installation/index.d.ts +2 -0
- package/dist/extensions/lib/installation/index.js +9 -0
- package/dist/extensions/lib/installation/manager.d.ts +5 -0
- package/dist/extensions/lib/installation/manager.js +90 -0
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
- package/dist/extensions/lib/sync-extensions.js +11 -10
- package/dist/extensions/manager.d.ts +27 -25
- package/dist/extensions/manager.js +214 -183
- package/dist/middleware/authenticate.d.ts +1 -0
- package/dist/middleware/error-handler.js +22 -18
- package/dist/middleware/extract-token.d.ts +6 -5
- package/dist/middleware/extract-token.js +27 -11
- package/dist/middleware/merge-content-versions.d.ts +2 -0
- package/dist/middleware/merge-content-versions.js +26 -0
- package/dist/middleware/respond.js +0 -12
- package/dist/middleware/validate-batch.d.ts +1 -0
- package/dist/request/agent-with-ip-validation.d.ts +1 -1
- package/dist/request/agent-with-ip-validation.js +5 -1
- package/dist/services/activity.js +3 -3
- package/dist/services/assets.js +2 -3
- package/dist/services/authentication.d.ts +7 -2
- package/dist/services/authentication.js +21 -13
- package/dist/services/extensions.d.ts +4 -8
- package/dist/services/extensions.js +110 -93
- package/dist/services/fields.js +28 -22
- package/dist/services/graphql/index.js +98 -42
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/mail/index.d.ts +1 -1
- package/dist/services/mail/index.js +6 -5
- package/dist/services/payload.js +2 -2
- package/dist/services/{permissions.d.ts → permissions/index.d.ts} +3 -4
- package/dist/services/{permissions.js → permissions/index.js} +6 -23
- package/dist/services/permissions/lib/with-app-minimal-permissions.d.ts +2 -0
- package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
- package/dist/services/relations.d.ts +2 -3
- package/dist/services/relations.js +2 -2
- package/dist/services/roles.js +1 -1
- package/dist/services/server.js +3 -0
- package/dist/services/shares.d.ts +3 -1
- package/dist/services/shares.js +9 -5
- package/dist/storage/index.js +5 -4
- package/dist/types/auth.d.ts +6 -4
- package/dist/types/graphql.d.ts +1 -0
- package/dist/utils/apply-query.js +3 -3
- package/dist/utils/filter-items.d.ts +2 -2
- package/dist/utils/filter-items.js +1 -3
- package/dist/utils/get-cache-headers.d.ts +1 -0
- package/dist/utils/get-cache-key.d.ts +1 -0
- package/dist/utils/get-graphql-query-and-variables.d.ts +1 -0
- package/dist/utils/get-ip-from-req.d.ts +1 -0
- package/dist/utils/get-milliseconds.d.ts +1 -1
- package/dist/utils/get-milliseconds.js +4 -1
- package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
- package/dist/utils/is-login-redirect-allowed.js +34 -0
- package/dist/utils/is-url-allowed.d.ts +1 -1
- package/dist/utils/is-url-allowed.js +5 -5
- package/dist/utils/is-valid-uuid.d.ts +3 -0
- package/dist/utils/is-valid-uuid.js +21 -0
- package/dist/utils/jwt.d.ts +1 -1
- package/dist/utils/jwt.js +3 -3
- package/dist/utils/merge-version-data.d.ts +3 -0
- package/dist/utils/merge-version-data.js +134 -0
- package/dist/utils/sanitize-query.js +2 -0
- package/dist/utils/should-skip-cache.d.ts +1 -0
- package/dist/utils/validate-keys.js +2 -2
- package/dist/utils/validate-query.js +1 -0
- package/dist/websocket/controllers/base.js +2 -2
- package/package.json +45 -46
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { ErrorCode, ForbiddenError, RouteNotFoundError, isDirectusError } from '@directus/errors';
|
|
3
|
+
import { EXTENSION_TYPES } from '@directus/extensions';
|
|
4
|
+
import { account, describe, list, } from '@directus/extensions-registry';
|
|
5
|
+
import { isIn } from '@directus/utils';
|
|
3
6
|
import express from 'express';
|
|
7
|
+
import { UUID_REGEX } from '../constants.js';
|
|
4
8
|
import { getExtensionManager } from '../extensions/index.js';
|
|
5
9
|
import { respond } from '../middleware/respond.js';
|
|
6
10
|
import useCollection from '../middleware/use-collection.js';
|
|
@@ -20,18 +24,96 @@ router.get('/', asyncHandler(async (req, res, next) => {
|
|
|
20
24
|
res.locals['payload'] = { data: extensions || null };
|
|
21
25
|
return next();
|
|
22
26
|
}), respond);
|
|
23
|
-
router.
|
|
27
|
+
router.get('/registry', asyncHandler(async (req, res, next) => {
|
|
28
|
+
if (req.accountability && req.accountability.admin !== true) {
|
|
29
|
+
throw new ForbiddenError();
|
|
30
|
+
}
|
|
31
|
+
const { search, limit, offset, type, by, sort } = req.query;
|
|
32
|
+
const query = {};
|
|
33
|
+
if (typeof search === 'string') {
|
|
34
|
+
query.search = search;
|
|
35
|
+
}
|
|
36
|
+
if (typeof limit === 'string') {
|
|
37
|
+
query.limit = Number(limit);
|
|
38
|
+
}
|
|
39
|
+
if (typeof offset === 'string') {
|
|
40
|
+
query.offset = Number(offset);
|
|
41
|
+
}
|
|
42
|
+
if (typeof by === 'string') {
|
|
43
|
+
query.by = by;
|
|
44
|
+
}
|
|
45
|
+
if (typeof sort === 'string' && isIn(sort, ['popular', 'recent', 'downloads'])) {
|
|
46
|
+
query.sort = sort;
|
|
47
|
+
}
|
|
48
|
+
if (typeof type === 'string') {
|
|
49
|
+
if (isIn(type, EXTENSION_TYPES) === false) {
|
|
50
|
+
throw new ForbiddenError();
|
|
51
|
+
}
|
|
52
|
+
query.type = type;
|
|
53
|
+
}
|
|
54
|
+
if (env['MARKETPLACE_TRUST'] === 'sandbox') {
|
|
55
|
+
query.sandbox = true;
|
|
56
|
+
}
|
|
57
|
+
const options = {};
|
|
58
|
+
if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
|
|
59
|
+
options.registry = env['MARKETPLACE_REGISTRY'];
|
|
60
|
+
}
|
|
61
|
+
const payload = await list(query, options);
|
|
62
|
+
res.locals['payload'] = payload;
|
|
63
|
+
return next();
|
|
64
|
+
}), respond);
|
|
65
|
+
router.get(`/registry/account/:pk(${UUID_REGEX})`, asyncHandler(async (req, res, next) => {
|
|
66
|
+
if (typeof req.params['pk'] !== 'string') {
|
|
67
|
+
throw new ForbiddenError();
|
|
68
|
+
}
|
|
69
|
+
const options = {};
|
|
70
|
+
if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
|
|
71
|
+
options.registry = env['MARKETPLACE_REGISTRY'];
|
|
72
|
+
}
|
|
73
|
+
const payload = await account(req.params['pk'], options);
|
|
74
|
+
res.locals['payload'] = payload;
|
|
75
|
+
return next();
|
|
76
|
+
}), respond);
|
|
77
|
+
router.get(`/registry/extension/:pk(${UUID_REGEX})`, asyncHandler(async (req, res, next) => {
|
|
78
|
+
if (typeof req.params['pk'] !== 'string') {
|
|
79
|
+
throw new ForbiddenError();
|
|
80
|
+
}
|
|
81
|
+
const options = {};
|
|
82
|
+
if (env['MARKETPLACE_REGISTRY'] && typeof env['MARKETPLACE_REGISTRY'] === 'string') {
|
|
83
|
+
options.registry = env['MARKETPLACE_REGISTRY'];
|
|
84
|
+
}
|
|
85
|
+
const payload = await describe(req.params['pk'], options);
|
|
86
|
+
res.locals['payload'] = payload;
|
|
87
|
+
return next();
|
|
88
|
+
}), respond);
|
|
89
|
+
router.post('/registry/install', asyncHandler(async (req, _res, next) => {
|
|
90
|
+
if (req.accountability && req.accountability.admin !== true) {
|
|
91
|
+
throw new ForbiddenError();
|
|
92
|
+
}
|
|
93
|
+
const { version, extension } = req.body;
|
|
94
|
+
if (!version || !extension) {
|
|
95
|
+
throw new ForbiddenError();
|
|
96
|
+
}
|
|
24
97
|
const service = new ExtensionsService({
|
|
25
98
|
accountability: req.accountability,
|
|
26
99
|
schema: req.schema,
|
|
27
100
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
101
|
+
await service.install(extension, version);
|
|
102
|
+
return next();
|
|
103
|
+
}), respond);
|
|
104
|
+
router.patch(`/:pk(${UUID_REGEX})`, asyncHandler(async (req, res, next) => {
|
|
105
|
+
if (req.accountability && req.accountability.admin !== true) {
|
|
31
106
|
throw new ForbiddenError();
|
|
32
107
|
}
|
|
108
|
+
if (typeof req.params['pk'] !== 'string') {
|
|
109
|
+
throw new ForbiddenError();
|
|
110
|
+
}
|
|
111
|
+
const service = new ExtensionsService({
|
|
112
|
+
accountability: req.accountability,
|
|
113
|
+
schema: req.schema,
|
|
114
|
+
});
|
|
33
115
|
try {
|
|
34
|
-
const result = await service.updateOne(
|
|
116
|
+
const result = await service.updateOne(req.params['pk'], req.body);
|
|
35
117
|
res.locals['payload'] = { data: result || null };
|
|
36
118
|
}
|
|
37
119
|
catch (error) {
|
|
@@ -46,6 +128,21 @@ router.patch('/:bundleOrName/:name?', asyncHandler(async (req, res, next) => {
|
|
|
46
128
|
}
|
|
47
129
|
return next();
|
|
48
130
|
}), respond);
|
|
131
|
+
router.delete(`/:pk(${UUID_REGEX})`, asyncHandler(async (req, _res, next) => {
|
|
132
|
+
if (req.accountability && req.accountability.admin !== true) {
|
|
133
|
+
throw new ForbiddenError();
|
|
134
|
+
}
|
|
135
|
+
const service = new ExtensionsService({
|
|
136
|
+
accountability: req.accountability,
|
|
137
|
+
schema: req.schema,
|
|
138
|
+
});
|
|
139
|
+
const pk = req.params['pk'];
|
|
140
|
+
if (typeof pk !== 'string') {
|
|
141
|
+
throw new ForbiddenError();
|
|
142
|
+
}
|
|
143
|
+
await service.deleteOne(pk);
|
|
144
|
+
return next();
|
|
145
|
+
}), respond);
|
|
49
146
|
router.get('/sources/:chunk', asyncHandler(async (req, res) => {
|
|
50
147
|
const chunk = req.params['chunk'];
|
|
51
148
|
const extensionManager = getExtensionManager();
|
|
@@ -4,6 +4,7 @@ import { ErrorCode, ForbiddenError, RouteNotFoundError } from '@directus/errors'
|
|
|
4
4
|
import collectionExists from '../middleware/collection-exists.js';
|
|
5
5
|
import { respond } from '../middleware/respond.js';
|
|
6
6
|
import { validateBatch } from '../middleware/validate-batch.js';
|
|
7
|
+
import { mergeContentVersions } from '../middleware/merge-content-versions.js';
|
|
7
8
|
import { ItemsService } from '../services/items.js';
|
|
8
9
|
import { MetaService } from '../services/meta.js';
|
|
9
10
|
import asyncHandler from '../utils/async-handler.js';
|
|
@@ -76,7 +77,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
|
|
76
77
|
return next();
|
|
77
78
|
});
|
|
78
79
|
router.search('/:collection', collectionExists, validateBatch('read'), readHandler, respond);
|
|
79
|
-
router.get('/:collection', collectionExists, readHandler, respond);
|
|
80
|
+
router.get('/:collection', collectionExists, readHandler, mergeContentVersions, respond);
|
|
80
81
|
router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, next) => {
|
|
81
82
|
if (isSystemCollection(req.params['collection']))
|
|
82
83
|
throw new ForbiddenError();
|
|
@@ -89,7 +90,7 @@ router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, n
|
|
|
89
90
|
data: result || null,
|
|
90
91
|
};
|
|
91
92
|
return next();
|
|
92
|
-
}), respond);
|
|
93
|
+
}), mergeContentVersions, respond);
|
|
93
94
|
router.patch('/:collection', collectionExists, validateBatch('update'), asyncHandler(async (req, res, next) => {
|
|
94
95
|
if (isSystemCollection(req.params['collection']))
|
|
95
96
|
throw new ForbiddenError();
|
|
@@ -4,7 +4,7 @@ import { respond } from '../middleware/respond.js';
|
|
|
4
4
|
import useCollection from '../middleware/use-collection.js';
|
|
5
5
|
import { validateBatch } from '../middleware/validate-batch.js';
|
|
6
6
|
import { MetaService } from '../services/meta.js';
|
|
7
|
-
import { PermissionsService } from '../services/permissions.js';
|
|
7
|
+
import { PermissionsService } from '../services/permissions/index.js';
|
|
8
8
|
import asyncHandler from '../utils/async-handler.js';
|
|
9
9
|
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
10
10
|
const router = express.Router();
|
|
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import Joi from 'joi';
|
|
5
|
-
import {
|
|
5
|
+
import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, UUID_REGEX } from '../constants.js';
|
|
6
6
|
import { respond } from '../middleware/respond.js';
|
|
7
7
|
import useCollection from '../middleware/use-collection.js';
|
|
8
8
|
import { validateBatch } from '../middleware/validate-batch.js';
|
|
@@ -15,6 +15,7 @@ router.use(useCollection('directus_shares'));
|
|
|
15
15
|
const sharedLoginSchema = Joi.object({
|
|
16
16
|
share: Joi.string().required(),
|
|
17
17
|
password: Joi.string(),
|
|
18
|
+
mode: Joi.string().valid('cookie', 'json', 'session').optional(),
|
|
18
19
|
}).unknown();
|
|
19
20
|
router.post('/auth', asyncHandler(async (req, res, next) => {
|
|
20
21
|
// This doesn't use accountability, as the user isn't logged in at this point
|
|
@@ -25,9 +26,23 @@ router.post('/auth', asyncHandler(async (req, res, next) => {
|
|
|
25
26
|
if (error) {
|
|
26
27
|
throw new InvalidPayloadError({ reason: error.message });
|
|
27
28
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const mode = req.body.mode ?? 'json';
|
|
30
|
+
const { accessToken, refreshToken, expires } = await service.login(req.body, {
|
|
31
|
+
session: mode === 'session',
|
|
32
|
+
});
|
|
33
|
+
const payload = { expires };
|
|
34
|
+
if (mode === 'json') {
|
|
35
|
+
payload.refresh_token = refreshToken;
|
|
36
|
+
payload.access_token = accessToken;
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'cookie') {
|
|
39
|
+
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
40
|
+
payload.access_token = accessToken;
|
|
41
|
+
}
|
|
42
|
+
if (mode === 'session') {
|
|
43
|
+
res.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
|
|
44
|
+
}
|
|
45
|
+
res.locals['payload'] = { data: payload };
|
|
31
46
|
return next();
|
|
32
47
|
}), respond);
|
|
33
48
|
const sharedInviteSchema = Joi.object({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseJSON, toArray } from '@directus/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
3
|
export async function up(knex) {
|
|
4
4
|
await knex.schema.createTable('directus_flows', (table) => {
|
|
5
5
|
table.uuid('id').primary().notNullable();
|
|
@@ -33,7 +33,7 @@ export async function up(knex) {
|
|
|
33
33
|
const flows = [];
|
|
34
34
|
const operations = [];
|
|
35
35
|
for (const webhook of webhooks) {
|
|
36
|
-
const flowID =
|
|
36
|
+
const flowID = randomUUID();
|
|
37
37
|
flows.push({
|
|
38
38
|
id: flowID,
|
|
39
39
|
name: webhook.name,
|
|
@@ -47,7 +47,7 @@ export async function up(knex) {
|
|
|
47
47
|
}),
|
|
48
48
|
});
|
|
49
49
|
operations.push({
|
|
50
|
-
id:
|
|
50
|
+
id: randomUUID(),
|
|
51
51
|
name: 'Request',
|
|
52
52
|
key: 'request',
|
|
53
53
|
type: 'request',
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { set } from 'lodash-es';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
3
|
function transformStringsNewFormat(oldStrings) {
|
|
4
4
|
return oldStrings.reduce((result, item) => {
|
|
5
5
|
if (!item.key || !item.translations)
|
|
6
6
|
return result;
|
|
7
7
|
for (const [language, value] of Object.entries(item.translations)) {
|
|
8
|
-
result.push({ id:
|
|
8
|
+
result.push({ id: randomUUID(), key: item.key, language, value });
|
|
9
9
|
}
|
|
10
10
|
return result;
|
|
11
11
|
}, []);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { resolvePackage } from '@directus/utils/node';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export async function up(knex) {
|
|
7
|
+
await knex.schema.alterTable('directus_extensions', (table) => {
|
|
8
|
+
table.uuid('id').nullable();
|
|
9
|
+
table.string('folder');
|
|
10
|
+
table.string('source');
|
|
11
|
+
table.uuid('bundle');
|
|
12
|
+
});
|
|
13
|
+
const installedExtensions = await knex.select('name').from('directus_extensions');
|
|
14
|
+
// name: id
|
|
15
|
+
const idMap = new Map();
|
|
16
|
+
for (const { name } of installedExtensions) {
|
|
17
|
+
// Delete extension meta status that used the legacy `${name}:${type}` name syntax for
|
|
18
|
+
// extension-folder scoped extensions
|
|
19
|
+
if (name.includes(':')) {
|
|
20
|
+
await knex('directus_extensions').delete().where({ name });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const id = randomUUID();
|
|
24
|
+
let source;
|
|
25
|
+
try {
|
|
26
|
+
// The NPM package name is the name used in the database. If we can resolve the
|
|
27
|
+
// extension as a node module it's safe to assume it's a npm-module source
|
|
28
|
+
resolvePackage(name, __dirname);
|
|
29
|
+
source = 'module';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
source = 'local';
|
|
33
|
+
}
|
|
34
|
+
await knex('directus_extensions').update({ id, source, folder: name }).where({ name });
|
|
35
|
+
idMap.set(name, id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const { name } of installedExtensions) {
|
|
39
|
+
if (!name.includes('/'))
|
|
40
|
+
continue;
|
|
41
|
+
const splittedName = name.split('/');
|
|
42
|
+
const isScopedModuleBundleParent = name.startsWith('@') && splittedName.length == 2;
|
|
43
|
+
if (isScopedModuleBundleParent)
|
|
44
|
+
continue;
|
|
45
|
+
const isScopedModuleBundleChild = name.startsWith('@') && splittedName.length > 2;
|
|
46
|
+
const bundleParentName = isScopedModuleBundleParent || isScopedModuleBundleChild ? splittedName.slice(0, 2).join('/') : splittedName[0];
|
|
47
|
+
const bundleParentId = idMap.get(bundleParentName);
|
|
48
|
+
if (!bundleParentId)
|
|
49
|
+
continue;
|
|
50
|
+
await knex('directus_extensions')
|
|
51
|
+
.update({ bundle: bundleParentId, folder: name.substring(bundleParentName.length + 1) })
|
|
52
|
+
.where({ folder: name });
|
|
53
|
+
}
|
|
54
|
+
await knex.schema.alterTable('directus_extensions', (table) => {
|
|
55
|
+
table.dropColumn('name');
|
|
56
|
+
table.uuid('id').alter().primary().notNullable();
|
|
57
|
+
table.string('source').alter().notNullable();
|
|
58
|
+
table.string('folder').alter().notNullable();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/*
|
|
62
|
+
* Note: For local extensions having a different package & folder name,
|
|
63
|
+
* we aren't able to revert to the exact same state as before.
|
|
64
|
+
* But we still need to do the name convertion, in order for the migration to succeed.
|
|
65
|
+
*/
|
|
66
|
+
export async function down(knex) {
|
|
67
|
+
await knex.schema.alterTable('directus_extensions', (table) => {
|
|
68
|
+
table.string('name');
|
|
69
|
+
});
|
|
70
|
+
const installedExtensions = await knex.select(['id', 'folder', 'bundle']).from('directus_extensions');
|
|
71
|
+
const idMap = new Map(installedExtensions.map((extension) => [extension.id, extension.folder]));
|
|
72
|
+
for (const { id, folder, bundle, source } of installedExtensions) {
|
|
73
|
+
if (source === 'registry') {
|
|
74
|
+
await knex('directus_extensions').delete().where({ id });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let name = folder;
|
|
78
|
+
if (bundle) {
|
|
79
|
+
const bundleParentName = idMap.get(bundle);
|
|
80
|
+
name = `${bundleParentName}/${name}`;
|
|
81
|
+
}
|
|
82
|
+
await knex('directus_extensions').update({ name }).where({ id });
|
|
83
|
+
}
|
|
84
|
+
await knex.schema.alterTable('directus_extensions', (table) => {
|
|
85
|
+
table.dropColumns('id', 'folder', 'source', 'bundle');
|
|
86
|
+
table.string('name').alter().primary().notNullable();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
1
2
|
import formatTitle from '@directus/format-title';
|
|
2
3
|
import fse from 'fs-extra';
|
|
3
4
|
import { orderBy } from 'lodash-es';
|
|
@@ -5,14 +6,14 @@ import { dirname } from 'node:path';
|
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
import { flushCaches } from '../../cache.js';
|
|
8
|
-
import { getExtensionsPath } from '../../extensions/lib/get-extensions-path.js';
|
|
9
9
|
import { useLogger } from '../../logger.js';
|
|
10
10
|
import getModuleDefault from '../../utils/get-module-default.js';
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
export default async function run(database, direction, log = true) {
|
|
13
|
+
const env = useEnv();
|
|
13
14
|
const logger = useLogger();
|
|
14
15
|
let migrationFiles = await fse.readdir(__dirname);
|
|
15
|
-
const customMigrationsPath = path.resolve(
|
|
16
|
+
const customMigrationsPath = path.resolve(env['MIGRATIONS_PATH']);
|
|
16
17
|
let customMigrationFiles = ((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];
|
|
17
18
|
migrationFiles = migrationFiles.filter((file) => /^[0-9]+[A-Z]-[^.]+\.(?:js|ts)$/.test(file));
|
|
18
19
|
customMigrationFiles = customMigrationFiles.filter((file) => file.includes('-') && /\.(c|m)?js$/.test(file));
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type { Extension } from '@directus/extensions';
|
|
1
|
+
import type { Extension, ExtensionSettings } from '@directus/extensions';
|
|
2
2
|
/**
|
|
3
3
|
* Loads stored settings for all extensions. Creates empty new rows in extensions tables for
|
|
4
4
|
* extensions that don't have settings yet, and remove any settings for extensions that are no
|
|
5
5
|
* longer installed.
|
|
6
6
|
*/
|
|
7
|
-
export declare const getExtensionsSettings: (
|
|
7
|
+
export declare const getExtensionsSettings: ({ local, module, registry, }: {
|
|
8
|
+
local: Map<string, Extension>;
|
|
9
|
+
module: Map<string, Extension>;
|
|
10
|
+
registry: Map<string, Extension>;
|
|
11
|
+
}) => Promise<ExtensionSettings[]>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import getDatabase from '../../database/index.js';
|
|
3
3
|
import { ExtensionsService } from '../../services/extensions.js';
|
|
4
4
|
import { getSchema } from '../../utils/get-schema.js';
|
|
@@ -7,33 +7,81 @@ import { getSchema } from '../../utils/get-schema.js';
|
|
|
7
7
|
* extensions that don't have settings yet, and remove any settings for extensions that are no
|
|
8
8
|
* longer installed.
|
|
9
9
|
*/
|
|
10
|
-
export const getExtensionsSettings = async (
|
|
10
|
+
export const getExtensionsSettings = async ({ local, module, registry, }) => {
|
|
11
11
|
const database = getDatabase();
|
|
12
12
|
const service = new ExtensionsService({
|
|
13
13
|
knex: database,
|
|
14
14
|
schema: await getSchema(),
|
|
15
15
|
});
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
const existingSettings = await service.extensionsItemService.readByQuery({ limit: -1 });
|
|
17
|
+
const newSettings = [];
|
|
18
|
+
const localSettings = existingSettings.filter(({ source }) => source === 'local');
|
|
19
|
+
const registrySettings = existingSettings.filter(({ source }) => source === 'registry');
|
|
20
|
+
const moduleSettings = existingSettings.filter(({ source }) => source === 'module');
|
|
21
|
+
const generateSettingsEntry = (folder, extension, source) => {
|
|
19
22
|
if (extension.type === 'bundle') {
|
|
20
|
-
|
|
23
|
+
const bundleId = randomUUID();
|
|
24
|
+
newSettings.push({
|
|
25
|
+
id: bundleId,
|
|
26
|
+
enabled: true,
|
|
27
|
+
source: source,
|
|
28
|
+
bundle: null,
|
|
29
|
+
folder: folder,
|
|
30
|
+
});
|
|
31
|
+
for (const entry of extension.entries) {
|
|
32
|
+
newSettings.push({
|
|
33
|
+
id: randomUUID(),
|
|
34
|
+
enabled: true,
|
|
35
|
+
source: source,
|
|
36
|
+
bundle: bundleId,
|
|
37
|
+
folder: entry.name,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
21
40
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
else {
|
|
42
|
+
newSettings.push({
|
|
43
|
+
id: randomUUID(),
|
|
44
|
+
enabled: true,
|
|
45
|
+
source: source,
|
|
46
|
+
bundle: null,
|
|
47
|
+
folder: folder,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
for (const [folder, extension] of local.entries()) {
|
|
52
|
+
const settingsExist = localSettings.some((settings) => settings.folder === folder);
|
|
53
|
+
if (settingsExist)
|
|
54
|
+
continue;
|
|
55
|
+
const settingsForName = localSettings.find((settings) => settings.folder === extension.name);
|
|
56
|
+
/*
|
|
57
|
+
* TODO: Consider removing this in follow-up versions after v10.10.0
|
|
58
|
+
*
|
|
59
|
+
* Previously, the package name (from package.json) was used to identify
|
|
60
|
+
* local extensions - now it's the folder name.
|
|
61
|
+
* If those two are different, we need to check for existing settings
|
|
62
|
+
* with the package name, too. On a match and if there's no other local extension
|
|
63
|
+
* with such a folder name, these settings can be taken over with the folder updated.
|
|
64
|
+
*/
|
|
65
|
+
if (settingsForName && !local.has(extension.name)) {
|
|
66
|
+
await service.extensionsItemService.updateOne(settingsForName.id, { folder });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!settingsExist)
|
|
70
|
+
generateSettingsEntry(folder, extension, 'local');
|
|
71
|
+
}
|
|
72
|
+
for (const [folder, extension] of module.entries()) {
|
|
73
|
+
const settingsExist = moduleSettings.some((settings) => settings.folder === folder);
|
|
74
|
+
if (!settingsExist)
|
|
75
|
+
generateSettingsEntry(folder, extension, 'module');
|
|
76
|
+
}
|
|
77
|
+
for (const [folder, extension] of registry.entries()) {
|
|
78
|
+
const settingsExist = registrySettings.some((settings) => settings.folder === folder);
|
|
79
|
+
if (!settingsExist)
|
|
80
|
+
generateSettingsEntry(folder, extension, 'registry');
|
|
81
|
+
}
|
|
82
|
+
if (newSettings.length > 0) {
|
|
83
|
+
await database.insert(newSettings).into('directus_extensions');
|
|
31
84
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* folder. Having them automatically synced feels dangerous, as it's a destructive action. In the
|
|
35
|
-
* edge case you'd deploy / start with the extensions folder misconfigured, it would remove all
|
|
36
|
-
* previous options on startup, without an option to undo.
|
|
37
|
-
*/
|
|
38
|
-
return settings.filter(({ name }) => extensionNames.includes(name));
|
|
85
|
+
const settings = [...existingSettings, ...newSettings];
|
|
86
|
+
return settings;
|
|
39
87
|
};
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
export declare const getExtensions: () => Promise<
|
|
1
|
+
export declare const getExtensions: () => Promise<{
|
|
2
|
+
local: Map<string, import("@directus/extensions/node").Extension>;
|
|
3
|
+
registry: Map<string, import("@directus/extensions/node").Extension>;
|
|
4
|
+
module: Map<string, import("@directus/extensions/node").Extension>;
|
|
5
|
+
}>;
|
|
@@ -1,36 +1,12 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { resolveFsExtensions, resolveModuleExtensions } from '@directus/extensions/node';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
4
|
import { getExtensionsPath } from './get-extensions-path.js';
|
|
5
5
|
export const getExtensions = async () => {
|
|
6
6
|
const env = useEnv();
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if (isExistingExtension) {
|
|
13
|
-
duplicateExtensions.push(extension.name);
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
if (extension.type === 'bundle') {
|
|
17
|
-
const bundleEntryNames = new Set();
|
|
18
|
-
for (const entry of extension.entries) {
|
|
19
|
-
if (bundleEntryNames.has(entry.name)) {
|
|
20
|
-
// Do not load entire bundle if it has duplicated entries
|
|
21
|
-
duplicateExtensions.push(extension.name);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
bundleEntryNames.add(entry.name);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
loadedExtensions.set(extension.name, extension);
|
|
28
|
-
};
|
|
29
|
-
(await getLocalExtensions(getExtensionsPath())).forEach(filterDuplicates);
|
|
30
|
-
(await resolvePackageExtensions(getExtensionsPath())).forEach(filterDuplicates);
|
|
31
|
-
(await getPackageExtensions(env['PACKAGE_FILE_LOCATION'])).forEach(filterDuplicates);
|
|
32
|
-
if (duplicateExtensions.length > 0) {
|
|
33
|
-
logger.warn(`Failed to load the following extensions because they have/contain duplicate names: ${duplicateExtensions.join(', ')}`);
|
|
34
|
-
}
|
|
35
|
-
return Array.from(loadedExtensions.values());
|
|
7
|
+
const localExtensions = await resolveFsExtensions(getExtensionsPath());
|
|
8
|
+
const registryExtensions = await resolveFsExtensions(join(getExtensionsPath(), '.registry'));
|
|
9
|
+
/** Extensions that are listed as dependencies in the root package.json */
|
|
10
|
+
const moduleExtensions = await resolveModuleExtensions(env['PACKAGE_FILE_LOCATION']);
|
|
11
|
+
return { local: localExtensions, registry: registryExtensions, module: moduleExtensions };
|
|
36
12
|
};
|