@strapi/strapi 4.6.0-beta.0 → 4.6.0-beta.2
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/README.md +8 -4
- package/bin/strapi.js +139 -1
- package/lib/Strapi.js +8 -5
- package/lib/commands/opt-in-telemetry.js +2 -2
- package/lib/commands/opt-out-telemetry.js +2 -2
- package/lib/commands/transfer/export.js +158 -0
- package/lib/commands/transfer/import.js +154 -0
- package/lib/commands/transfer/transfer.js +127 -0
- package/lib/commands/transfer/utils.js +132 -0
- package/lib/commands/utils/commander.js +136 -0
- package/lib/commands/utils/helpers.js +108 -0
- package/lib/core-api/service/index.d.ts +1 -1
- package/lib/core-api/service/single-type.js +14 -1
- package/lib/services/entity-service/components.js +10 -4
- package/lib/services/entity-service/index.js +12 -2
- package/lib/services/errors.js +5 -1
- package/lib/services/event-hub.js +70 -8
- package/lib/services/metrics/admin-user-hash.js +21 -0
- package/lib/services/metrics/index.js +6 -4
- package/lib/services/metrics/middleware.js +1 -1
- package/lib/services/metrics/sender.js +17 -9
- package/lib/types/core/attributes/relation.d.ts +9 -12
- package/lib/types/core/schemas/index.d.ts +6 -1
- package/lib/types/core/strapi/index.d.ts +15 -4
- package/lib/types/factories.d.ts +3 -3
- package/lib/utils/ee.js +1 -1
- package/lib/utils/success.js +1 -1
- package/package.json +16 -15
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const Table = require('cli-table3');
|
|
5
|
+
const { Option } = require('commander');
|
|
6
|
+
const { TransferGroupPresets } = require('@strapi/data-transfer/lib/engine');
|
|
7
|
+
const { readableBytes, exitWith } = require('../utils/helpers');
|
|
8
|
+
const strapi = require('../../index');
|
|
9
|
+
const { getParseListWithChoices } = require('../utils/commander');
|
|
10
|
+
|
|
11
|
+
const pad = (n) => {
|
|
12
|
+
return (n < 10 ? '0' : '') + String(n);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const yyyymmddHHMMSS = () => {
|
|
16
|
+
const date = new Date();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
date.getFullYear() +
|
|
20
|
+
pad(date.getMonth() + 1) +
|
|
21
|
+
pad(date.getDate()) +
|
|
22
|
+
pad(date.getHours()) +
|
|
23
|
+
pad(date.getMinutes()) +
|
|
24
|
+
pad(date.getSeconds())
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getDefaultExportName = () => {
|
|
29
|
+
return `export_${yyyymmddHHMMSS()}`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const buildTransferTable = (resultData) => {
|
|
33
|
+
// Build pretty table
|
|
34
|
+
const table = new Table({
|
|
35
|
+
head: ['Type', 'Count', 'Size'].map((text) => chalk.bold.blue(text)),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let totalBytes = 0;
|
|
39
|
+
let totalItems = 0;
|
|
40
|
+
Object.keys(resultData).forEach((key) => {
|
|
41
|
+
const item = resultData[key];
|
|
42
|
+
|
|
43
|
+
table.push([
|
|
44
|
+
{ hAlign: 'left', content: chalk.bold(key) },
|
|
45
|
+
{ hAlign: 'right', content: item.count },
|
|
46
|
+
{ hAlign: 'right', content: `${readableBytes(item.bytes, 1, 11)} ` },
|
|
47
|
+
]);
|
|
48
|
+
totalBytes += item.bytes;
|
|
49
|
+
totalItems += item.count;
|
|
50
|
+
|
|
51
|
+
if (item.aggregates) {
|
|
52
|
+
Object.keys(item.aggregates)
|
|
53
|
+
.sort()
|
|
54
|
+
.forEach((subkey) => {
|
|
55
|
+
const subitem = item.aggregates[subkey];
|
|
56
|
+
|
|
57
|
+
table.push([
|
|
58
|
+
{ hAlign: 'left', content: `-- ${chalk.bold.grey(subkey)}` },
|
|
59
|
+
{ hAlign: 'right', content: chalk.grey(subitem.count) },
|
|
60
|
+
{ hAlign: 'right', content: chalk.grey(`(${readableBytes(subitem.bytes, 1, 11)})`) },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
table.push([
|
|
66
|
+
{ hAlign: 'left', content: chalk.bold.green('Total') },
|
|
67
|
+
{ hAlign: 'right', content: chalk.bold.green(totalItems) },
|
|
68
|
+
{ hAlign: 'right', content: `${chalk.bold.green(readableBytes(totalBytes, 1, 11))} ` },
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
return table;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DEFAULT_IGNORED_CONTENT_TYPES = [
|
|
75
|
+
'admin::permission',
|
|
76
|
+
'admin::user',
|
|
77
|
+
'admin::role',
|
|
78
|
+
'admin::api-token',
|
|
79
|
+
'admin::api-token-permission',
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const createStrapiInstance = async (logLevel = 'error') => {
|
|
83
|
+
const appContext = await strapi.compile();
|
|
84
|
+
const app = strapi(appContext);
|
|
85
|
+
|
|
86
|
+
app.log.level = logLevel;
|
|
87
|
+
|
|
88
|
+
return app.load();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const transferDataTypes = Object.keys(TransferGroupPresets);
|
|
92
|
+
|
|
93
|
+
const excludeOption = new Option(
|
|
94
|
+
'--exclude <comma-separated data types>',
|
|
95
|
+
`Exclude this data. Options used here override --only. Available types: ${transferDataTypes.join(
|
|
96
|
+
','
|
|
97
|
+
)}`
|
|
98
|
+
).argParser(getParseListWithChoices(transferDataTypes, 'Invalid options for "exclude"'));
|
|
99
|
+
|
|
100
|
+
const onlyOption = new Option(
|
|
101
|
+
'--only <command-separated data types>',
|
|
102
|
+
`Include only this data (plus schemas). Available types: ${transferDataTypes.join(',')}`
|
|
103
|
+
).argParser(getParseListWithChoices(transferDataTypes, 'Invalid options for "only"'));
|
|
104
|
+
|
|
105
|
+
const validateExcludeOnly = (command) => {
|
|
106
|
+
const { exclude, only } = command.opts();
|
|
107
|
+
if (!only || !exclude) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const choicesInBoth = only.filter((n) => {
|
|
112
|
+
return exclude.indexOf(n) !== -1;
|
|
113
|
+
});
|
|
114
|
+
if (choicesInBoth.length > 0) {
|
|
115
|
+
exitWith(
|
|
116
|
+
1,
|
|
117
|
+
`Data types may not be used in both "exclude" and "only" in the same command. Found in both: ${choicesInBoth.join(
|
|
118
|
+
','
|
|
119
|
+
)}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
buildTransferTable,
|
|
126
|
+
getDefaultExportName,
|
|
127
|
+
DEFAULT_IGNORED_CONTENT_TYPES,
|
|
128
|
+
createStrapiInstance,
|
|
129
|
+
excludeOption,
|
|
130
|
+
onlyOption,
|
|
131
|
+
validateExcludeOnly,
|
|
132
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This file includes hooks to use for commander.hook and argParsers for commander.argParser
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const { InvalidOptionArgumentError, Option } = require('commander');
|
|
9
|
+
const { bold, green, cyan } = require('chalk');
|
|
10
|
+
const { exitWith } = require('./helpers');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* argParser: Parse a comma-delimited string as an array
|
|
14
|
+
*/
|
|
15
|
+
const parseList = (value) => {
|
|
16
|
+
let list;
|
|
17
|
+
try {
|
|
18
|
+
list = value.split(',').map((item) => item.trim()); // trim shouldn't be necessary but might help catch unexpected whitespace characters
|
|
19
|
+
} catch (e) {
|
|
20
|
+
exitWith(1, `Unrecognized input: ${value}`);
|
|
21
|
+
}
|
|
22
|
+
return list;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns an argParser that returns a list
|
|
27
|
+
*/
|
|
28
|
+
const getParseListWithChoices = (choices, errorMessage = 'Invalid options:') => {
|
|
29
|
+
return (value) => {
|
|
30
|
+
const list = parseList(value);
|
|
31
|
+
const invalid = list.filter((item) => {
|
|
32
|
+
return !choices.includes(item);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (invalid.length > 0) {
|
|
36
|
+
exitWith(1, `${errorMessage}: ${invalid.join(',')}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return list;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* argParser: Parse a string as a URL object
|
|
45
|
+
*/
|
|
46
|
+
const parseURL = (value) => {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(value);
|
|
49
|
+
if (!url.host) {
|
|
50
|
+
throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return url;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
throw new InvalidOptionArgumentError(`Could not parse url ${value}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* hook: if encrypt==true and key not provided, prompt for it
|
|
61
|
+
*/
|
|
62
|
+
const promptEncryptionKey = async (thisCommand) => {
|
|
63
|
+
const opts = thisCommand.opts();
|
|
64
|
+
|
|
65
|
+
if (!opts.encrypt && opts.key) {
|
|
66
|
+
return exitWith(1, 'Key may not be present unless encryption is used');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// if encrypt==true but we have no key, prompt for it
|
|
70
|
+
if (opts.encrypt && !(opts.key && opts.key.length > 0)) {
|
|
71
|
+
try {
|
|
72
|
+
const answers = await inquirer.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: 'password',
|
|
75
|
+
message: 'Please enter an encryption key',
|
|
76
|
+
name: 'key',
|
|
77
|
+
validate(key) {
|
|
78
|
+
if (key.length > 0) return true;
|
|
79
|
+
|
|
80
|
+
return 'Key must be present when using the encrypt option';
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
opts.key = answers.key;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return exitWith(1, 'Failed to get encryption key');
|
|
87
|
+
}
|
|
88
|
+
if (!opts.key) {
|
|
89
|
+
return exitWith(1, 'Failed to get encryption key');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* hook: require a confirmation message to be accepted unless forceOption (-f,--force) is used
|
|
96
|
+
*
|
|
97
|
+
* @param {string} message The message to confirm with user
|
|
98
|
+
* @param {object} options Additional options
|
|
99
|
+
*/
|
|
100
|
+
const confirmMessage = (message) => {
|
|
101
|
+
return async (command) => {
|
|
102
|
+
// if we have a force option, assume yes
|
|
103
|
+
const opts = command.opts();
|
|
104
|
+
if (opts?.force === true) {
|
|
105
|
+
// attempt to mimic the inquirer prompt exactly
|
|
106
|
+
console.log(`${green('?')} ${bold(message)} ${cyan('Yes')}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const answers = await inquirer.prompt([
|
|
111
|
+
{
|
|
112
|
+
type: 'confirm',
|
|
113
|
+
message,
|
|
114
|
+
name: `confirm`,
|
|
115
|
+
default: false,
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
if (!answers.confirm) {
|
|
119
|
+
exitWith(0);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const forceOption = new Option(
|
|
125
|
+
'-f, --force',
|
|
126
|
+
`Automatically answer "yes" to all prompts, including potentially destructive requests, and run non-interactively.`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
getParseListWithChoices,
|
|
131
|
+
parseList,
|
|
132
|
+
parseURL,
|
|
133
|
+
promptEncryptionKey,
|
|
134
|
+
confirmMessage,
|
|
135
|
+
forceOption,
|
|
136
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper functions for the Strapi CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { isString, isArray } = require('lodash/fp');
|
|
9
|
+
|
|
10
|
+
const bytesPerKb = 1024;
|
|
11
|
+
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert bytes to a human readable formatted string, for example "1024" becomes "1KB"
|
|
15
|
+
*
|
|
16
|
+
* @param {number} bytes The bytes to be converted
|
|
17
|
+
* @param {number} decimals How many decimals to include in the final number
|
|
18
|
+
* @param {number} padStart Pad the string with space at the beginning so that it has at least this many characters
|
|
19
|
+
*/
|
|
20
|
+
const readableBytes = (bytes, decimals = 1, padStart = 0) => {
|
|
21
|
+
if (!bytes) {
|
|
22
|
+
return '0';
|
|
23
|
+
}
|
|
24
|
+
const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
|
|
25
|
+
const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
|
|
26
|
+
2
|
|
27
|
+
)}`;
|
|
28
|
+
|
|
29
|
+
return result.padStart(padStart);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
* Display message(s) to console and then call process.exit with code.
|
|
35
|
+
* If code is zero, console.log and green text is used for messages, otherwise console.error and red text.
|
|
36
|
+
*
|
|
37
|
+
* @param {number} code Code to exit process with
|
|
38
|
+
* @param {string | Array} message Message(s) to display before exiting
|
|
39
|
+
*/
|
|
40
|
+
const exitWith = (code, message = undefined) => {
|
|
41
|
+
const logger = (message) => {
|
|
42
|
+
if (code === 0) {
|
|
43
|
+
console.log(chalk.green(message));
|
|
44
|
+
} else {
|
|
45
|
+
console.error(chalk.red(message));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (isString(message)) {
|
|
50
|
+
logger(message);
|
|
51
|
+
} else if (isArray(message)) {
|
|
52
|
+
message.forEach((msg) => logger(msg));
|
|
53
|
+
}
|
|
54
|
+
process.exit(code);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* assert that a URL object has a protocol value
|
|
59
|
+
*
|
|
60
|
+
* @param {URL} url
|
|
61
|
+
* @param {string[]|string|undefined} [protocol]
|
|
62
|
+
*/
|
|
63
|
+
const assertUrlHasProtocol = (url, protocol = undefined) => {
|
|
64
|
+
if (!url.protocol) {
|
|
65
|
+
exitWith(1, `${url.toString()} does not have a protocol`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// if just checking for the existence of a protocol, return
|
|
69
|
+
if (!protocol) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isString(protocol)) {
|
|
74
|
+
if (protocol !== url.protocol) {
|
|
75
|
+
exitWith(1, `${url.toString()} must have the protocol ${protocol}`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// assume an array
|
|
81
|
+
if (!protocol.some((protocol) => url.protocol === protocol)) {
|
|
82
|
+
return exitWith(
|
|
83
|
+
1,
|
|
84
|
+
`${url.toString()} must have one of the following protocols: ${protocol.join(',')}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Passes commander options to conditionCallback(). If it returns true, call isMetCallback otherwise call isNotMetCallback
|
|
91
|
+
*/
|
|
92
|
+
const ifOptions = (conditionCallback, isMetCallback = () => {}, isNotMetCallback = () => {}) => {
|
|
93
|
+
return async (command) => {
|
|
94
|
+
const opts = command.opts();
|
|
95
|
+
if (await conditionCallback(opts)) {
|
|
96
|
+
await isMetCallback(command);
|
|
97
|
+
} else {
|
|
98
|
+
await isNotMetCallback(command);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
exitWith,
|
|
105
|
+
assertUrlHasProtocol,
|
|
106
|
+
ifOptions,
|
|
107
|
+
readableBytes,
|
|
108
|
+
};
|
|
@@ -21,5 +21,5 @@ export interface CollectionTypeService extends BaseService {
|
|
|
21
21
|
export type Service = SingleTypeService | CollectionTypeService;
|
|
22
22
|
|
|
23
23
|
export type GenericService = Partial<Service> & {
|
|
24
|
-
[method: string | number | symbol]:
|
|
24
|
+
[method: string | number | symbol]: (...args: any) => any;
|
|
25
25
|
};
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { propOr } = require('lodash/fp');
|
|
4
|
+
|
|
3
5
|
const { ValidationError } = require('@strapi/utils').errors;
|
|
6
|
+
const {
|
|
7
|
+
hasDraftAndPublish,
|
|
8
|
+
constants: { PUBLISHED_AT_ATTRIBUTE },
|
|
9
|
+
} = require('@strapi/utils').contentTypes;
|
|
10
|
+
|
|
11
|
+
const setPublishedAt = (data) => {
|
|
12
|
+
data[PUBLISHED_AT_ATTRIBUTE] = propOr(new Date(), PUBLISHED_AT_ATTRIBUTE, data);
|
|
13
|
+
};
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
16
|
* Returns a single type service to handle default core-api actions
|
|
@@ -27,7 +37,7 @@ const createSingleTypeService = ({ contentType }) => {
|
|
|
27
37
|
* @return {Promise}
|
|
28
38
|
*/
|
|
29
39
|
async createOrUpdate({ data, ...params } = {}) {
|
|
30
|
-
const entity = await this.find(params);
|
|
40
|
+
const entity = await this.find({ ...params, publicationState: 'preview' });
|
|
31
41
|
|
|
32
42
|
if (!entity) {
|
|
33
43
|
const count = await strapi.query(uid).count();
|
|
@@ -35,6 +45,9 @@ const createSingleTypeService = ({ contentType }) => {
|
|
|
35
45
|
throw new ValidationError('singleType.alreadyExists');
|
|
36
46
|
}
|
|
37
47
|
|
|
48
|
+
if (hasDraftAndPublish(contentType)) {
|
|
49
|
+
setPublishedAt(data);
|
|
50
|
+
}
|
|
38
51
|
return strapi.entityService.create(uid, { ...params, data });
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
|
-
const { has, prop, omit, toString } = require('lodash/fp');
|
|
4
|
+
const { has, prop, omit, toString, pipe, assign } = require('lodash/fp');
|
|
5
5
|
|
|
6
6
|
const { contentTypes: contentTypesUtils } = require('@strapi/utils');
|
|
7
7
|
const { ApplicationError } = require('@strapi/utils').errors;
|
|
@@ -311,10 +311,16 @@ const createComponent = async (uid, data) => {
|
|
|
311
311
|
const model = strapi.getModel(uid);
|
|
312
312
|
|
|
313
313
|
const componentData = await createComponents(uid, data);
|
|
314
|
+
const transform = pipe(
|
|
315
|
+
// Make sure we don't save the component with a pre-defined ID
|
|
316
|
+
omit('id'),
|
|
317
|
+
// Remove the component data from the original data object ...
|
|
318
|
+
(payload) => omitComponentData(model, payload),
|
|
319
|
+
// ... and assign the newly created component instead
|
|
320
|
+
assign(componentData)
|
|
321
|
+
);
|
|
314
322
|
|
|
315
|
-
return strapi.query(uid).create({
|
|
316
|
-
data: Object.assign(omitComponentData(model, data), componentData),
|
|
317
|
-
});
|
|
323
|
+
return strapi.query(uid).create({ data: transform(data) });
|
|
318
324
|
};
|
|
319
325
|
|
|
320
326
|
// components can have nested compos so this must be recursive
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const delegate = require('delegates');
|
|
5
|
-
const { InvalidTimeError, InvalidDateError, InvalidDateTimeError } =
|
|
5
|
+
const { InvalidTimeError, InvalidDateError, InvalidDateTimeError, InvalidRelationError } =
|
|
6
6
|
require('@strapi/database').errors;
|
|
7
7
|
const {
|
|
8
8
|
webhook: webhookUtils,
|
|
@@ -34,7 +34,12 @@ const transformLoadParamsToQuery = (uid, field, params = {}, pagination = {}) =>
|
|
|
34
34
|
// TODO: those should be strapi events used by the webhooks not the other way arround
|
|
35
35
|
const { ENTRY_CREATE, ENTRY_UPDATE, ENTRY_DELETE } = webhookUtils.webhookEvents;
|
|
36
36
|
|
|
37
|
-
const databaseErrorsToTransform = [
|
|
37
|
+
const databaseErrorsToTransform = [
|
|
38
|
+
InvalidTimeError,
|
|
39
|
+
InvalidDateTimeError,
|
|
40
|
+
InvalidDateError,
|
|
41
|
+
InvalidRelationError,
|
|
42
|
+
];
|
|
38
43
|
|
|
39
44
|
const creationPipeline = (data, context) => {
|
|
40
45
|
return applyTransforms(data, context);
|
|
@@ -55,6 +60,11 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
|
|
|
55
60
|
},
|
|
56
61
|
|
|
57
62
|
async emitEvent(uid, event, entity) {
|
|
63
|
+
// Ignore audit log events to prevent infinite loops
|
|
64
|
+
if (uid === 'admin::audit-log') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
const model = strapi.getModel(uid);
|
|
59
69
|
const sanitizedEntity = await sanitize.sanitizers.defaultSanitizeOutput(model, entity);
|
|
60
70
|
|
package/lib/services/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const createError = require('http-errors');
|
|
4
|
-
const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError } =
|
|
4
|
+
const { NotFoundError, UnauthorizedError, ForbiddenError, PayloadTooLargeError, RateLimitError } =
|
|
5
5
|
require('@strapi/utils').errors;
|
|
6
6
|
|
|
7
7
|
const mapErrorsAndStatus = [
|
|
@@ -21,6 +21,10 @@ const mapErrorsAndStatus = [
|
|
|
21
21
|
classError: PayloadTooLargeError,
|
|
22
22
|
status: 413,
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
classError: RateLimitError,
|
|
26
|
+
status: 429,
|
|
27
|
+
},
|
|
24
28
|
];
|
|
25
29
|
|
|
26
30
|
const formatApplicationError = (error) => {
|
|
@@ -1,16 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The event hub is Strapi's event control center.
|
|
3
5
|
*/
|
|
6
|
+
module.exports = function createEventHub() {
|
|
7
|
+
const listeners = new Map();
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
// Default subscriber to easily add listeners with the on() method
|
|
10
|
+
const defaultSubscriber = async (eventName, ...args) => {
|
|
11
|
+
if (listeners.has(eventName)) {
|
|
12
|
+
for (const listener of listeners.get(eventName)) {
|
|
13
|
+
await listener(...args);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
6
17
|
|
|
7
|
-
|
|
18
|
+
// Store of subscribers that will be called when an event is emitted
|
|
19
|
+
const subscribers = [defaultSubscriber];
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
const eventHub = {
|
|
22
|
+
async emit(eventName, ...args) {
|
|
23
|
+
for (const subscriber of subscribers) {
|
|
24
|
+
await subscriber(eventName, ...args);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
10
27
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
subscribe(subscriber) {
|
|
29
|
+
subscribers.push(subscriber);
|
|
30
|
+
|
|
31
|
+
// Return a function to remove the subscriber
|
|
32
|
+
return () => {
|
|
33
|
+
eventHub.unsubscribe(subscriber);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
unsubscribe(subscriber) {
|
|
38
|
+
subscribers.splice(subscribers.indexOf(subscriber), 1);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
on(eventName, listener) {
|
|
42
|
+
if (!listeners.has(eventName)) {
|
|
43
|
+
listeners.set(eventName, [listener]);
|
|
44
|
+
} else {
|
|
45
|
+
listeners.get(eventName).push(listener);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Return a function to remove the listener
|
|
49
|
+
return () => {
|
|
50
|
+
eventHub.off(eventName, listener);
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
off(eventName, listener) {
|
|
55
|
+
listeners.get(eventName).splice(listeners.get(eventName).indexOf(listener), 1);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
once(eventName, listener) {
|
|
59
|
+
return eventHub.on(eventName, async (...args) => {
|
|
60
|
+
eventHub.off(eventName, listener);
|
|
61
|
+
return listener(...args);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
destroy() {
|
|
66
|
+
listeners.clear();
|
|
67
|
+
subscribers.length = 0;
|
|
68
|
+
return this;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...eventHub,
|
|
74
|
+
removeListener: eventHub.off,
|
|
75
|
+
removeAllListeners: eventHub.destroy,
|
|
76
|
+
addListener: eventHub.on,
|
|
77
|
+
};
|
|
16
78
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate an admin user hash
|
|
7
|
+
*
|
|
8
|
+
* @param {Strapi.Strapi} strapi
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
const generateAdminUserHash = (strapi) => {
|
|
12
|
+
const ctx = strapi?.requestContext?.get();
|
|
13
|
+
if (!ctx?.state?.user) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
return crypto.createHash('sha256').update(ctx.state.user.email).digest('hex');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
generateAdminUserHash,
|
|
21
|
+
};
|
|
@@ -55,10 +55,12 @@ const createTelemetryInstance = (strapi) => {
|
|
|
55
55
|
return sendEvent(
|
|
56
56
|
'didCheckLicense',
|
|
57
57
|
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
groupProperties: {
|
|
59
|
+
licenseInfo: {
|
|
60
|
+
...ee.licenseInfo,
|
|
61
|
+
projectHash: hashProject(strapi),
|
|
62
|
+
dependencyHash: hashDep(strapi),
|
|
63
|
+
},
|
|
62
64
|
},
|
|
63
65
|
},
|
|
64
66
|
{
|
|
@@ -19,7 +19,7 @@ const createMiddleware = ({ sendEvent }) => {
|
|
|
19
19
|
|
|
20
20
|
// Send max. 1000 events per day.
|
|
21
21
|
if (_state.counter < 1000) {
|
|
22
|
-
sendEvent('didReceiveRequest', { url: ctx.request.url });
|
|
22
|
+
sendEvent('didReceiveRequest', { eventProperties: { url: ctx.request.url } });
|
|
23
23
|
|
|
24
24
|
// Increase counter.
|
|
25
25
|
_state.counter += 1;
|