@strapi/strapi 4.4.0-alpha.0 → 4.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +17 -8
  2. package/lib/Strapi.js +10 -10
  3. package/lib/commands/configurationDump.js +2 -2
  4. package/lib/core/loaders/apis.js +1 -1
  5. package/lib/core/loaders/plugins/get-enabled-plugins.js +3 -1
  6. package/lib/core/loaders/plugins/index.js +5 -5
  7. package/lib/core/registries/content-types.js +2 -2
  8. package/lib/core/registries/controllers.js +2 -2
  9. package/lib/core/registries/hooks.js +1 -1
  10. package/lib/core/registries/middlewares.js +2 -2
  11. package/lib/core/registries/policies.js +1 -1
  12. package/lib/core/registries/services.js +2 -2
  13. package/lib/core-api/controller/transform.js +1 -1
  14. package/lib/services/auth/index.js +4 -1
  15. package/lib/services/content-api/index.js +74 -0
  16. package/lib/services/content-api/permissions/engine.js +5 -0
  17. package/lib/services/content-api/permissions/index.js +148 -0
  18. package/lib/services/content-api/permissions/providers/action.js +19 -0
  19. package/lib/services/content-api/permissions/providers/condition.js +19 -0
  20. package/lib/services/content-api/permissions/providers/index.js +9 -0
  21. package/lib/services/cron.js +1 -1
  22. package/lib/services/entity-service/components.js +28 -8
  23. package/lib/services/entity-service/index.js +9 -2
  24. package/lib/services/entity-validator/index.js +22 -17
  25. package/lib/services/metrics/middleware.js +1 -1
  26. package/lib/services/metrics/sender.js +2 -2
  27. package/lib/services/server/register-routes.js +2 -2
  28. package/lib/services/utils/upload-files.js +1 -1
  29. package/lib/services/worker-queue.js +2 -2
  30. package/lib/types/core/strapi/index.d.ts +5 -0
  31. package/lib/utils/addSlash.js +6 -5
  32. package/package.json +15 -14
  33. package/lib/core/registries/custom-fields.js +0 -54
  34. package/lib/services/custom-fields.js +0 -11
  35. package/lib/utils/convert-custom-field-type.js +0 -22
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  <p align="center">
2
- <a href="https://strapi.io">
2
+ <a href="https://strapi.io/#gh-light-mode-only">
3
3
  <img src="https://strapi.io/assets/strapi-logo-dark.svg" width="318px" alt="Strapi logo" />
4
4
  </a>
5
+ <a href="https://strapi.io/#gh-dark-mode-only">
6
+ <img src="https://strapi.io/assets/strapi-logo-light.svg" width="318px" alt="Strapi logo" />
7
+ </a>
5
8
  </p>
9
+
6
10
  <h3 align="center">API creation made simple, secure and fast.</h3>
7
11
  <p align="center">The most advanced open-source headless CMS to build powerful APIs with no effort.</p>
8
12
  <p align="center"><a href="https://strapi.io/demo">Try live demo</a></p>
@@ -18,6 +22,9 @@
18
22
  <a href="https://discord.strapi.io">
19
23
  <img src="https://img.shields.io/discord/811989166782021633?label=Discord" alt="Strapi on Discord" />
20
24
  </a>
25
+ <a href="https://github.com/strapi/strapi/actions/workflows/nightly.yml">
26
+ <img src="https://github.com/strapi/strapi/actions/workflows/nightly.yml/badge.svg" alt="Strapi Nightly Release Build Status" />
27
+ </a>
21
28
  </p>
22
29
 
23
30
  <br>
@@ -79,15 +86,17 @@ Complete installation requirements can be found in the documentation under <a hr
79
86
 
80
87
  **Node:**
81
88
 
82
- - NodeJS >= 12 <= 16
89
+ - NodeJS >= 14 <= 18
83
90
  - NPM >= 6.x
84
91
 
85
92
  **Database:**
86
93
 
87
- - MySQL >= 5.7.8
88
- - MariaDB >= 10.2.7
89
- - PostgreSQL >= 10
90
- - SQLite >= 3
94
+ | Database | Minimum | Recommended |
95
+ | ---------- | ------- | ----------- |
96
+ | MySQL | 5.7.8 | 8.0 |
97
+ | MariaDB | 10.3 | 10.6 |
98
+ | PostgreSQL | 11.0 | 14.0 |
99
+ | SQLite | 3 | 3 |
91
100
 
92
101
  **We recommend always using the latest version of Strapi to start your new projects**.
93
102
 
@@ -114,7 +123,7 @@ For general help using Strapi, please refer to [the official Strapi documentatio
114
123
  - [Discord](https://discord.strapi.io) (For live discussion with the Community and Strapi team)
115
124
  - [GitHub](https://github.com/strapi/strapi) (Bug reports, Contributions)
116
125
  - [Community Forum](https://forum.strapi.io) (Questions and Discussions)
117
- - [Roadmap & Feature Requests](https://feedback.strapi.io/)
126
+ - [Feedback section](https://feedback.strapi.io) (Roadmap, Feature requests)
118
127
  - [Twitter](https://twitter.com/strapijs) (Get the news fast)
119
128
  - [Facebook](https://www.facebook.com/Strapi-616063331867161)
120
129
  - [YouTube Channel](https://www.youtube.com/strapi) (Learn from Video Tutorials)
@@ -125,7 +134,7 @@ Follow our [migration guides](https://docs.strapi.io/developer-docs/latest/updat
125
134
 
126
135
  ## Roadmap
127
136
 
128
- Check out our [roadmap](https://feedback.strapi.io/) to get informed of the latest features released and the upcoming ones. You may also give us insights and vote for a specific feature.
137
+ Check out our [roadmap](https://feedback.strapi.io) to get informed of the latest features released and the upcoming ones. You may also give us insights and vote for a specific feature.
129
138
 
130
139
  ## Documentation
131
140
 
package/lib/Strapi.js CHANGED
@@ -22,7 +22,7 @@ const createCronService = require('./services/cron');
22
22
  const entityValidator = require('./services/entity-validator');
23
23
  const createTelemetry = require('./services/metrics');
24
24
  const createAuth = require('./services/auth');
25
- const createCustomFields = require('./services/custom-fields');
25
+ const createContentAPI = require('./services/content-api');
26
26
  const createUpdateNotifier = require('./utils/update-notifier');
27
27
  const createStartupLogger = require('./utils/startup-logger');
28
28
  const { LIFECYCLES } = require('./utils/lifecycles');
@@ -35,14 +35,12 @@ const hooksRegistry = require('./core/registries/hooks');
35
35
  const controllersRegistry = require('./core/registries/controllers');
36
36
  const modulesRegistry = require('./core/registries/modules');
37
37
  const pluginsRegistry = require('./core/registries/plugins');
38
- const customFieldsRegistry = require('./core/registries/custom-fields');
39
38
  const createConfigProvider = require('./core/registries/config');
40
39
  const apisRegistry = require('./core/registries/apis');
41
40
  const bootstrap = require('./core/bootstrap');
42
41
  const loaders = require('./core/loaders');
43
42
  const { destroyOnSignal } = require('./utils/signals');
44
43
  const sanitizersRegistry = require('./core/registries/sanitizers');
45
- const convertCustomFieldType = require('./utils/convert-custom-field-type');
46
44
 
47
45
  // TODO: move somewhere else
48
46
  const draftAndPublishSync = require('./migrations/draft-publish');
@@ -77,7 +75,7 @@ class Strapi {
77
75
  // Load the app configuration from the dist directory
78
76
  const appConfig = loadConfiguration({ appDir: rootDirs.app, distDir: rootDirs.dist }, opts);
79
77
 
80
- // Instanciate the Strapi container
78
+ // Instantiate the Strapi container
81
79
  this.container = createContainer(this);
82
80
 
83
81
  // Register every Strapi registry in the container
@@ -90,9 +88,9 @@ class Strapi {
90
88
  this.container.register('controllers', controllersRegistry(this));
91
89
  this.container.register('modules', modulesRegistry(this));
92
90
  this.container.register('plugins', pluginsRegistry(this));
93
- this.container.register('custom-fields', customFieldsRegistry(this));
94
91
  this.container.register('apis', apisRegistry(this));
95
92
  this.container.register('auth', createAuth(this));
93
+ this.container.register('content-api', createContentAPI(this));
96
94
  this.container.register('sanitizers', sanitizersRegistry(this));
97
95
 
98
96
  // Create a mapping of every useful directory (for the app, dist and static directories)
@@ -102,7 +100,7 @@ class Strapi {
102
100
  this.isLoaded = false;
103
101
  this.reload = this.reload();
104
102
 
105
- // Instanciate the Koa app & the HTTP server
103
+ // Instantiate the Koa app & the HTTP server
106
104
  this.server = createServer(this);
107
105
 
108
106
  // Strapi utils instanciation
@@ -113,8 +111,6 @@ class Strapi {
113
111
  this.cron = createCronService();
114
112
  this.telemetry = createTelemetry(this);
115
113
 
116
- this.customFields = createCustomFields(this);
117
-
118
114
  createUpdateNotifier(this).notify();
119
115
  }
120
116
 
@@ -194,6 +190,10 @@ class Strapi {
194
190
  return this.container.get('auth');
195
191
  }
196
192
 
193
+ get contentAPI() {
194
+ return this.container.get('content-api');
195
+ }
196
+
197
197
  get sanitizers() {
198
198
  return this.container.get('sanitizers');
199
199
  }
@@ -451,6 +451,8 @@ class Strapi {
451
451
  await this.server.initMiddlewares();
452
452
  await this.server.initRouting();
453
453
 
454
+ await this.contentAPI.permissions.registerActions();
455
+
454
456
  await this.runLifecyclesFunctions(LIFECYCLES.BOOTSTRAP);
455
457
 
456
458
  this.cron.start();
@@ -460,8 +462,6 @@ class Strapi {
460
462
 
461
463
  async load() {
462
464
  await this.register();
463
- // Swap type customField for underlying data type
464
- convertCustomFieldType(this);
465
465
  await this.bootstrap();
466
466
 
467
467
  this.isLoaded = true;
@@ -9,7 +9,7 @@ const CHUNK_SIZE = 100;
9
9
  * Will dump configurations to a file or stdout
10
10
  * @param {string} file filepath to use as output
11
11
  */
12
- module.exports = async function ({ file: filePath, pretty }) {
12
+ module.exports = async ({ file: filePath, pretty }) => {
13
13
  const output = filePath ? fs.createWriteStream(filePath) : process.stdout;
14
14
 
15
15
  const appContext = await strapi.compile();
@@ -21,7 +21,7 @@ module.exports = async function ({ file: filePath, pretty }) {
21
21
 
22
22
  const pageCount = Math.ceil(count / CHUNK_SIZE);
23
23
 
24
- for (let page = 0; page < pageCount; page++) {
24
+ for (let page = 0; page < pageCount; page += 1) {
25
25
  const results = await app
26
26
  .query('strapi::core-store')
27
27
  .findMany({ limit: CHUNK_SIZE, offset: page * CHUNK_SIZE, orderBy: 'key' });
@@ -40,7 +40,7 @@ module.exports = async (strapi) => {
40
40
 
41
41
  validateContentTypesUnicity(apis);
42
42
 
43
- for (const apiName in apis) {
43
+ for (const apiName of Object.keys(apis)) {
44
44
  strapi.container.get('apis').add(apiName, apis[apiName]);
45
45
  }
46
46
  };
@@ -59,7 +59,9 @@ const getEnabledPlugins = async (strapi) => {
59
59
  }
60
60
 
61
61
  const installedPlugins = {};
62
- for (const dep in strapi.config.get('info.dependencies', {})) {
62
+ const dependencies = strapi.config.get('info.dependencies', {});
63
+
64
+ for (const dep of Object.keys(dependencies)) {
63
65
  const packagePath = join(dep, 'package.json');
64
66
  let packageInfo;
65
67
  try {
@@ -34,10 +34,10 @@ const applyUserExtension = async (plugins) => {
34
34
  const extendedSchemas = await loadFiles(extensionsDir, '**/content-types/**/schema.json');
35
35
  const strapiServers = await loadFiles(extensionsDir, '**/strapi-server.js');
36
36
 
37
- for (const pluginName in plugins) {
37
+ for (const pluginName of Object.keys(plugins)) {
38
38
  const plugin = plugins[pluginName];
39
39
  // first: load json schema
40
- for (const ctName in plugin.contentTypes) {
40
+ for (const ctName of Object.keys(plugin.contentTypes)) {
41
41
  const extendedSchema = get([pluginName, 'content-types', ctName, 'schema'], extendedSchemas);
42
42
  if (extendedSchema) {
43
43
  plugin.contentTypes[ctName].schema = {
@@ -57,7 +57,7 @@ const applyUserExtension = async (plugins) => {
57
57
  const applyUserConfig = async (plugins) => {
58
58
  const userPluginsConfig = await getUserPluginsConfig();
59
59
 
60
- for (const pluginName in plugins) {
60
+ for (const pluginName of Object.keys(plugins)) {
61
61
  const plugin = plugins[pluginName];
62
62
  const userPluginConfig = getOr({}, `${pluginName}.config`, userPluginsConfig);
63
63
  const defaultConfig =
@@ -82,7 +82,7 @@ const loadPlugins = async (strapi) => {
82
82
 
83
83
  strapi.config.set('enabledPlugins', enabledPlugins);
84
84
 
85
- for (const pluginName in enabledPlugins) {
85
+ for (const pluginName of Object.keys(enabledPlugins)) {
86
86
  const enabledPlugin = enabledPlugins[pluginName];
87
87
 
88
88
  const serverEntrypointPath = join(enabledPlugin.pathToPlugin, 'strapi-server.js');
@@ -100,7 +100,7 @@ const loadPlugins = async (strapi) => {
100
100
  await applyUserConfig(plugins);
101
101
  await applyUserExtension(plugins);
102
102
 
103
- for (const pluginName in plugins) {
103
+ for (const pluginName of Object.keys(plugins)) {
104
104
  strapi.container.get('plugins').add(pluginName, plugins[pluginName]);
105
105
  }
106
106
  };
@@ -5,7 +5,7 @@ const { createContentType } = require('../domain/content-type');
5
5
  const { addNamespace, hasNamespace } = require('../utils');
6
6
 
7
7
  const validateKeySameToSingularName = (contentTypes) => {
8
- for (const ctName in contentTypes) {
8
+ for (const ctName of Object.keys(contentTypes)) {
9
9
  const contentType = contentTypes[ctName];
10
10
 
11
11
  if (ctName !== contentType.schema.info.singularName) {
@@ -63,7 +63,7 @@ const contentTypesRegistry = () => {
63
63
  add(namespace, newContentTypes) {
64
64
  validateKeySameToSingularName(newContentTypes);
65
65
 
66
- for (const rawCtName in newContentTypes) {
66
+ for (const rawCtName of Object.keys(newContentTypes)) {
67
67
  const uid = addNamespace(rawCtName, namespace);
68
68
 
69
69
  if (has(uid, contentTypes)) {
@@ -48,7 +48,7 @@ const controllersRegistry = () => {
48
48
  const filteredControllers = pickBy((_, uid) => hasNamespace(uid, namespace))(controllers);
49
49
 
50
50
  const map = {};
51
- for (const uid in filteredControllers) {
51
+ for (const uid of Object.keys(filteredControllers)) {
52
52
  Object.defineProperty(map, uid, {
53
53
  enumerable: true,
54
54
  get: () => {
@@ -78,7 +78,7 @@ const controllersRegistry = () => {
78
78
  * @returns
79
79
  */
80
80
  add(namespace, newControllers) {
81
- for (const controllerName in newControllers) {
81
+ for (const controllerName of Object.keys(newControllers)) {
82
82
  const controller = newControllers[controllerName];
83
83
  const uid = addNamespace(controllerName, namespace);
84
84
 
@@ -54,7 +54,7 @@ const hooksRegistry = () => {
54
54
  * @returns
55
55
  */
56
56
  add(namespace, hooks) {
57
- for (const hookName in hooks) {
57
+ for (const hookName of Object.keys(hooks)) {
58
58
  const hook = hooks[hookName];
59
59
  const uid = addNamespace(hookName, namespace);
60
60
 
@@ -54,8 +54,8 @@ const middlewaresRegistry = () => {
54
54
  * @param {{ [key: string]: Middleware }} newMiddlewares
55
55
  * @returns
56
56
  */
57
- add(namespace, rawMiddlewares) {
58
- for (const middlewareName in rawMiddlewares) {
57
+ add(namespace, rawMiddlewares = {}) {
58
+ for (const middlewareName of Object.keys(rawMiddlewares)) {
59
59
  const middleware = rawMiddlewares[middlewareName];
60
60
  const uid = addNamespace(middlewareName, namespace);
61
61
 
@@ -55,7 +55,7 @@ const policiesRegistry = () => {
55
55
  * @returns
56
56
  */
57
57
  add(namespace, newPolicies) {
58
- for (const policyName in newPolicies) {
58
+ for (const policyName of Object.keys(newPolicies)) {
59
59
  const policy = newPolicies[policyName];
60
60
  const uid = addNamespace(policyName, namespace);
61
61
 
@@ -48,7 +48,7 @@ const servicesRegistry = (strapi) => {
48
48
 
49
49
  // create lazy accessor to avoid instantiating the services;
50
50
  const map = {};
51
- for (const uid in filteredServices) {
51
+ for (const uid of Object.keys(filteredServices)) {
52
52
  Object.defineProperty(map, uid, {
53
53
  enumerable: true,
54
54
  get: () => {
@@ -78,7 +78,7 @@ const servicesRegistry = (strapi) => {
78
78
  * @returns
79
79
  */
80
80
  add(namespace, newServices) {
81
- for (const serviceName in newServices) {
81
+ for (const serviceName of Object.keys(newServices)) {
82
82
  const service = newServices[serviceName];
83
83
  const uid = addNamespace(serviceName, namespace);
84
84
 
@@ -56,7 +56,7 @@ const transformEntry = (entry, type) => {
56
56
 
57
57
  const attributeValues = {};
58
58
 
59
- for (const key in properties) {
59
+ for (const key of Object.keys(properties)) {
60
60
  const property = properties[key];
61
61
  const attribute = type && type.attributes[key];
62
62
 
@@ -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, error = null, credentials } = result || {};
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,5 @@
1
+ 'use strict';
2
+
3
+ const permissions = require('@strapi/permissions');
4
+
5
+ module.exports = ({ providers }) => permissions.engine.new({ providers });
@@ -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
+ };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const createActionProvider = require('./action');
4
+ const createConditionProvider = require('./condition');
5
+
6
+ module.exports = {
7
+ createActionProvider,
8
+ createConditionProvider,
9
+ };
@@ -9,7 +9,7 @@ const createCronService = () => {
9
9
 
10
10
  return {
11
11
  add(tasks = {}) {
12
- for (const taskExpression in tasks) {
12
+ for (const taskExpression of Object.keys(tasks)) {
13
13
  const taskValue = tasks[taskExpression];
14
14
 
15
15
  let fn;
@@ -5,6 +5,7 @@ const { has, prop, omit, toString } = require('lodash/fp');
5
5
 
6
6
  const { contentTypes: contentTypesUtils } = require('@strapi/utils');
7
7
  const { ApplicationError } = require('@strapi/utils').errors;
8
+ const { getComponentAttributes } = require('@strapi/utils').contentTypes;
8
9
 
9
10
  const omitComponentData = (contentType, data) => {
10
11
  const { attributes } = contentType;
@@ -17,11 +18,11 @@ const omitComponentData = (contentType, data) => {
17
18
 
18
19
  // NOTE: we could generalize the logic to allow CRUD of relation directly in the DB layer
19
20
  const createComponents = async (uid, data) => {
20
- const { attributes } = strapi.getModel(uid);
21
+ const { attributes = {} } = strapi.getModel(uid);
21
22
 
22
23
  const componentBody = {};
23
24
 
24
- for (const attributeName in attributes) {
25
+ for (const attributeName of Object.keys(attributes)) {
25
26
  const attribute = attributes[attributeName];
26
27
 
27
28
  if (!has(attributeName, data) || !contentTypesUtils.isComponentAttribute(attribute)) {
@@ -100,16 +101,28 @@ const createComponents = async (uid, data) => {
100
101
  return componentBody;
101
102
  };
102
103
 
104
+ /**
105
+ * @param {str} uid
106
+ * @param {object} entity
107
+ * @return {Promise<{uid: string, entity: object}>}
108
+ */
109
+ const getComponents = async (uid, entity) => {
110
+ const componentAttributes = getComponentAttributes(strapi.getModel(uid));
111
+
112
+ if (_.isEmpty(componentAttributes)) return {};
113
+ return strapi.query(uid).load(entity, componentAttributes);
114
+ };
115
+
103
116
  /*
104
117
  delete old components
105
118
  create or update
106
119
  */
107
120
  const updateComponents = async (uid, entityToUpdate, data) => {
108
- const { attributes } = strapi.getModel(uid);
121
+ const { attributes = {} } = strapi.getModel(uid);
109
122
 
110
123
  const componentBody = {};
111
124
 
112
- for (const attributeName in attributes) {
125
+ for (const attributeName of Object.keys(attributes)) {
113
126
  const attribute = attributes[attributeName];
114
127
 
115
128
  if (!has(attributeName, data)) {
@@ -262,15 +275,18 @@ const deleteOldDZComponents = async (uid, entityToUpdate, attributeName, dynamic
262
275
  };
263
276
 
264
277
  const deleteComponents = async (uid, entityToDelete) => {
265
- const { attributes } = strapi.getModel(uid);
278
+ const { attributes = {} } = strapi.getModel(uid);
266
279
 
267
- for (const attributeName in attributes) {
280
+ for (const attributeName of Object.keys(attributes)) {
268
281
  const attribute = attributes[attributeName];
269
282
 
270
283
  if (attribute.type === 'component') {
271
284
  const { component: componentUID } = attribute;
272
285
 
273
- const value = await strapi.query(uid).load(entityToDelete, attributeName);
286
+ // Load attribute value if it's not already loaded
287
+ const value =
288
+ entityToDelete[attributeName] ||
289
+ (await strapi.query(uid).load(entityToDelete, attributeName));
274
290
 
275
291
  if (!value) {
276
292
  continue;
@@ -286,7 +302,9 @@ const deleteComponents = async (uid, entityToDelete) => {
286
302
  }
287
303
 
288
304
  if (attribute.type === 'dynamiczone') {
289
- const value = await strapi.query(uid).load(entityToDelete, attributeName);
305
+ const value =
306
+ entityToDelete[attributeName] ||
307
+ (await strapi.query(uid).load(entityToDelete, attributeName));
290
308
 
291
309
  if (!value) {
292
310
  continue;
@@ -352,7 +370,9 @@ const deleteComponent = async (uid, componentToDelete) => {
352
370
 
353
371
  module.exports = {
354
372
  omitComponentData,
373
+ getComponents,
355
374
  createComponents,
356
375
  updateComponents,
357
376
  deleteComponents,
377
+ deleteComponent,
358
378
  };
@@ -14,6 +14,7 @@ const uploadFiles = require('../utils/upload-files');
14
14
 
15
15
  const {
16
16
  omitComponentData,
17
+ getComponents,
17
18
  createComponents,
18
19
  updateComponents,
19
20
  deleteComponents,
@@ -213,8 +214,10 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
213
214
  return null;
214
215
  }
215
216
 
217
+ const componentsToDelete = await getComponents(uid, entityToDelete);
218
+
216
219
  await db.query(uid).delete({ where: { id: entityToDelete.id } });
217
- await deleteComponents(uid, entityToDelete);
220
+ await deleteComponents(uid, { ...entityToDelete, ...componentsToDelete });
218
221
 
219
222
  await this.emitEvent(uid, ENTRY_DELETE, entityToDelete);
220
223
 
@@ -234,8 +237,12 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
234
237
  return null;
235
238
  }
236
239
 
240
+ const componentsToDelete = await Promise.all(
241
+ entitiesToDelete.map((entityToDelete) => getComponents(uid, entityToDelete))
242
+ );
243
+
237
244
  const deletedEntities = await db.query(uid).deleteMany(query);
238
- await Promise.all(entitiesToDelete.map((entity) => deleteComponents(uid, entity)));
245
+ await Promise.all(componentsToDelete.map((compos) => deleteComponents(uid, compos)));
239
246
 
240
247
  // Trigger webhooks. One for each entity
241
248
  await Promise.all(entitiesToDelete.map((entity) => this.emitEvent(uid, ENTRY_DELETE, entity)));
@@ -14,51 +14,56 @@ const { isMediaAttribute, isScalarAttribute, getWritableAttributes } = strapiUti
14
14
  const { ValidationError } = strapiUtils.errors;
15
15
 
16
16
  const addMinMax = (validator, { attr, updatedAttribute }) => {
17
+ let nextValidator = validator;
18
+
17
19
  if (
18
20
  Number.isInteger(attr.min) &&
19
21
  (attr.required || (Array.isArray(updatedAttribute.value) && updatedAttribute.value.length > 0))
20
22
  ) {
21
- validator = validator.min(attr.min);
23
+ nextValidator = nextValidator.min(attr.min);
22
24
  }
23
25
  if (Number.isInteger(attr.max)) {
24
- validator = validator.max(attr.max);
26
+ nextValidator = nextValidator.max(attr.max);
25
27
  }
26
- return validator;
28
+ return nextValidator;
27
29
  };
28
30
 
29
- const addRequiredValidation =
30
- (createOrUpdate) =>
31
- (validator, { attr: { required } }) => {
31
+ const addRequiredValidation = (createOrUpdate) => {
32
+ return (validator, { attr: { required } }) => {
33
+ let nextValidator = validator;
32
34
  if (required) {
33
35
  if (createOrUpdate === 'creation') {
34
- validator = validator.notNil();
36
+ nextValidator = nextValidator.notNil();
35
37
  } else if (createOrUpdate === 'update') {
36
- validator = validator.notNull();
38
+ nextValidator = nextValidator.notNull();
37
39
  }
38
40
  } else {
39
- validator = validator.nullable();
41
+ nextValidator = nextValidator.nullable();
40
42
  }
41
- return validator;
43
+ return nextValidator;
42
44
  };
45
+ };
46
+
47
+ const addDefault = (createOrUpdate) => {
48
+ return (validator, { attr }) => {
49
+ let nextValidator = validator;
43
50
 
44
- const addDefault =
45
- (createOrUpdate) =>
46
- (validator, { attr }) => {
47
51
  if (createOrUpdate === 'creation') {
48
52
  if (
49
53
  ((attr.type === 'component' && attr.repeatable) || attr.type === 'dynamiczone') &&
50
54
  !attr.required
51
55
  ) {
52
- validator = validator.default([]);
56
+ nextValidator = nextValidator.default([]);
53
57
  } else {
54
- validator = validator.default(attr.default);
58
+ nextValidator = nextValidator.default(attr.default);
55
59
  }
56
60
  } else {
57
- validator = validator.default(undefined);
61
+ nextValidator = nextValidator.default(undefined);
58
62
  }
59
63
 
60
- return validator;
64
+ return nextValidator;
61
65
  };
66
+ };
62
67
 
63
68
  const preventCast = (validator) => validator.transform((val, originalVal) => originalVal);
64
69
 
@@ -22,7 +22,7 @@ const createMiddleware = ({ sendEvent }) => {
22
22
  sendEvent('didReceiveRequest', { url: ctx.request.url });
23
23
 
24
24
  // Increase counter.
25
- _state.counter++;
25
+ _state.counter += 1;
26
26
  }
27
27
  }
28
28
 
@@ -45,12 +45,12 @@ module.exports = (strapi) => {
45
45
  environment: strapi.config.environment,
46
46
  os: os.type(),
47
47
  osPlatform: os.platform(),
48
+ osArch: os.arch(),
48
49
  osRelease: os.release(),
49
- nodeVersion: process.version,
50
+ nodeVersion: process.versions.node,
50
51
  docker: process.env.DOCKER || isDocker(),
51
52
  isCI: ciEnv.isCI,
52
53
  version: strapi.config.get('info.strapi'),
53
- strapiVersion: strapi.config.get('info.strapi'),
54
54
  projectType: isEE ? 'Enterprise' : 'Community',
55
55
  useTypescriptOnServer: isUsingTypeScriptSync(serverRootPath),
56
56
  useTypescriptOnAdmin: isUsingTypeScriptSync(adminRootPath),
@@ -50,7 +50,7 @@ const registerAdminRoutes = (strapi) => {
50
50
  * @param {import('../../').Strapi} strapi
51
51
  */
52
52
  const registerPluginRoutes = (strapi) => {
53
- for (const pluginName in strapi.plugins) {
53
+ for (const pluginName of Object.keys(strapi.plugins)) {
54
54
  const plugin = strapi.plugins[pluginName];
55
55
 
56
56
  const generateRouteScope = createRouteScopeGenerator(`plugin::${pluginName}`);
@@ -86,7 +86,7 @@ const registerPluginRoutes = (strapi) => {
86
86
  * @param {import('../../').Strapi} strapi
87
87
  */
88
88
  const registerAPIRoutes = (strapi) => {
89
- for (const apiName in strapi.api) {
89
+ for (const apiName of Object.keys(strapi.api)) {
90
90
  const api = strapi.api[apiName];
91
91
 
92
92
  const generateRouteScope = createRouteScopeGenerator(`api::${apiName}`);
@@ -27,7 +27,7 @@ module.exports = async (uid, entity, files) => {
27
27
  let tmpModel = modelDef;
28
28
  let modelUID = uid;
29
29
 
30
- for (let i = 0; i < path.length; i++) {
30
+ for (let i = 0; i < path.length; i += 1) {
31
31
  if (!tmpModel) return {};
32
32
  const part = path[i];
33
33
  const attr = tmpModel.attributes[part];
@@ -26,7 +26,7 @@ module.exports = class WorkerQueue {
26
26
  enqueue(payload) {
27
27
  debug('Enqueue event in worker queue');
28
28
  if (this.running < this.concurrency) {
29
- this.running++;
29
+ this.running += 1;
30
30
  this.execute(payload);
31
31
  } else {
32
32
  this.queue.unshift(payload);
@@ -40,7 +40,7 @@ module.exports = class WorkerQueue {
40
40
  if (payload) {
41
41
  this.execute(payload);
42
42
  } else {
43
- this.running--;
43
+ this.running -= 1;
44
44
  }
45
45
  }
46
46
 
@@ -24,6 +24,11 @@ export interface Strapi {
24
24
  */
25
25
  readonly auth: any;
26
26
 
27
+ /**
28
+ * Getter for the Strapi content API container
29
+ */
30
+ readonly contentAPI: any;
31
+
27
32
  /**
28
33
  * Getter for the Strapi sanitizers container
29
34
  */
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  module.exports = (path) => {
4
- if (typeof path !== 'string') throw new Error('admin.url must be a string');
5
- if (path === '' || path === '/') return '/';
4
+ let tmpPath = path;
5
+ if (typeof tmpPath !== 'string') throw new Error('admin.url must be a string');
6
+ if (tmpPath === '' || tmpPath === '/') return '/';
6
7
 
7
- if (path[0] != '/') path = `/${path}`;
8
- if (path[path.length - 1] != '/') path += '/';
9
- return path;
8
+ if (tmpPath[0] !== '/') tmpPath = `/${tmpPath}`;
9
+ if (tmpPath[tmpPath.length - 1] !== '/') tmpPath += '/';
10
+ return tmpPath;
10
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/strapi",
3
- "version": "4.4.0-alpha.0",
3
+ "version": "4.4.0-beta.1",
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",
@@ -80,17 +80,18 @@
80
80
  "dependencies": {
81
81
  "@koa/cors": "3.4.1",
82
82
  "@koa/router": "10.1.1",
83
- "@strapi/admin": "4.4.0-alpha.0",
84
- "@strapi/database": "4.4.0-alpha.0",
85
- "@strapi/generate-new": "4.4.0-alpha.0",
86
- "@strapi/generators": "4.4.0-alpha.0",
87
- "@strapi/logger": "4.4.0-alpha.0",
88
- "@strapi/plugin-content-manager": "4.4.0-alpha.0",
89
- "@strapi/plugin-content-type-builder": "4.4.0-alpha.0",
90
- "@strapi/plugin-email": "4.4.0-alpha.0",
91
- "@strapi/plugin-upload": "4.4.0-alpha.0",
92
- "@strapi/typescript-utils": "4.4.0-alpha.0",
93
- "@strapi/utils": "4.4.0-alpha.0",
83
+ "@strapi/admin": "4.4.0-beta.1",
84
+ "@strapi/database": "4.4.0-beta.1",
85
+ "@strapi/generate-new": "4.4.0-beta.1",
86
+ "@strapi/generators": "4.4.0-beta.1",
87
+ "@strapi/logger": "4.4.0-beta.1",
88
+ "@strapi/permissions": "4.4.0-beta.1",
89
+ "@strapi/plugin-content-manager": "4.4.0-beta.1",
90
+ "@strapi/plugin-content-type-builder": "4.4.0-beta.1",
91
+ "@strapi/plugin-email": "4.4.0-beta.1",
92
+ "@strapi/plugin-upload": "4.4.0-beta.1",
93
+ "@strapi/typescript-utils": "4.4.0-beta.1",
94
+ "@strapi/utils": "4.4.0-beta.1",
94
95
  "bcryptjs": "2.4.3",
95
96
  "boxen": "5.1.2",
96
97
  "chalk": "4.1.2",
@@ -136,8 +137,8 @@
136
137
  "typescript": "4.6.2"
137
138
  },
138
139
  "engines": {
139
- "node": ">=14.19.1 <=16.x.x",
140
+ "node": ">=14.19.1 <=18.x.x",
140
141
  "npm": ">=6.0.0"
141
142
  },
142
- "gitHead": "fc78298ae4f9b247d636beda568734d5f8ed7b3e"
143
+ "gitHead": "cae16f7f259fa4473a55e8fea57839cda98f34ae"
143
144
  }
@@ -1,54 +0,0 @@
1
- 'use strict';
2
-
3
- const { has } = require('lodash/fp');
4
- const validators = require('../../services/entity-validator/validators');
5
-
6
- const customFieldsRegistry = strapi => {
7
- const customFields = {};
8
-
9
- return {
10
- getAll() {
11
- return customFields;
12
- },
13
- get(customField) {
14
- const registeredCustomField = customFields[customField];
15
- if (!registeredCustomField) {
16
- throw new Error(`Could not find Custom Field: ${customField}`);
17
- }
18
-
19
- return registeredCustomField;
20
- },
21
- add(customField) {
22
- const customFieldList = Array.isArray(customField) ? customField : [customField];
23
-
24
- for (const cf of customFieldList) {
25
- if (!has('name', cf) || !has('type', cf)) {
26
- throw new Error(`Custom fields require a 'name' and 'type' key`);
27
- }
28
-
29
- const { name, plugin, type } = cf;
30
- if (!has(type, validators)) {
31
- throw new Error(
32
- `Custom field type: '${type}' is not a valid Strapi type or it can't be used with a Custom Field`
33
- );
34
- }
35
-
36
- const isValidObjectKey = /^(?![0-9])[a-zA-Z0-9$_-]+$/g;
37
- if (!isValidObjectKey.test(name)) {
38
- throw new Error(`Custom field name: '${name}' is not a valid object key`);
39
- }
40
-
41
- // When no plugin is specified, or it isn't found in Strapi, default to global
42
- const uid = strapi.plugin(plugin) ? `plugin::${plugin}.${name}` : `global::${name}`;
43
-
44
- if (has(uid, customFields)) {
45
- throw new Error(`Custom field: '${uid}' has already been registered`);
46
- }
47
-
48
- customFields[uid] = cf;
49
- }
50
- },
51
- };
52
- };
53
-
54
- module.exports = customFieldsRegistry;
@@ -1,11 +0,0 @@
1
- 'use strict';
2
-
3
- const createCustomFields = strapi => {
4
- return {
5
- register(customField) {
6
- strapi.container.get('custom-fields').add(customField);
7
- },
8
- };
9
- };
10
-
11
- module.exports = createCustomFields;
@@ -1,22 +0,0 @@
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;