@strapi/strapi 4.5.1 → 4.6.0-alpha.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/bin/strapi.js +108 -1
- package/lib/commands/transfer/export.js +166 -0
- package/lib/commands/transfer/import.js +65 -0
- package/lib/commands/utils/commander.js +92 -0
- package/lib/commands/utils/index.js +20 -0
- package/lib/services/entity-validator/index.js +21 -6
- 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 +10 -4
- package/lib/types/factories.d.ts +2 -2
- package/package.json +16 -15
package/bin/strapi.js
CHANGED
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
const _ = require('lodash');
|
|
8
8
|
const resolveCwd = require('resolve-cwd');
|
|
9
9
|
const { yellow } = require('chalk');
|
|
10
|
-
const { Command } = require('commander');
|
|
10
|
+
const { Command, Option } = require('commander');
|
|
11
|
+
const inquirer = require('inquirer');
|
|
11
12
|
|
|
12
13
|
const program = new Command();
|
|
13
14
|
|
|
14
15
|
const packageJSON = require('../package.json');
|
|
16
|
+
const {
|
|
17
|
+
parseInputList,
|
|
18
|
+
parseInputBool,
|
|
19
|
+
promptEncryptionKey,
|
|
20
|
+
confirmKeyValue,
|
|
21
|
+
} = require('../lib/commands/utils/commander');
|
|
15
22
|
|
|
16
23
|
const checkCwdIsStrapiApp = (name) => {
|
|
17
24
|
const logErrorAndExit = () => {
|
|
@@ -60,6 +67,13 @@ const getLocalScript =
|
|
|
60
67
|
});
|
|
61
68
|
};
|
|
62
69
|
|
|
70
|
+
// option to exclude types of data for the export, import, and transfer commands
|
|
71
|
+
// TODO: validate these inputs. Hopefully here, but worst case it may require adding a hook on each command
|
|
72
|
+
const excludeOption = new Option(
|
|
73
|
+
'--exclude <data,to,exclude>',
|
|
74
|
+
'Comma-separated list of data to exclude (files [localMediaFiles, providerMediaFiles], content [entities, links], schema, configuration)' // ['webhooks', 'content', 'localmedia', 'providermedia', 'relations']
|
|
75
|
+
).argParser(parseInputList);
|
|
76
|
+
|
|
63
77
|
// Initial program setup
|
|
64
78
|
program.storeOptionsAsProperties(false).allowUnknownOption(true);
|
|
65
79
|
|
|
@@ -255,4 +269,97 @@ program
|
|
|
255
269
|
.option('-s, --silent', `Run the generation silently, without any output`, false)
|
|
256
270
|
.action(getLocalScript('ts/generate-types'));
|
|
257
271
|
|
|
272
|
+
// `$ strapi export`
|
|
273
|
+
program
|
|
274
|
+
.command('export')
|
|
275
|
+
.description('Export data from Strapi to file')
|
|
276
|
+
.addOption(
|
|
277
|
+
new Option(
|
|
278
|
+
'--encrypt <boolean>',
|
|
279
|
+
`Encrypt output file using the 'aes-128-ecb' algorithm. Prompts for key unless key option is used.`
|
|
280
|
+
)
|
|
281
|
+
.default(true)
|
|
282
|
+
.argParser(parseInputBool)
|
|
283
|
+
)
|
|
284
|
+
.addOption(
|
|
285
|
+
new Option('--compress <boolean>', 'Compress output file using gzip compression')
|
|
286
|
+
.default(true)
|
|
287
|
+
.argParser(parseInputBool)
|
|
288
|
+
)
|
|
289
|
+
.addOption(
|
|
290
|
+
new Option('--key <string>', 'Provide encryption key in command instead of using a prompt')
|
|
291
|
+
)
|
|
292
|
+
.addOption(
|
|
293
|
+
new Option('--max-size <max MB per file>', 'split final file when exceeding size in MB')
|
|
294
|
+
)
|
|
295
|
+
.addOption(
|
|
296
|
+
new Option(
|
|
297
|
+
'--max-size-jsonl <max MB per internal backup file>',
|
|
298
|
+
'split internal jsonl files when exceeding max size in MB'
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
.addOption(new Option('-f, --file <file>', 'name to use for exported file (without extensions)'))
|
|
302
|
+
.addOption(excludeOption)
|
|
303
|
+
.allowExcessArguments(false)
|
|
304
|
+
.hook('preAction', promptEncryptionKey)
|
|
305
|
+
.action(getLocalScript('transfer/export'));
|
|
306
|
+
|
|
307
|
+
// `$ strapi import`
|
|
308
|
+
program
|
|
309
|
+
.command('import')
|
|
310
|
+
.description('Import data from file to Strapi')
|
|
311
|
+
.addOption(
|
|
312
|
+
new Option('--conflictStrategy <conflictStrategy>', 'Which strategy to use for ID conflicts')
|
|
313
|
+
.choices(['restore', 'abort', 'keep', 'replace'])
|
|
314
|
+
.default('restore')
|
|
315
|
+
)
|
|
316
|
+
.addOption(excludeOption)
|
|
317
|
+
.addOption(
|
|
318
|
+
new Option(
|
|
319
|
+
'--schemaComparison <schemaComparison>',
|
|
320
|
+
'exact requires every field to match, strict requires Strapi version and content type schema fields do not break, subset requires source schema to exist in destination, bypass skips checks',
|
|
321
|
+
parseInputList
|
|
322
|
+
)
|
|
323
|
+
.choices(['exact', 'strict', 'subset', 'bypass'])
|
|
324
|
+
.default('exact')
|
|
325
|
+
)
|
|
326
|
+
.requiredOption(
|
|
327
|
+
'-f, --file <file>',
|
|
328
|
+
'path and filename to the Strapi export file you want to import'
|
|
329
|
+
)
|
|
330
|
+
.addOption(
|
|
331
|
+
new Option('--key <string>', 'Provide encryption key in command instead of using a prompt')
|
|
332
|
+
)
|
|
333
|
+
.allowExcessArguments(false)
|
|
334
|
+
.hook('preAction', async (thisCommand) => {
|
|
335
|
+
const opts = thisCommand.opts();
|
|
336
|
+
|
|
337
|
+
// check extension to guess if we should prompt for key
|
|
338
|
+
if (String(opts.file).endsWith('.enc')) {
|
|
339
|
+
if (!opts.key) {
|
|
340
|
+
const answers = await inquirer.prompt([
|
|
341
|
+
{
|
|
342
|
+
type: 'password',
|
|
343
|
+
message: 'Please enter your decryption key',
|
|
344
|
+
name: 'key',
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
if (!answers.key?.length) {
|
|
348
|
+
console.log('No key entered, aborting import.');
|
|
349
|
+
process.exit(0);
|
|
350
|
+
}
|
|
351
|
+
opts.key = answers.key;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
.hook(
|
|
356
|
+
'preAction',
|
|
357
|
+
confirmKeyValue(
|
|
358
|
+
'conflictStrategy',
|
|
359
|
+
'restore',
|
|
360
|
+
"Using strategy 'restore' will delete all data in your database. Are you sure you want to proceed?"
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
.action(getLocalScript('transfer/import'));
|
|
364
|
+
|
|
258
365
|
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createLocalFileDestinationProvider,
|
|
5
|
+
createLocalStrapiSourceProvider,
|
|
6
|
+
createTransferEngine,
|
|
7
|
+
// TODO: we need to solve this issue with typescript modules
|
|
8
|
+
// eslint-disable-next-line import/no-unresolved, node/no-missing-require
|
|
9
|
+
} = require('@strapi/data-transfer');
|
|
10
|
+
const _ = require('lodash/fp');
|
|
11
|
+
const Table = require('cli-table3');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
|
|
14
|
+
const chalk = require('chalk');
|
|
15
|
+
const strapi = require('../../index');
|
|
16
|
+
const { readableBytes } = require('../utils');
|
|
17
|
+
|
|
18
|
+
const pad = (n) => {
|
|
19
|
+
return (n < 10 ? '0' : '') + String(n);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const yyyymmddHHMMSS = () => {
|
|
23
|
+
const date = new Date();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
date.getFullYear() +
|
|
27
|
+
pad(date.getMonth() + 1) +
|
|
28
|
+
pad(date.getDate()) +
|
|
29
|
+
pad(date.getHours()) +
|
|
30
|
+
pad(date.getMinutes()) +
|
|
31
|
+
pad(date.getSeconds())
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getDefaultExportName = () => {
|
|
36
|
+
return `export_${yyyymmddHHMMSS()}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const logger = console;
|
|
40
|
+
|
|
41
|
+
const BYTES_IN_MB = 1024 * 1024;
|
|
42
|
+
|
|
43
|
+
module.exports = async (opts) => {
|
|
44
|
+
// validate inputs from Commander
|
|
45
|
+
if (!_.isObject(opts)) {
|
|
46
|
+
logger.error('Could not parse arguments');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const filename = opts.file;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* From local Strapi instance
|
|
53
|
+
*/
|
|
54
|
+
const sourceOptions = {
|
|
55
|
+
async getStrapi() {
|
|
56
|
+
return strapi(await strapi.compile()).load();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const source = createLocalStrapiSourceProvider(sourceOptions);
|
|
60
|
+
|
|
61
|
+
const file = _.isString(filename) && filename.length > 0 ? filename : getDefaultExportName();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* To a Strapi backup file
|
|
65
|
+
*/
|
|
66
|
+
// treat any unknown arguments as filenames
|
|
67
|
+
const destinationOptions = {
|
|
68
|
+
file: {
|
|
69
|
+
path: file,
|
|
70
|
+
maxSize: _.isFinite(opts.maxSize) ? Math.floor(opts.maxSize) * BYTES_IN_MB : undefined,
|
|
71
|
+
maxSizeJsonl: _.isFinite(opts.maxSizeJsonl)
|
|
72
|
+
? Math.floor(opts.maxSizeJsonl) * BYTES_IN_MB
|
|
73
|
+
: undefined,
|
|
74
|
+
},
|
|
75
|
+
encryption: {
|
|
76
|
+
enabled: opts.encrypt,
|
|
77
|
+
key: opts.key,
|
|
78
|
+
},
|
|
79
|
+
compression: {
|
|
80
|
+
enabled: opts.compress,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const destination = createLocalFileDestinationProvider(destinationOptions);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Configure and run the transfer engine
|
|
87
|
+
*/
|
|
88
|
+
const engineOptions = {
|
|
89
|
+
strategy: 'restore', // for an export to file, strategy will always be 'restore'
|
|
90
|
+
versionMatching: 'ignore', // for an export to file, versionMatching will always be skipped
|
|
91
|
+
exclude: opts.exclude,
|
|
92
|
+
};
|
|
93
|
+
const engine = createTransferEngine(source, destination, engineOptions);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
let resultData = [];
|
|
97
|
+
logger.log(`Starting export...`);
|
|
98
|
+
|
|
99
|
+
engine.progress.stream.on('start', ({ stage }) => {
|
|
100
|
+
logger.log(`Starting transfer of ${stage}...`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// engine.progress.stream..on('progress', ({ stage, data }) => {
|
|
104
|
+
// logger.log('progress');
|
|
105
|
+
// });
|
|
106
|
+
|
|
107
|
+
engine.progress.stream.on('complete', ({ stage, data }) => {
|
|
108
|
+
logger.log(`...${stage} complete`);
|
|
109
|
+
resultData = data;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const results = await engine.transfer();
|
|
113
|
+
|
|
114
|
+
// Build pretty table
|
|
115
|
+
const table = new Table({
|
|
116
|
+
head: ['Type', 'Count', 'Size'],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let totalBytes = 0;
|
|
120
|
+
let totalItems = 0;
|
|
121
|
+
Object.keys(resultData).forEach((key) => {
|
|
122
|
+
const item = resultData[key];
|
|
123
|
+
|
|
124
|
+
table.push([
|
|
125
|
+
{ hAlign: 'left', content: chalk.bold(key) },
|
|
126
|
+
{ hAlign: 'right', content: item.count },
|
|
127
|
+
{ hAlign: 'right', content: `${readableBytes(item.bytes, 1, 11)} ` },
|
|
128
|
+
]);
|
|
129
|
+
totalBytes += item.bytes;
|
|
130
|
+
totalItems += item.count;
|
|
131
|
+
|
|
132
|
+
if (item.aggregates) {
|
|
133
|
+
Object.keys(item.aggregates).forEach((subkey) => {
|
|
134
|
+
const subitem = item.aggregates[subkey];
|
|
135
|
+
|
|
136
|
+
table.push([
|
|
137
|
+
{ hAlign: 'left', content: `-- ${chalk.bold(subkey)}` },
|
|
138
|
+
{ hAlign: 'right', content: subitem.count },
|
|
139
|
+
{ hAlign: 'right', content: `(${chalk.grey(readableBytes(subitem.bytes, 1, 11))})` },
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
table.push([
|
|
145
|
+
{ hAlign: 'left', content: chalk.bold.green('Total') },
|
|
146
|
+
{ hAlign: 'right', content: chalk.bold.green(totalItems) },
|
|
147
|
+
{ hAlign: 'right', content: `${chalk.bold.green(readableBytes(totalBytes, 1, 11))} ` },
|
|
148
|
+
]);
|
|
149
|
+
logger.log(table.toString());
|
|
150
|
+
|
|
151
|
+
// TODO: once archiving is implemented, we need to check file extensions
|
|
152
|
+
if (!fs.pathExistsSync(results.destination.file.path)) {
|
|
153
|
+
logger.log(file);
|
|
154
|
+
throw new Error('Export file not created');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
logger.log(`
|
|
158
|
+
${chalk.bold('Export process has been completed successfully!')}
|
|
159
|
+
Export archive is in ${chalk.green(results.destination.file.path)}
|
|
160
|
+
`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
logger.error('Export process failed unexpectedly:', e.toString());
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createLocalFileSourceProvider,
|
|
5
|
+
createLocalStrapiDestinationProvider,
|
|
6
|
+
createTransferEngine,
|
|
7
|
+
// TODO: we need to solve this issue with typescript modules
|
|
8
|
+
// eslint-disable-next-line import/no-unresolved, node/no-missing-require
|
|
9
|
+
} = require('@strapi/data-transfer');
|
|
10
|
+
const { isObject } = require('lodash/fp');
|
|
11
|
+
const strapi = require('../../index');
|
|
12
|
+
|
|
13
|
+
const logger = console;
|
|
14
|
+
|
|
15
|
+
module.exports = async (opts) => {
|
|
16
|
+
// validate inputs from Commander
|
|
17
|
+
if (!isObject(opts)) {
|
|
18
|
+
logger.error('Could not parse arguments');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const filename = opts.file;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* From strapi backup file
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// treat any unknown arguments as filenames
|
|
28
|
+
const sourceOptions = {
|
|
29
|
+
backupFilePath: filename,
|
|
30
|
+
};
|
|
31
|
+
const source = createLocalFileSourceProvider(sourceOptions);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* To local Strapi instance
|
|
35
|
+
*/
|
|
36
|
+
const destinationOptions = {
|
|
37
|
+
async getStrapi() {
|
|
38
|
+
return strapi(await strapi.compile()).load();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const destination = createLocalStrapiDestinationProvider(destinationOptions);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Configure and run the transfer engine
|
|
45
|
+
*/
|
|
46
|
+
const engineOptions = {
|
|
47
|
+
strategy: opts.conflictStrategy,
|
|
48
|
+
versionMatching: opts.schemaComparison,
|
|
49
|
+
exclude: opts.exclude,
|
|
50
|
+
};
|
|
51
|
+
const engine = createTransferEngine(source, destination, engineOptions);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
logger.log('Importing data...');
|
|
55
|
+
const result = await engine.transfer();
|
|
56
|
+
logger.log('Import process has been completed successfully!');
|
|
57
|
+
|
|
58
|
+
// TODO: this won't dump the entire results, we will print a pretty summary
|
|
59
|
+
logger.log('Results:', result);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
logger.log(`Import process failed unexpectedly: ${e.message}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parseType } = require('@strapi/utils/lib');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* argsParser: Parse a string argument from the command line as a boolean
|
|
8
|
+
*/
|
|
9
|
+
const parseInputBool = (arg) => {
|
|
10
|
+
try {
|
|
11
|
+
return parseType({ type: 'boolean', value: arg });
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error(e.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* argsParser: Parse a comma-delimited string as an array
|
|
20
|
+
*/
|
|
21
|
+
const parseInputList = (value) => {
|
|
22
|
+
return value.split(',');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* hook: if encrpyt=true and key not provided, prompt for it
|
|
27
|
+
*/
|
|
28
|
+
const promptEncryptionKey = async (thisCommand) => {
|
|
29
|
+
const opts = thisCommand.opts();
|
|
30
|
+
|
|
31
|
+
if (!opts.encrypt && opts.key) {
|
|
32
|
+
console.error('Key may not be present unless encryption is used');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// if encrypt is set but we have no key, prompt for it
|
|
37
|
+
if (opts.encrypt && !(opts.key && opts.key.length > 0)) {
|
|
38
|
+
try {
|
|
39
|
+
const answers = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: 'password',
|
|
42
|
+
message: 'Please enter an encryption key',
|
|
43
|
+
name: 'key',
|
|
44
|
+
validate(key) {
|
|
45
|
+
if (key.length > 0) return true;
|
|
46
|
+
|
|
47
|
+
return 'Key must be present when using the encrypt option';
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
opts.key = answers.key;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error('Failed to get encryption key');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (!opts.key) {
|
|
57
|
+
console.error('Failed to get encryption key');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* hook: confirm that key has a value with a provided message
|
|
65
|
+
*/
|
|
66
|
+
const confirmKeyValue = (key, value, message) => {
|
|
67
|
+
return async (thisCommand) => {
|
|
68
|
+
const opts = thisCommand.opts();
|
|
69
|
+
|
|
70
|
+
if (!opts[key] || opts[key] !== value) {
|
|
71
|
+
console.error(`Could not confirm key ${key}, halting operation.`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const answers = await inquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: 'confirm',
|
|
77
|
+
message,
|
|
78
|
+
name: `confirm_${key}`,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
if (!answers[`confirm_${key}`]) {
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
parseInputList,
|
|
89
|
+
parseInputBool,
|
|
90
|
+
promptEncryptionKey,
|
|
91
|
+
confirmKeyValue,
|
|
92
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const bytesPerKb = 1024;
|
|
4
|
+
const sizes = ['B ', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
5
|
+
|
|
6
|
+
const readableBytes = (bytes, decimals = 1, padStart = 0) => {
|
|
7
|
+
if (!bytes) {
|
|
8
|
+
return '0';
|
|
9
|
+
}
|
|
10
|
+
const i = Math.floor(Math.log(bytes) / Math.log(bytesPerKb));
|
|
11
|
+
const result = `${parseFloat((bytes / bytesPerKb ** i).toFixed(decimals))} ${sizes[i].padStart(
|
|
12
|
+
2
|
|
13
|
+
)}`;
|
|
14
|
+
|
|
15
|
+
return result.padStart(padStart);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
readableBytes,
|
|
20
|
+
};
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
const { uniqBy, castArray, isNil } = require('lodash');
|
|
9
|
-
const { has, assoc, prop, isObject, isEmpty
|
|
8
|
+
const { uniqBy, castArray, isNil, isArray, mergeWith } = require('lodash');
|
|
9
|
+
const { has, assoc, prop, isObject, isEmpty } = require('lodash/fp');
|
|
10
10
|
const strapiUtils = require('@strapi/utils');
|
|
11
11
|
const validators = require('./validators');
|
|
12
12
|
|
|
@@ -247,9 +247,14 @@ const createValidateEntity =
|
|
|
247
247
|
* @returns {Object}
|
|
248
248
|
*/
|
|
249
249
|
const buildRelationsStore = ({ uid, data }) => {
|
|
250
|
+
if (!uid) {
|
|
251
|
+
throw new ValidationError(`Cannot build relations store: "uid" is undefined`);
|
|
252
|
+
}
|
|
253
|
+
|
|
250
254
|
if (isEmpty(data)) {
|
|
251
255
|
return {};
|
|
252
256
|
}
|
|
257
|
+
|
|
253
258
|
const currentModel = strapi.getModel(uid);
|
|
254
259
|
|
|
255
260
|
return Object.keys(currentModel.attributes).reduce((result, attributeName) => {
|
|
@@ -290,12 +295,17 @@ const buildRelationsStore = ({ uid, data }) => {
|
|
|
290
295
|
case 'component': {
|
|
291
296
|
return castArray(value).reduce(
|
|
292
297
|
(relationsStore, componentValue) =>
|
|
293
|
-
|
|
298
|
+
mergeWith(
|
|
294
299
|
relationsStore,
|
|
295
300
|
buildRelationsStore({
|
|
296
301
|
uid: attribute.component,
|
|
297
302
|
data: componentValue,
|
|
298
|
-
})
|
|
303
|
+
}),
|
|
304
|
+
(objValue, srcValue) => {
|
|
305
|
+
if (isArray(objValue)) {
|
|
306
|
+
return objValue.concat(srcValue);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
299
309
|
),
|
|
300
310
|
result
|
|
301
311
|
);
|
|
@@ -303,12 +313,17 @@ const buildRelationsStore = ({ uid, data }) => {
|
|
|
303
313
|
case 'dynamiczone': {
|
|
304
314
|
return value.reduce(
|
|
305
315
|
(relationsStore, dzValue) =>
|
|
306
|
-
|
|
316
|
+
mergeWith(
|
|
307
317
|
relationsStore,
|
|
308
318
|
buildRelationsStore({
|
|
309
319
|
uid: dzValue.__component,
|
|
310
320
|
data: dzValue,
|
|
311
|
-
})
|
|
321
|
+
}),
|
|
322
|
+
(objValue, srcValue) => {
|
|
323
|
+
if (isArray(objValue)) {
|
|
324
|
+
return objValue.concat(srcValue);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
312
327
|
),
|
|
313
328
|
result
|
|
314
329
|
);
|
|
@@ -3,7 +3,7 @@ import { Attribute, ConfigurableOption, PrivateOption } from './base';
|
|
|
3
3
|
import { GetAttributesByType, GetAttributesValues } from './utils';
|
|
4
4
|
|
|
5
5
|
export type BasicRelationsType = 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany';
|
|
6
|
-
export type PolymorphicRelationsType =
|
|
6
|
+
export type PolymorphicRelationsType = 'morphToOne' | 'morphToMany' | 'morphOne' | 'morphMany';
|
|
7
7
|
export type RelationsType = BasicRelationsType | PolymorphicRelationsType;
|
|
8
8
|
|
|
9
9
|
export interface BasicRelationAttributeProperties<
|
|
@@ -17,16 +17,14 @@ export interface BasicRelationAttributeProperties<
|
|
|
17
17
|
mappedBy?: RelationsKeysFromTo<T, S>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface PolymorphicRelationAttributeProperties<
|
|
21
|
-
R extends RelationsType,
|
|
22
|
-
> {
|
|
20
|
+
export interface PolymorphicRelationAttributeProperties<R extends RelationsType> {
|
|
23
21
|
relation: R;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
export type RelationAttribute<
|
|
27
25
|
S extends SchemaUID,
|
|
28
26
|
R extends RelationsType,
|
|
29
|
-
T extends R extends PolymorphicRelationsType ? never: SchemaUID = never
|
|
27
|
+
T extends R extends PolymorphicRelationsType ? never : SchemaUID = never
|
|
30
28
|
> = Attribute<'relation'> &
|
|
31
29
|
// Properties
|
|
32
30
|
(R extends BasicRelationsType
|
|
@@ -34,22 +32,21 @@ export type RelationAttribute<
|
|
|
34
32
|
: PolymorphicRelationAttributeProperties<R>) &
|
|
35
33
|
// Options
|
|
36
34
|
ConfigurableOption &
|
|
37
|
-
PrivateOption
|
|
35
|
+
PrivateOption;
|
|
38
36
|
|
|
39
37
|
export type RelationsKeysFromTo<
|
|
40
38
|
TTarget extends SchemaUID,
|
|
41
39
|
TSource extends SchemaUID
|
|
42
40
|
> = keyof PickRelationsFromTo<TTarget, TSource>;
|
|
43
41
|
|
|
44
|
-
export type PickRelationsFromTo<
|
|
45
|
-
TTarget,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
>;
|
|
42
|
+
export type PickRelationsFromTo<
|
|
43
|
+
TTarget extends SchemaUID,
|
|
44
|
+
TSource extends SchemaUID
|
|
45
|
+
> = GetAttributesByType<TTarget, 'relation', { target: TSource }>;
|
|
49
46
|
|
|
50
47
|
export type RelationPluralityModifier<
|
|
51
48
|
TRelation extends RelationsType,
|
|
52
|
-
TValue extends
|
|
49
|
+
TValue extends Record<string, unknown>
|
|
53
50
|
> = TRelation extends `${string}Many` ? TValue[] : TValue;
|
|
54
51
|
|
|
55
52
|
export type RelationValue<
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Attribute, ComponentAttribute } from '../attributes';
|
|
2
|
-
import { KeysBy, StringRecord } from '../../utils';
|
|
2
|
+
import { KeysBy, SchemaUID, StringRecord } from '../../utils';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Literal union type representing the possible natures of a content type
|
|
@@ -98,6 +98,11 @@ export interface PluginOptions {}
|
|
|
98
98
|
export interface ContentTypeSchema extends Schema {
|
|
99
99
|
modelType: 'contentType';
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Unique identifier of the schema
|
|
103
|
+
*/
|
|
104
|
+
uid: SchemaUID;
|
|
105
|
+
|
|
101
106
|
/**
|
|
102
107
|
* Determine the type of the content type (single-type or collection-type)
|
|
103
108
|
*/
|
|
@@ -2,8 +2,8 @@ import type Koa from 'koa';
|
|
|
2
2
|
import { Database } from '@strapi/database';
|
|
3
3
|
|
|
4
4
|
import type { StringMap } from './utils';
|
|
5
|
-
import type { GenericController } from '../../../core-api/controller'
|
|
6
|
-
import type { GenericService } from '../../../core-api/service'
|
|
5
|
+
import type { GenericController } from '../../../core-api/controller';
|
|
6
|
+
import type { GenericService } from '../../../core-api/service';
|
|
7
7
|
|
|
8
8
|
// TODO move custom fields types to a separate file
|
|
9
9
|
interface CustomFieldServerOptions {
|
|
@@ -92,9 +92,16 @@ export interface Strapi {
|
|
|
92
92
|
*/
|
|
93
93
|
contentType(uid: string): any;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Getter for the Strapi component container
|
|
97
|
+
*
|
|
98
|
+
* It returns all the registered components
|
|
99
|
+
*/
|
|
100
|
+
readonly components: any;
|
|
101
|
+
|
|
95
102
|
/**
|
|
96
103
|
* The custom fields registry
|
|
97
|
-
*
|
|
104
|
+
*
|
|
98
105
|
* It returns the custom fields interface
|
|
99
106
|
*/
|
|
100
107
|
readonly customFields: CustomFields;
|
|
@@ -361,7 +368,6 @@ export interface Strapi {
|
|
|
361
368
|
*/
|
|
362
369
|
log: any;
|
|
363
370
|
|
|
364
|
-
|
|
365
371
|
/**
|
|
366
372
|
* Used to manage cron within Strapi
|
|
367
373
|
*/
|
package/lib/types/factories.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Service,GenericService } from '../core-api/service';
|
|
1
|
+
import { Service, GenericService } from '../core-api/service';
|
|
2
2
|
import { Controller, GenericController } from '../core-api/controller';
|
|
3
3
|
import { Middleware } from '../middlewares';
|
|
4
4
|
import { Policy } from '../core/registries/policies';
|
|
5
|
-
import { Strapi } from '
|
|
5
|
+
import { Strapi } from './core/strapi';
|
|
6
6
|
|
|
7
7
|
type ControllerConfig<T extends Controller = Controller> = T;
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/strapi",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0-alpha.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",
|
|
@@ -80,18 +80,19 @@
|
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@koa/cors": "3.4.3",
|
|
82
82
|
"@koa/router": "10.1.1",
|
|
83
|
-
"@strapi/admin": "4.
|
|
84
|
-
"@strapi/
|
|
85
|
-
"@strapi/
|
|
86
|
-
"@strapi/
|
|
87
|
-
"@strapi/
|
|
88
|
-
"@strapi/
|
|
89
|
-
"@strapi/
|
|
90
|
-
"@strapi/plugin-content-
|
|
91
|
-
"@strapi/plugin-
|
|
92
|
-
"@strapi/plugin-
|
|
93
|
-
"@strapi/
|
|
94
|
-
"@strapi/utils": "4.
|
|
83
|
+
"@strapi/admin": "4.6.0-alpha.0",
|
|
84
|
+
"@strapi/data-transfer": "4.6.0-alpha.0",
|
|
85
|
+
"@strapi/database": "4.6.0-alpha.0",
|
|
86
|
+
"@strapi/generate-new": "4.6.0-alpha.0",
|
|
87
|
+
"@strapi/generators": "4.6.0-alpha.0",
|
|
88
|
+
"@strapi/logger": "4.6.0-alpha.0",
|
|
89
|
+
"@strapi/permissions": "4.6.0-alpha.0",
|
|
90
|
+
"@strapi/plugin-content-manager": "4.6.0-alpha.0",
|
|
91
|
+
"@strapi/plugin-content-type-builder": "4.6.0-alpha.0",
|
|
92
|
+
"@strapi/plugin-email": "4.6.0-alpha.0",
|
|
93
|
+
"@strapi/plugin-upload": "4.6.0-alpha.0",
|
|
94
|
+
"@strapi/typescript-utils": "4.6.0-alpha.0",
|
|
95
|
+
"@strapi/utils": "4.6.0-alpha.0",
|
|
95
96
|
"bcryptjs": "2.4.3",
|
|
96
97
|
"boxen": "5.1.2",
|
|
97
98
|
"chalk": "4.1.2",
|
|
@@ -128,7 +129,7 @@
|
|
|
128
129
|
"package-json": "7.0.0",
|
|
129
130
|
"qs": "6.10.1",
|
|
130
131
|
"resolve-cwd": "3.0.0",
|
|
131
|
-
"semver": "7.3.
|
|
132
|
+
"semver": "7.3.8",
|
|
132
133
|
"statuses": "2.0.1",
|
|
133
134
|
"uuid": "^8.3.2"
|
|
134
135
|
},
|
|
@@ -140,5 +141,5 @@
|
|
|
140
141
|
"node": ">=14.19.1 <=18.x.x",
|
|
141
142
|
"npm": ">=6.0.0"
|
|
142
143
|
},
|
|
143
|
-
"gitHead": "
|
|
144
|
+
"gitHead": "b7a87dcffc6f44e18eedef92e354096ffe32ce0c"
|
|
144
145
|
}
|