@strapi/strapi 4.5.0-alpha.0 → 4.5.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/lib/Strapi.js +19 -3
- package/lib/commands/builders/admin.js +1 -0
- package/lib/core/app-configuration/load-config-file.js +1 -3
- package/lib/core/loaders/apis.js +1 -2
- package/lib/core/loaders/middlewares.js +1 -2
- package/lib/core/loaders/policies.js +1 -2
- package/lib/core/loaders/src-index.js +1 -3
- package/lib/core/registries/custom-fields.js +72 -0
- package/lib/load/load-files.js +1 -1
- package/lib/middlewares/favicon.js +16 -3
- package/lib/services/auth/index.js +4 -1
- package/lib/services/content-api/index.js +74 -0
- package/lib/services/content-api/permissions/engine.js +5 -0
- package/lib/services/content-api/permissions/index.js +148 -0
- package/lib/services/content-api/permissions/providers/action.js +19 -0
- package/lib/services/content-api/permissions/providers/condition.js +19 -0
- package/lib/services/content-api/permissions/providers/index.js +9 -0
- package/lib/services/custom-fields.js +11 -0
- package/lib/services/entity-service/components.js +16 -27
- package/lib/services/entity-service/index.js +29 -26
- package/lib/services/metrics/sender.js +2 -0
- package/lib/services/server/middleware.js +2 -1
- package/lib/types/core/attributes/common.d.ts +3 -0
- package/lib/types/core/strapi/index.d.ts +41 -7
- package/lib/utils/convert-custom-field-type.js +22 -0
- package/lib/utils/index.js +0 -2
- package/package.json +15 -14
- package/lib/utils/import-default.js +0 -10
package/lib/Strapi.js
CHANGED
|
@@ -23,6 +23,8 @@ const entityValidator = require('./services/entity-validator');
|
|
|
23
23
|
const createTelemetry = require('./services/metrics');
|
|
24
24
|
const requestContext = require('./services/request-context');
|
|
25
25
|
const createAuth = require('./services/auth');
|
|
26
|
+
const createCustomFields = require('./services/custom-fields');
|
|
27
|
+
const createContentAPI = require('./services/content-api');
|
|
26
28
|
const createUpdateNotifier = require('./utils/update-notifier');
|
|
27
29
|
const createStartupLogger = require('./utils/startup-logger');
|
|
28
30
|
const { LIFECYCLES } = require('./utils/lifecycles');
|
|
@@ -35,12 +37,14 @@ const hooksRegistry = require('./core/registries/hooks');
|
|
|
35
37
|
const controllersRegistry = require('./core/registries/controllers');
|
|
36
38
|
const modulesRegistry = require('./core/registries/modules');
|
|
37
39
|
const pluginsRegistry = require('./core/registries/plugins');
|
|
40
|
+
const customFieldsRegistry = require('./core/registries/custom-fields');
|
|
38
41
|
const createConfigProvider = require('./core/registries/config');
|
|
39
42
|
const apisRegistry = require('./core/registries/apis');
|
|
40
43
|
const bootstrap = require('./core/bootstrap');
|
|
41
44
|
const loaders = require('./core/loaders');
|
|
42
45
|
const { destroyOnSignal } = require('./utils/signals');
|
|
43
46
|
const sanitizersRegistry = require('./core/registries/sanitizers');
|
|
47
|
+
const convertCustomFieldType = require('./utils/convert-custom-field-type');
|
|
44
48
|
|
|
45
49
|
// TODO: move somewhere else
|
|
46
50
|
const draftAndPublishSync = require('./migrations/draft-publish');
|
|
@@ -75,7 +79,7 @@ class Strapi {
|
|
|
75
79
|
// Load the app configuration from the dist directory
|
|
76
80
|
const appConfig = loadConfiguration({ appDir: rootDirs.app, distDir: rootDirs.dist }, opts);
|
|
77
81
|
|
|
78
|
-
//
|
|
82
|
+
// Instantiate the Strapi container
|
|
79
83
|
this.container = createContainer(this);
|
|
80
84
|
|
|
81
85
|
// Register every Strapi registry in the container
|
|
@@ -88,8 +92,10 @@ class Strapi {
|
|
|
88
92
|
this.container.register('controllers', controllersRegistry(this));
|
|
89
93
|
this.container.register('modules', modulesRegistry(this));
|
|
90
94
|
this.container.register('plugins', pluginsRegistry(this));
|
|
95
|
+
this.container.register('custom-fields', customFieldsRegistry(this));
|
|
91
96
|
this.container.register('apis', apisRegistry(this));
|
|
92
97
|
this.container.register('auth', createAuth(this));
|
|
98
|
+
this.container.register('content-api', createContentAPI(this));
|
|
93
99
|
this.container.register('sanitizers', sanitizersRegistry(this));
|
|
94
100
|
|
|
95
101
|
// Create a mapping of every useful directory (for the app, dist and static directories)
|
|
@@ -99,7 +105,7 @@ class Strapi {
|
|
|
99
105
|
this.isLoaded = false;
|
|
100
106
|
this.reload = this.reload();
|
|
101
107
|
|
|
102
|
-
//
|
|
108
|
+
// Instantiate the Koa app & the HTTP server
|
|
103
109
|
this.server = createServer(this);
|
|
104
110
|
|
|
105
111
|
// Strapi utils instanciation
|
|
@@ -111,6 +117,8 @@ class Strapi {
|
|
|
111
117
|
this.telemetry = createTelemetry(this);
|
|
112
118
|
this.requestContext = requestContext;
|
|
113
119
|
|
|
120
|
+
this.customFields = createCustomFields(this);
|
|
121
|
+
|
|
114
122
|
createUpdateNotifier(this).notify();
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -119,7 +127,7 @@ class Strapi {
|
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
get EE() {
|
|
122
|
-
return ee({ dir: this.dirs.
|
|
130
|
+
return ee({ dir: this.dirs.app.root, logger: this.log });
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
get services() {
|
|
@@ -190,6 +198,10 @@ class Strapi {
|
|
|
190
198
|
return this.container.get('auth');
|
|
191
199
|
}
|
|
192
200
|
|
|
201
|
+
get contentAPI() {
|
|
202
|
+
return this.container.get('content-api');
|
|
203
|
+
}
|
|
204
|
+
|
|
193
205
|
get sanitizers() {
|
|
194
206
|
return this.container.get('sanitizers');
|
|
195
207
|
}
|
|
@@ -379,6 +391,8 @@ class Strapi {
|
|
|
379
391
|
this.telemetry.register();
|
|
380
392
|
|
|
381
393
|
await this.runLifecyclesFunctions(LIFECYCLES.REGISTER);
|
|
394
|
+
// NOTE: Swap type customField for underlying data type
|
|
395
|
+
convertCustomFieldType(this);
|
|
382
396
|
|
|
383
397
|
return this;
|
|
384
398
|
}
|
|
@@ -447,6 +461,8 @@ class Strapi {
|
|
|
447
461
|
await this.server.initMiddlewares();
|
|
448
462
|
await this.server.initRouting();
|
|
449
463
|
|
|
464
|
+
await this.contentAPI.permissions.registerActions();
|
|
465
|
+
|
|
450
466
|
await this.runLifecyclesFunctions(LIFECYCLES.BOOTSTRAP);
|
|
451
467
|
|
|
452
468
|
this.cron.start();
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
-
const { templateConfiguration, env } = require('@strapi/utils');
|
|
6
|
-
|
|
7
|
-
const importDefault = require('../../utils/import-default');
|
|
5
|
+
const { templateConfiguration, env, importDefault } = require('@strapi/utils');
|
|
8
6
|
|
|
9
7
|
const loadJsFile = (file) => {
|
|
10
8
|
try {
|
package/lib/core/loaders/apis.js
CHANGED
|
@@ -4,8 +4,7 @@ const { join, extname, basename } = require('path');
|
|
|
4
4
|
const { existsSync } = require('fs-extra');
|
|
5
5
|
const _ = require('lodash');
|
|
6
6
|
const fse = require('fs-extra');
|
|
7
|
-
const { isKebabCase } = require('@strapi/utils');
|
|
8
|
-
const { importDefault } = require('../../utils');
|
|
7
|
+
const { isKebabCase, importDefault } = require('@strapi/utils');
|
|
9
8
|
|
|
10
9
|
const DEFAULT_CONTENT_TYPE = {
|
|
11
10
|
schema: {},
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { join, extname, basename } = require('path');
|
|
4
4
|
const fse = require('fs-extra');
|
|
5
|
-
|
|
6
|
-
const { importDefault } = require('../../utils');
|
|
5
|
+
const { importDefault } = require('@strapi/utils');
|
|
7
6
|
|
|
8
7
|
// TODO:: allow folders with index.js inside for bigger policies
|
|
9
8
|
module.exports = async function loadMiddlewares(strapi) {
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { join, extname, basename } = require('path');
|
|
4
4
|
const fse = require('fs-extra');
|
|
5
|
-
|
|
6
|
-
const { importDefault } = require('../../utils');
|
|
5
|
+
const { importDefault } = require('@strapi/utils');
|
|
7
6
|
|
|
8
7
|
// TODO:: allow folders with index.js inside for bigger policies
|
|
9
8
|
module.exports = async function loadPolicies(strapi) {
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { resolve } = require('path');
|
|
4
4
|
const { statSync, existsSync } = require('fs');
|
|
5
|
-
const { yup } = require('@strapi/utils');
|
|
6
|
-
|
|
7
|
-
const { importDefault } = require('../../utils');
|
|
5
|
+
const { yup, importDefault } = require('@strapi/utils');
|
|
8
6
|
|
|
9
7
|
const srcSchema = yup
|
|
10
8
|
.object()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { has } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const ALLOWED_TYPES = [
|
|
6
|
+
'biginteger',
|
|
7
|
+
'boolean',
|
|
8
|
+
'date',
|
|
9
|
+
'datetime',
|
|
10
|
+
'decimal',
|
|
11
|
+
'email',
|
|
12
|
+
'enumeration',
|
|
13
|
+
'float',
|
|
14
|
+
'integer',
|
|
15
|
+
'json',
|
|
16
|
+
'password',
|
|
17
|
+
'richtext',
|
|
18
|
+
'string',
|
|
19
|
+
'text',
|
|
20
|
+
'time',
|
|
21
|
+
'uid',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const customFieldsRegistry = (strapi) => {
|
|
25
|
+
const customFields = {};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
getAll() {
|
|
29
|
+
return customFields;
|
|
30
|
+
},
|
|
31
|
+
get(customField) {
|
|
32
|
+
const registeredCustomField = customFields[customField];
|
|
33
|
+
if (!registeredCustomField) {
|
|
34
|
+
throw new Error(`Could not find Custom Field: ${customField}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return registeredCustomField;
|
|
38
|
+
},
|
|
39
|
+
add(customField) {
|
|
40
|
+
const customFieldList = Array.isArray(customField) ? customField : [customField];
|
|
41
|
+
|
|
42
|
+
for (const cf of customFieldList) {
|
|
43
|
+
if (!has('name', cf) || !has('type', cf)) {
|
|
44
|
+
throw new Error(`Custom fields require a 'name' and 'type' key`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { name, plugin, type } = cf;
|
|
48
|
+
if (!ALLOWED_TYPES.includes(type)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Custom field type: '${type}' is not a valid Strapi type or it can't be used with a Custom Field`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isValidObjectKey = /^(?![0-9])[a-zA-Z0-9$_-]+$/g;
|
|
55
|
+
if (!isValidObjectKey.test(name)) {
|
|
56
|
+
throw new Error(`Custom field name: '${name}' is not a valid object key`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// When no plugin is specified, or it isn't found in Strapi, default to global
|
|
60
|
+
const uid = strapi.plugin(plugin) ? `plugin::${plugin}.${name}` : `global::${name}`;
|
|
61
|
+
|
|
62
|
+
if (has(uid, customFields)) {
|
|
63
|
+
throw new Error(`Custom field: '${uid}' has already been registered`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
customFields[uid] = cf;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
module.exports = customFieldsRegistry;
|
package/lib/load/load-files.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const _ = require('lodash');
|
|
5
5
|
const fse = require('fs-extra');
|
|
6
6
|
|
|
7
|
-
const { importDefault } = require('
|
|
7
|
+
const { importDefault } = require('@strapi/utils');
|
|
8
8
|
const glob = require('./glob');
|
|
9
9
|
const filePathToPath = require('./filepath-to-prop-path');
|
|
10
10
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { existsSync } = require('fs');
|
|
3
4
|
const { resolve } = require('path');
|
|
4
5
|
const { defaultsDeep } = require('lodash/fp');
|
|
5
6
|
const favicon = require('koa-favicon');
|
|
6
7
|
|
|
7
8
|
const defaults = {
|
|
8
|
-
path: 'favicon.
|
|
9
|
+
path: 'favicon.png',
|
|
9
10
|
maxAge: 86400000,
|
|
10
11
|
};
|
|
11
12
|
|
|
@@ -13,7 +14,19 @@ const defaults = {
|
|
|
13
14
|
* @type {import('./').MiddlewareFactory}
|
|
14
15
|
*/
|
|
15
16
|
module.exports = (config, { strapi }) => {
|
|
16
|
-
const { maxAge, path:
|
|
17
|
+
const { maxAge, path: faviconDefaultPath } = defaultsDeep(defaults, config);
|
|
18
|
+
const { root: appRoot } = strapi.dirs.app;
|
|
19
|
+
let faviconPath = faviconDefaultPath;
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
/** TODO (v5): Updating the favicon to use a png caused
|
|
22
|
+
* https://github.com/strapi/strapi/issues/14693
|
|
23
|
+
*
|
|
24
|
+
* This check ensures backwards compatibility until
|
|
25
|
+
* the next major version
|
|
26
|
+
*/
|
|
27
|
+
if (!existsSync(resolve(appRoot, faviconPath))) {
|
|
28
|
+
faviconPath = 'favicon.ico';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return favicon(resolve(appRoot, faviconPath), { maxAge });
|
|
19
32
|
};
|
|
@@ -32,6 +32,7 @@ const createAuthentication = () => {
|
|
|
32
32
|
|
|
33
33
|
return this;
|
|
34
34
|
},
|
|
35
|
+
|
|
35
36
|
async authenticate(ctx, next) {
|
|
36
37
|
const { route } = ctx.state;
|
|
37
38
|
|
|
@@ -47,7 +48,7 @@ const createAuthentication = () => {
|
|
|
47
48
|
for (const strategy of strategiesToUse) {
|
|
48
49
|
const result = await strategy.authenticate(ctx);
|
|
49
50
|
|
|
50
|
-
const { authenticated = false,
|
|
51
|
+
const { authenticated = false, credentials, ability = null, error = null } = result || {};
|
|
51
52
|
|
|
52
53
|
if (error !== null) {
|
|
53
54
|
return ctx.unauthorized(error);
|
|
@@ -58,6 +59,7 @@ const createAuthentication = () => {
|
|
|
58
59
|
ctx.state.auth = {
|
|
59
60
|
strategy,
|
|
60
61
|
credentials,
|
|
62
|
+
ability,
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
return next();
|
|
@@ -66,6 +68,7 @@ const createAuthentication = () => {
|
|
|
66
68
|
|
|
67
69
|
return ctx.unauthorized('Missing or invalid credentials');
|
|
68
70
|
},
|
|
71
|
+
|
|
69
72
|
async verify(auth, config = {}) {
|
|
70
73
|
if (config === false) {
|
|
71
74
|
return;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const instantiatePermissionsUtilities = require('./permissions');
|
|
5
|
+
|
|
6
|
+
const transformRoutePrefixFor = (pluginName) => (route) => {
|
|
7
|
+
const prefix = route.config && route.config.prefix;
|
|
8
|
+
const path = prefix !== undefined ? `${prefix}${route.path}` : `/${pluginName}${route.path}`;
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...route,
|
|
12
|
+
path,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a content API container that holds logic, tools and utils. (eg: permissions, ...)
|
|
18
|
+
*/
|
|
19
|
+
const createContentAPI = (strapi) => {
|
|
20
|
+
const getRoutesMap = async () => {
|
|
21
|
+
const routesMap = {};
|
|
22
|
+
|
|
23
|
+
_.forEach(strapi.api, (api, apiName) => {
|
|
24
|
+
const routes = _.flatMap(api.routes, (route) => {
|
|
25
|
+
if (_.has(route, 'routes')) {
|
|
26
|
+
return route.routes;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return route;
|
|
30
|
+
}).filter((route) => route.info.type === 'content-api');
|
|
31
|
+
|
|
32
|
+
if (routes.length === 0) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const apiPrefix = strapi.config.get('api.rest.prefix');
|
|
37
|
+
routesMap[`api::${apiName}`] = routes.map((route) => ({
|
|
38
|
+
...route,
|
|
39
|
+
path: `${apiPrefix}${route.path}`,
|
|
40
|
+
}));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
_.forEach(strapi.plugins, (plugin, pluginName) => {
|
|
44
|
+
const transformPrefix = transformRoutePrefixFor(pluginName);
|
|
45
|
+
|
|
46
|
+
const routes = _.flatMap(plugin.routes, (route) => {
|
|
47
|
+
if (_.has(route, 'routes')) {
|
|
48
|
+
return route.routes.map(transformPrefix);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return transformPrefix(route);
|
|
52
|
+
}).filter((route) => route.info.type === 'content-api');
|
|
53
|
+
|
|
54
|
+
if (routes.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const apiPrefix = strapi.config.get('api.rest.prefix');
|
|
59
|
+
routesMap[`plugin::${pluginName}`] = routes.map((route) => ({
|
|
60
|
+
...route,
|
|
61
|
+
path: `${apiPrefix}${route.path}`,
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return routesMap;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
permissions: instantiatePermissionsUtilities(strapi),
|
|
70
|
+
getRoutesMap,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
module.exports = createContentAPI;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const { createActionProvider, createConditionProvider } = require('./providers');
|
|
5
|
+
const createPermissionEngine = require('./engine');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates an handler that checks if the permission's action exists in the action registry
|
|
9
|
+
*/
|
|
10
|
+
const createValidatePermissionHandler =
|
|
11
|
+
(actionProvider) =>
|
|
12
|
+
({ permission }) => {
|
|
13
|
+
const action = actionProvider.get(permission.action);
|
|
14
|
+
|
|
15
|
+
// If the action isn't registered into the action provider, then ignore the permission and warn the user
|
|
16
|
+
if (!action) {
|
|
17
|
+
strapi.log.debug(
|
|
18
|
+
`Unknown action "${permission.action}" supplied when registering a new permission`
|
|
19
|
+
);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create instances of providers and permission engine for the core content-API service.
|
|
26
|
+
* Also, expose utilities to get informations about available actions and such.
|
|
27
|
+
*
|
|
28
|
+
* @param {Strapi.Strapi} strapi
|
|
29
|
+
*/
|
|
30
|
+
module.exports = (strapi) => {
|
|
31
|
+
// NOTE: Here we define both an action and condition provider,
|
|
32
|
+
// but at the moment, we're only using the action one.
|
|
33
|
+
const providers = {
|
|
34
|
+
action: createActionProvider(),
|
|
35
|
+
condition: createConditionProvider(),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get a tree representation of the available Content API actions
|
|
40
|
+
* based on the methods of the Content API controllers.
|
|
41
|
+
*
|
|
42
|
+
* @note Only actions bound to a content-API route are returned.
|
|
43
|
+
*
|
|
44
|
+
* @return {{ [api: string]: { [controller: string]: string[] }}}
|
|
45
|
+
*/
|
|
46
|
+
const getActionsMap = () => {
|
|
47
|
+
const actionMap = {};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a controller's action is bound to the
|
|
51
|
+
* content-api by looking at a potential __type__ symbol
|
|
52
|
+
*
|
|
53
|
+
* @param {object} action
|
|
54
|
+
*
|
|
55
|
+
* @return {boolean}
|
|
56
|
+
*/
|
|
57
|
+
const isContentApi = (action) => {
|
|
58
|
+
if (!_.has(action, Symbol.for('__type__'))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return action[Symbol.for('__type__')].includes('content-api');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register actions from a specific API source into the result tree
|
|
67
|
+
*
|
|
68
|
+
* @param {{ [apiName]: { controllers: { [controller]: object } }}} apis The API container
|
|
69
|
+
* @param {string} source The prefix to use in front the API name
|
|
70
|
+
*
|
|
71
|
+
* @return {void}
|
|
72
|
+
*/
|
|
73
|
+
const registerAPIsActions = (apis, source) => {
|
|
74
|
+
_.forEach(apis, (api, apiName) => {
|
|
75
|
+
const controllers = _.reduce(
|
|
76
|
+
api.controllers,
|
|
77
|
+
(acc, controller, controllerName) => {
|
|
78
|
+
const contentApiActions = _.pickBy(controller, isContentApi);
|
|
79
|
+
|
|
80
|
+
if (_.isEmpty(contentApiActions)) {
|
|
81
|
+
return acc;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
acc[controllerName] = Object.keys(contentApiActions);
|
|
85
|
+
|
|
86
|
+
return acc;
|
|
87
|
+
},
|
|
88
|
+
{}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!_.isEmpty(controllers)) {
|
|
92
|
+
actionMap[`${source}::${apiName}`] = { controllers };
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
registerAPIsActions(strapi.api, 'api');
|
|
98
|
+
registerAPIsActions(strapi.plugins, 'plugin');
|
|
99
|
+
|
|
100
|
+
return actionMap;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Register all the content-API's controllers actions into the action provider.
|
|
105
|
+
* This method make use of the {@link getActionsMap} to generate the list of actions to register.
|
|
106
|
+
*
|
|
107
|
+
* @return {void}
|
|
108
|
+
*/
|
|
109
|
+
const registerActions = async () => {
|
|
110
|
+
const actionsMap = getActionsMap();
|
|
111
|
+
|
|
112
|
+
// For each API
|
|
113
|
+
for (const [api, value] of Object.entries(actionsMap)) {
|
|
114
|
+
const { controllers } = value;
|
|
115
|
+
|
|
116
|
+
// Register controllers methods as actions
|
|
117
|
+
for (const [controller, actions] of Object.entries(controllers)) {
|
|
118
|
+
// Register each action individually
|
|
119
|
+
await Promise.all(
|
|
120
|
+
actions.map((action) => {
|
|
121
|
+
const actionUID = `${api}.${controller}.${action}`;
|
|
122
|
+
|
|
123
|
+
return providers.action.register(actionUID, {
|
|
124
|
+
api,
|
|
125
|
+
controller,
|
|
126
|
+
action,
|
|
127
|
+
uid: actionUID,
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Create an instance of a content-API permission engine
|
|
136
|
+
// and binds a custom validation handler to it
|
|
137
|
+
const engine = createPermissionEngine({ providers }).on(
|
|
138
|
+
'before-format::validate.permission',
|
|
139
|
+
createValidatePermissionHandler(providers.action)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
engine,
|
|
144
|
+
providers,
|
|
145
|
+
registerActions,
|
|
146
|
+
getActionsMap,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { providerFactory } = require('@strapi/utils');
|
|
4
|
+
|
|
5
|
+
module.exports = (options = {}) => {
|
|
6
|
+
const provider = providerFactory(options);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
...provider,
|
|
10
|
+
|
|
11
|
+
async register(action, payload) {
|
|
12
|
+
if (strapi.isLoaded) {
|
|
13
|
+
throw new Error(`You can't register new actions outside the bootstrap function.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return provider.register(action, payload);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { providerFactory } = require('@strapi/utils');
|
|
4
|
+
|
|
5
|
+
module.exports = (options = {}) => {
|
|
6
|
+
const provider = providerFactory(options);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
...provider,
|
|
10
|
+
|
|
11
|
+
async register(condition) {
|
|
12
|
+
if (strapi.isLoaded) {
|
|
13
|
+
throw new Error(`You can't register new conditions outside the bootstrap function.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return provider.register(condition.name, condition);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -47,7 +47,6 @@ const createComponents = async (uid, data) => {
|
|
|
47
47
|
componentValue.map((value) => createComponent(componentUID, value))
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
// TODO: add order
|
|
51
50
|
componentBody[attributeName] = components.map(({ id }) => {
|
|
52
51
|
return {
|
|
53
52
|
id,
|
|
@@ -268,44 +267,34 @@ const deleteOldDZComponents = async (uid, entityToUpdate, attributeName, dynamic
|
|
|
268
267
|
}
|
|
269
268
|
};
|
|
270
269
|
|
|
271
|
-
const deleteComponents = async (uid, entityToDelete) => {
|
|
270
|
+
const deleteComponents = async (uid, entityToDelete, { loadComponents = true } = {}) => {
|
|
272
271
|
const { attributes = {} } = strapi.getModel(uid);
|
|
273
272
|
|
|
274
273
|
for (const attributeName of Object.keys(attributes)) {
|
|
275
274
|
const attribute = attributes[attributeName];
|
|
276
275
|
|
|
277
|
-
if (attribute.type === 'component') {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const value =
|
|
282
|
-
entityToDelete[attributeName] ||
|
|
283
|
-
(await strapi.query(uid).load(entityToDelete, attributeName));
|
|
284
|
-
|
|
285
|
-
if (!value) {
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (Array.isArray(value)) {
|
|
290
|
-
await Promise.all(value.map((subValue) => deleteComponent(componentUID, subValue)));
|
|
276
|
+
if (attribute.type === 'component' || attribute.type === 'dynamiczone') {
|
|
277
|
+
let value;
|
|
278
|
+
if (loadComponents) {
|
|
279
|
+
value = await strapi.query(uid).load(entityToDelete, attributeName);
|
|
291
280
|
} else {
|
|
292
|
-
|
|
281
|
+
value = entityToDelete[attributeName];
|
|
293
282
|
}
|
|
294
283
|
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (attribute.type === 'dynamiczone') {
|
|
299
|
-
const value =
|
|
300
|
-
entityToDelete[attributeName] ||
|
|
301
|
-
(await strapi.query(uid).load(entityToDelete, attributeName));
|
|
302
|
-
|
|
303
284
|
if (!value) {
|
|
304
285
|
continue;
|
|
305
286
|
}
|
|
306
287
|
|
|
307
|
-
if (
|
|
308
|
-
|
|
288
|
+
if (attribute.type === 'component') {
|
|
289
|
+
const { component: componentUID } = attribute;
|
|
290
|
+
await Promise.all(
|
|
291
|
+
_.castArray(value).map((subValue) => deleteComponent(componentUID, subValue))
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
// delete dynamic zone components
|
|
295
|
+
await Promise.all(
|
|
296
|
+
_.castArray(value).map((subValue) => deleteComponent(subValue.__component, subValue))
|
|
297
|
+
);
|
|
309
298
|
}
|
|
310
299
|
|
|
311
300
|
continue;
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
sanitize,
|
|
11
11
|
} = require('@strapi/utils');
|
|
12
12
|
const { ValidationError } = require('@strapi/utils').errors;
|
|
13
|
+
const { isAnyToMany } = require('@strapi/utils').relations;
|
|
13
14
|
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
|
|
14
15
|
const uploadFiles = require('../utils/upload-files');
|
|
15
16
|
|
|
@@ -23,6 +24,13 @@ const {
|
|
|
23
24
|
const { pickSelectionParams } = require('./params');
|
|
24
25
|
const { applyTransforms } = require('./attributes');
|
|
25
26
|
|
|
27
|
+
const transformLoadParamsToQuery = (uid, field, params = {}, pagination = {}) => {
|
|
28
|
+
return {
|
|
29
|
+
...transformParamsToQuery(uid, { populate: { [field]: params } }).populate[field],
|
|
30
|
+
...pagination,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
26
34
|
// TODO: those should be strapi events used by the webhooks not the other way arround
|
|
27
35
|
const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
|
|
28
36
|
|
|
@@ -211,7 +219,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
|
|
|
211
219
|
const componentsToDelete = await getComponents(uid, entityToDelete);
|
|
212
220
|
|
|
213
221
|
await db.query(uid).delete({ where: { id: entityToDelete.id } });
|
|
214
|
-
await deleteComponents(uid, {
|
|
222
|
+
await deleteComponents(uid, componentsToDelete, { loadComponents: false });
|
|
215
223
|
|
|
216
224
|
await this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
|
|
217
225
|
|
|
@@ -236,7 +244,9 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
|
|
|
236
244
|
);
|
|
237
245
|
|
|
238
246
|
const deletedEntities = await db.query(uid).deleteMany(query);
|
|
239
|
-
await Promise.all(
|
|
247
|
+
await Promise.all(
|
|
248
|
+
componentsToDelete.map((compos) => deleteComponents(uid, compos, { loadComponents: false }))
|
|
249
|
+
);
|
|
240
250
|
|
|
241
251
|
// Trigger webhooks. One for each entity
|
|
242
252
|
await Promise.all(entitiesToDelete.map((entity) => this.emitEvent(uid, ENTRY_DELETE, entity)));
|
|
@@ -245,35 +255,28 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
|
|
|
245
255
|
},
|
|
246
256
|
|
|
247
257
|
load(uid, entity, field, params = {}) {
|
|
248
|
-
|
|
258
|
+
if (!_.isString(field)) {
|
|
259
|
+
throw new Error(`Invalid load. Expected "${field}" to be a string`);
|
|
260
|
+
}
|
|
249
261
|
|
|
250
|
-
|
|
262
|
+
return db.query(uid).load(entity, field, transformLoadParamsToQuery(uid, field, params));
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
loadPages(uid, entity, field, params = {}, pagination = {}) {
|
|
266
|
+
if (!_.isString(field)) {
|
|
267
|
+
throw new Error(`Invalid load. Expected "${field}" to be a string`);
|
|
268
|
+
}
|
|
251
269
|
|
|
252
|
-
const
|
|
270
|
+
const { attributes } = strapi.getModel(uid);
|
|
271
|
+
const attribute = attributes[field];
|
|
253
272
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
Object.assign(loadParams, transformParamsToQuery(attribute.target, params));
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
case 'component': {
|
|
260
|
-
Object.assign(loadParams, transformParamsToQuery(attribute.component, params));
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
case 'dynamiczone': {
|
|
264
|
-
Object.assign(loadParams, transformParamsToQuery(null, params));
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
case 'media': {
|
|
268
|
-
Object.assign(loadParams, transformParamsToQuery('plugin::upload.file', params));
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
default: {
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
273
|
+
if (!isAnyToMany(attribute)) {
|
|
274
|
+
throw new Error(`Invalid load. Expected "${field}" to be an anyToMany relational attribute`);
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
|
|
277
|
+
const query = transformLoadParamsToQuery(uid, field, params, pagination);
|
|
278
|
+
|
|
279
|
+
return db.query(uid).loadPages(entity, field, query);
|
|
277
280
|
},
|
|
278
281
|
});
|
|
279
282
|
|
|
@@ -7,6 +7,7 @@ const isDocker = require('is-docker');
|
|
|
7
7
|
const fetch = require('node-fetch');
|
|
8
8
|
const ciEnv = require('ci-info');
|
|
9
9
|
const { isUsingTypeScriptSync } = require('@strapi/typescript-utils');
|
|
10
|
+
const { env } = require('@strapi/utils');
|
|
10
11
|
const ee = require('../../utils/ee');
|
|
11
12
|
const machineID = require('../../utils/machine-id');
|
|
12
13
|
const stringifyDeep = require('./stringify-deep');
|
|
@@ -54,6 +55,7 @@ module.exports = (strapi) => {
|
|
|
54
55
|
projectType: isEE ? 'Enterprise' : 'Community',
|
|
55
56
|
useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath),
|
|
56
57
|
useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
|
|
58
|
+
isHostedOnStrapiCloud: env('STRAPI_HOSTING', null) === 'strapi.cloud',
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
addPackageJsonStrapiMetadata(anonymousMetadata, strapi);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { propOr, isArray, isNil } = require('lodash/fp');
|
|
5
|
+
const { importDefault } = require('@strapi/utils');
|
|
5
6
|
|
|
6
7
|
const getMiddlewareConfig = propOr([], 'config.middlewares');
|
|
7
8
|
|
|
@@ -119,7 +120,7 @@ const resolveCustomMiddleware = (resolve, strapi) => {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
try {
|
|
122
|
-
return
|
|
123
|
+
return importDefault(modulePath);
|
|
123
124
|
} catch (err) {
|
|
124
125
|
throw new Error(`Could not load middleware "${modulePath}".`);
|
|
125
126
|
}
|
|
@@ -29,6 +29,9 @@ export type NonUniqueAttribute = { unique: false };
|
|
|
29
29
|
export type ConfigurableAttribute = { configurable: true };
|
|
30
30
|
export type NonConfigurableAttribute = { configurable: false };
|
|
31
31
|
|
|
32
|
+
// custom field
|
|
33
|
+
export type CustomField<T extends string, P extends object = undefined> = { customField: T, options?: P };
|
|
34
|
+
|
|
32
35
|
// min/max
|
|
33
36
|
export type SetMinMax<T extends MinMaxOption<U>, U = number> = T;
|
|
34
37
|
|
|
@@ -5,6 +5,28 @@ import type { StringMap } from './utils';
|
|
|
5
5
|
import type { GenericController } from '../../../core-api/controller'
|
|
6
6
|
import type { GenericService } from '../../../core-api/service'
|
|
7
7
|
|
|
8
|
+
// TODO move custom fields types to a separate file
|
|
9
|
+
interface CustomFieldServerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The name of the custom field
|
|
12
|
+
*/
|
|
13
|
+
name: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The name of the plugin creating the custom field
|
|
17
|
+
*/
|
|
18
|
+
plugin?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The existing Strapi data type the custom field uses
|
|
22
|
+
*/
|
|
23
|
+
type: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CustomFields {
|
|
27
|
+
register: (customFields: CustomFieldServerOptions[] | CustomFieldServerOptions) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
/**
|
|
9
31
|
* The Strapi interface implemented by the main Strapi class.
|
|
10
32
|
*/
|
|
@@ -24,6 +46,11 @@ export interface Strapi {
|
|
|
24
46
|
*/
|
|
25
47
|
readonly auth: any;
|
|
26
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Getter for the Strapi content API container
|
|
51
|
+
*/
|
|
52
|
+
readonly contentAPI: any;
|
|
53
|
+
|
|
27
54
|
/**
|
|
28
55
|
* Getter for the Strapi sanitizers container
|
|
29
56
|
*/
|
|
@@ -65,6 +92,13 @@ export interface Strapi {
|
|
|
65
92
|
*/
|
|
66
93
|
contentType(uid: string): any;
|
|
67
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The custom fields registry
|
|
97
|
+
*
|
|
98
|
+
* It returns the custom fields interface
|
|
99
|
+
*/
|
|
100
|
+
readonly customFields: CustomFields;
|
|
101
|
+
|
|
68
102
|
/**
|
|
69
103
|
* Getter for the Strapi policies container
|
|
70
104
|
*
|
|
@@ -195,7 +229,7 @@ export interface Strapi {
|
|
|
195
229
|
/**
|
|
196
230
|
* Restart the server and reload all the configuration.
|
|
197
231
|
* It re-runs all the lifecycles phases.
|
|
198
|
-
*
|
|
232
|
+
*
|
|
199
233
|
* @example
|
|
200
234
|
* ``` ts
|
|
201
235
|
* setImmediate(() => strapi.reload());
|
|
@@ -223,13 +257,13 @@ export interface Strapi {
|
|
|
223
257
|
/**
|
|
224
258
|
* Opent he administration panel in a browser if the option is enabled.
|
|
225
259
|
* You can disable it using the admin.autoOpen configuration variable.
|
|
226
|
-
*
|
|
260
|
+
*
|
|
227
261
|
* Note: It only works in development envs.
|
|
228
262
|
*/
|
|
229
263
|
openAdmin(options: { isInitialized: boolean }): Promise<void>;
|
|
230
264
|
|
|
231
265
|
/**
|
|
232
|
-
* Load the admin panel server logic into the server code and initialize its configuration.
|
|
266
|
+
* Load the admin panel server logic into the server code and initialize its configuration.
|
|
233
267
|
*/
|
|
234
268
|
loadAdmin(): Promise<void>;
|
|
235
269
|
|
|
@@ -288,7 +322,7 @@ export interface Strapi {
|
|
|
288
322
|
container: any;
|
|
289
323
|
|
|
290
324
|
/**
|
|
291
|
-
* References to all the directories handled by Strapi
|
|
325
|
+
* References to all the directories handled by Strapi
|
|
292
326
|
*/
|
|
293
327
|
dirs: StrapiDirectories;
|
|
294
328
|
|
|
@@ -323,7 +357,7 @@ export interface Strapi {
|
|
|
323
357
|
startupLogger: any;
|
|
324
358
|
|
|
325
359
|
/**
|
|
326
|
-
* Strapi logger used to send errors, warning or information messages
|
|
360
|
+
* Strapi logger used to send errors, warning or information messages
|
|
327
361
|
*/
|
|
328
362
|
log: any;
|
|
329
363
|
|
|
@@ -356,7 +390,7 @@ export interface Strapi {
|
|
|
356
390
|
/**
|
|
357
391
|
* Entity Service instance
|
|
358
392
|
*/
|
|
359
|
-
entityService: any;
|
|
393
|
+
entityService: any;
|
|
360
394
|
}
|
|
361
395
|
|
|
362
396
|
export interface Lifecycles {
|
|
@@ -389,4 +423,4 @@ export interface StrapiDirectories {
|
|
|
389
423
|
middlewares: string;
|
|
390
424
|
config: string;
|
|
391
425
|
};
|
|
392
|
-
}
|
|
426
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const convertCustomFieldType = (strapi) => {
|
|
4
|
+
const allContentTypeSchemaAttributes = Object.values(strapi.contentTypes).map(
|
|
5
|
+
(schema) => schema.attributes
|
|
6
|
+
);
|
|
7
|
+
const allComponentSchemaAttributes = Object.values(strapi.components).map(
|
|
8
|
+
(schema) => schema.attributes
|
|
9
|
+
);
|
|
10
|
+
const allSchemasAttributes = [...allContentTypeSchemaAttributes, ...allComponentSchemaAttributes];
|
|
11
|
+
|
|
12
|
+
for (const schemaAttrbutes of allSchemasAttributes) {
|
|
13
|
+
for (const attribute of Object.values(schemaAttrbutes)) {
|
|
14
|
+
if (attribute.type === 'customField') {
|
|
15
|
+
const customField = strapi.container.get('custom-fields').get(attribute.customField);
|
|
16
|
+
attribute.type = customField.type;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = convertCustomFieldType;
|
package/lib/utils/index.js
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
const openBrowser = require('./open-browser');
|
|
4
4
|
const isInitialized = require('./is-initialized');
|
|
5
5
|
const getDirs = require('./get-dirs');
|
|
6
|
-
const importDefault = require('./import-default');
|
|
7
6
|
|
|
8
7
|
module.exports = {
|
|
9
8
|
isInitialized,
|
|
10
9
|
openBrowser,
|
|
11
10
|
getDirs,
|
|
12
|
-
importDefault,
|
|
13
11
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/strapi",
|
|
3
|
-
"version": "4.5.0
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "An open source headless CMS solution to create and manage your own API. It provides a powerful dashboard and features to make your life easier. Databases supported: MySQL, MariaDB, PostgreSQL, SQLite",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
|
@@ -78,19 +78,20 @@
|
|
|
78
78
|
"test:unit": "jest --verbose"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@koa/cors": "3.4.
|
|
81
|
+
"@koa/cors": "3.4.3",
|
|
82
82
|
"@koa/router": "10.1.1",
|
|
83
|
-
"@strapi/admin": "4.5.0
|
|
84
|
-
"@strapi/database": "4.5.0
|
|
85
|
-
"@strapi/generate-new": "4.5.0
|
|
86
|
-
"@strapi/generators": "4.5.0
|
|
87
|
-
"@strapi/logger": "4.5.0
|
|
88
|
-
"@strapi/
|
|
89
|
-
"@strapi/plugin-content-
|
|
90
|
-
"@strapi/plugin-
|
|
91
|
-
"@strapi/plugin-
|
|
92
|
-
"@strapi/
|
|
93
|
-
"@strapi/utils": "4.5.0
|
|
83
|
+
"@strapi/admin": "4.5.0",
|
|
84
|
+
"@strapi/database": "4.5.0",
|
|
85
|
+
"@strapi/generate-new": "4.5.0",
|
|
86
|
+
"@strapi/generators": "4.5.0",
|
|
87
|
+
"@strapi/logger": "4.5.0",
|
|
88
|
+
"@strapi/permissions": "4.5.0",
|
|
89
|
+
"@strapi/plugin-content-manager": "4.5.0",
|
|
90
|
+
"@strapi/plugin-content-type-builder": "4.5.0",
|
|
91
|
+
"@strapi/plugin-email": "4.5.0",
|
|
92
|
+
"@strapi/plugin-upload": "4.5.0",
|
|
93
|
+
"@strapi/typescript-utils": "4.5.0",
|
|
94
|
+
"@strapi/utils": "4.5.0",
|
|
94
95
|
"bcryptjs": "2.4.3",
|
|
95
96
|
"boxen": "5.1.2",
|
|
96
97
|
"chalk": "4.1.2",
|
|
@@ -139,5 +140,5 @@
|
|
|
139
140
|
"node": ">=14.19.1 <=18.x.x",
|
|
140
141
|
"npm": ">=6.0.0"
|
|
141
142
|
},
|
|
142
|
-
"gitHead": "
|
|
143
|
+
"gitHead": "33debd57010667a3fc5dfa343a673206cfb956e1"
|
|
143
144
|
}
|