directus-template-cli 0.7.8 → 0.8.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/README.md +134 -37
- package/dist/commands/apply.d.ts +5 -0
- package/dist/commands/apply.js +32 -68
- package/dist/commands/extract.d.ts +30 -0
- package/dist/commands/extract.js +14 -5
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +90 -87
- package/dist/lib/extract/expand-deep-plan.d.ts +2 -0
- package/dist/lib/extract/expand-deep-plan.js +54 -0
- package/dist/lib/extract/expand-schema-plan.d.ts +2 -0
- package/dist/lib/extract/expand-schema-plan.js +55 -0
- package/dist/lib/extract/extract-assets.js +19 -6
- package/dist/lib/extract/extract-collections.d.ts +2 -1
- package/dist/lib/extract/extract-collections.js +5 -2
- package/dist/lib/extract/extract-content.d.ts +2 -1
- package/dist/lib/extract/extract-content.js +108 -13
- package/dist/lib/extract/extract-fields.d.ts +2 -1
- package/dist/lib/extract/extract-fields.js +5 -5
- package/dist/lib/extract/extract-relations.d.ts +2 -1
- package/dist/lib/extract/extract-relations.js +6 -4
- package/dist/lib/extract/index.d.ts +2 -1
- package/dist/lib/extract/index.js +67 -29
- package/dist/lib/init/index.d.ts +15 -0
- package/dist/lib/init/index.js +29 -13
- package/dist/lib/load/apply-flags.d.ts +5 -2
- package/dist/lib/load/apply-flags.js +0 -50
- package/dist/lib/load/finalize-collections.d.ts +2 -0
- package/dist/lib/load/finalize-collections.js +28 -0
- package/dist/lib/load/finalize-fields.d.ts +2 -0
- package/dist/lib/load/finalize-fields.js +25 -0
- package/dist/lib/load/index.js +38 -18
- package/dist/lib/load/load-collections.d.ts +2 -1
- package/dist/lib/load/load-collections.js +17 -30
- package/dist/lib/load/load-data.d.ts +3 -1
- package/dist/lib/load/load-data.js +47 -34
- package/dist/lib/load/load-relations.d.ts +2 -1
- package/dist/lib/load/load-relations.js +17 -7
- package/dist/lib/template-plan/collections.d.ts +4 -0
- package/dist/lib/template-plan/collections.js +26 -0
- package/dist/lib/template-plan/flags.d.ts +18 -0
- package/dist/lib/template-plan/flags.js +61 -0
- package/dist/lib/template-plan/index.d.ts +16 -0
- package/dist/lib/template-plan/index.js +77 -0
- package/dist/lib/template-plan/junctions.d.ts +10 -0
- package/dist/lib/template-plan/junctions.js +19 -0
- package/dist/lib/template-plan/metadata-plan.d.ts +2 -0
- package/dist/lib/template-plan/metadata-plan.js +36 -0
- package/dist/lib/template-plan/metadata.d.ts +5 -0
- package/dist/lib/template-plan/metadata.js +42 -0
- package/dist/lib/template-plan/types.d.ts +34 -0
- package/dist/lib/template-plan/types.js +1 -0
- package/oclif.manifest.json +173 -16
- package/package.json +1 -1
- package/dist/lib/load/update-required-fields.d.ts +0 -1
- package/dist/lib/load/update-required-fields.js +0 -20
package/dist/commands/init.js
CHANGED
|
@@ -62,76 +62,67 @@ export default class InitCommand extends BaseCommand {
|
|
|
62
62
|
* @returns Promise that resolves when the command is complete.
|
|
63
63
|
*/
|
|
64
64
|
async run() {
|
|
65
|
-
const { args, flags } = await this.parse(InitCommand);
|
|
65
|
+
const { args, flags, metadata } = await this.parse(InitCommand);
|
|
66
66
|
const typedFlags = flags;
|
|
67
67
|
const typedArgs = args;
|
|
68
|
+
const explicitFlags = {
|
|
69
|
+
gitInit: !metadata.flags.gitInit?.setFromDefault,
|
|
70
|
+
installDeps: !metadata.flags.installDeps?.setFromDefault,
|
|
71
|
+
};
|
|
68
72
|
// Set the target directory and create it if it doesn't exist
|
|
69
73
|
this.targetDir = path.resolve(args.directory);
|
|
70
|
-
await this.runInteractive(typedFlags, typedArgs);
|
|
74
|
+
await this.runInteractive(typedFlags, typedArgs, explicitFlags);
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await animatedBunny('Let\'s create a new Directus project!');
|
|
81
|
-
intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Create Project`);
|
|
82
|
-
// Check Docker availability before proceeding
|
|
83
|
-
const { createDocker } = await import('../services/docker.js');
|
|
84
|
-
const { DOCKER_CONFIG } = await import('../lib/init/config.js');
|
|
85
|
-
const dockerService = createDocker(DOCKER_CONFIG);
|
|
86
|
-
const dockerStatus = await dockerService.checkDocker();
|
|
87
|
-
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
88
|
-
cancel(dockerStatus.message || 'Docker is required to initialize a Directus project.');
|
|
89
|
-
process.exit(1);
|
|
76
|
+
async confirmBooleanFlag(message) {
|
|
77
|
+
const response = await confirm({
|
|
78
|
+
initialValue: true,
|
|
79
|
+
message,
|
|
80
|
+
});
|
|
81
|
+
if (isCancel(response)) {
|
|
82
|
+
cancel('Project creation cancelled.');
|
|
83
|
+
process.exit(0);
|
|
90
84
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// If there's no response, set a default
|
|
104
|
-
if (!dirResponse) {
|
|
105
|
-
clackLog.warn('No directory provided, using default: ./my-directus-project');
|
|
106
|
-
dirResponse = './my-directus-project';
|
|
107
|
-
}
|
|
108
|
-
this.targetDir = dirResponse;
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
async confirmOverwriteDirectory(flags) {
|
|
88
|
+
if (!fs.existsSync(this.targetDir) || flags.overwriteDir)
|
|
89
|
+
return Boolean(flags.overwriteDir);
|
|
90
|
+
const overwriteDirResponse = await confirm({
|
|
91
|
+
initialValue: false,
|
|
92
|
+
message: 'Directory already exists. Would you like to overwrite it?',
|
|
93
|
+
});
|
|
94
|
+
if (isCancel(overwriteDirResponse) || overwriteDirResponse === false) {
|
|
95
|
+
cancel('Project creation cancelled.');
|
|
96
|
+
process.exit(0);
|
|
109
97
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
async promptForTargetDirectory(args) {
|
|
101
|
+
if (args.directory && args.directory !== '.')
|
|
102
|
+
return;
|
|
103
|
+
let dirResponse = await text({
|
|
104
|
+
message: 'Enter the directory to create the project in:',
|
|
105
|
+
placeholder: './my-directus-project',
|
|
106
|
+
});
|
|
107
|
+
if (isCancel(dirResponse)) {
|
|
108
|
+
cancel('Project creation cancelled.');
|
|
109
|
+
process.exit(0);
|
|
122
110
|
}
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
// If there's no response, set a default
|
|
112
|
+
if (!dirResponse) {
|
|
113
|
+
clackLog.warn('No directory provided, using default: ./my-directus-project');
|
|
114
|
+
dirResponse = './my-directus-project';
|
|
115
|
+
}
|
|
116
|
+
this.targetDir = dirResponse;
|
|
117
|
+
}
|
|
118
|
+
async promptForValidTemplate(template, availableTemplates) {
|
|
128
119
|
if (!template) {
|
|
129
120
|
const templateResponse = await select({
|
|
130
121
|
message: 'Which Directus backend template would you like to use?',
|
|
131
122
|
options: availableTemplates.map(tmpl => ({
|
|
132
|
-
hint: tmpl.description,
|
|
133
|
-
label: tmpl.name,
|
|
134
|
-
value: tmpl.id,
|
|
123
|
+
hint: tmpl.description,
|
|
124
|
+
label: tmpl.name,
|
|
125
|
+
value: tmpl.id,
|
|
135
126
|
})),
|
|
136
127
|
});
|
|
137
128
|
if (isCancel(templateResponse)) {
|
|
@@ -140,14 +131,9 @@ export default class InitCommand extends BaseCommand {
|
|
|
140
131
|
}
|
|
141
132
|
template = templateResponse;
|
|
142
133
|
}
|
|
143
|
-
|
|
144
|
-
chosenTemplateObject = availableTemplates.find(t => t.id === template);
|
|
145
|
-
// 3. Validate that the template exists in the available list
|
|
146
|
-
const isDirectUrl = template?.startsWith('http');
|
|
147
|
-
// Validate against the 'id' property of the template objects
|
|
148
|
-
while (!isDirectUrl && !availableTemplates.some(t => t.id === template)) {
|
|
149
|
-
// Keep the warning message simple or refer back to the list shown in the prompt
|
|
134
|
+
while (!template.startsWith('http') && !availableTemplates.some(t => t.id === template)) {
|
|
150
135
|
clackLog.warn(`Template ID "${template}" is not valid. Please choose from the list provided or enter a direct GitHub URL.`);
|
|
136
|
+
// eslint-disable-next-line no-await-in-loop
|
|
151
137
|
const templateNameResponse = await text({
|
|
152
138
|
message: 'Please enter a valid template ID, a direct GitHub URL, or Ctrl+C to cancel:',
|
|
153
139
|
});
|
|
@@ -156,8 +142,37 @@ export default class InitCommand extends BaseCommand {
|
|
|
156
142
|
process.exit(0);
|
|
157
143
|
}
|
|
158
144
|
template = templateNameResponse;
|
|
159
|
-
chosenTemplateObject = availableTemplates.find(t => t.id === template); // Update chosen object after re-entry
|
|
160
145
|
}
|
|
146
|
+
return template;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Interactive mode: prompts the user for each piece of info, with added template checks.
|
|
150
|
+
* @param flags - The flags passed to the command.
|
|
151
|
+
* @param args - The arguments passed to the command.
|
|
152
|
+
* @returns void
|
|
153
|
+
*/
|
|
154
|
+
async runInteractive(flags, args, explicitFlags) {
|
|
155
|
+
// Show animated intro
|
|
156
|
+
await animatedBunny('Let\'s create a new Directus project!');
|
|
157
|
+
intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Create Project`);
|
|
158
|
+
// Check Docker availability before proceeding
|
|
159
|
+
const { createDocker } = await import('../services/docker.js');
|
|
160
|
+
const { DOCKER_CONFIG } = await import('../lib/init/config.js');
|
|
161
|
+
const dockerService = createDocker(DOCKER_CONFIG);
|
|
162
|
+
const dockerStatus = await dockerService.checkDocker();
|
|
163
|
+
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
164
|
+
cancel(dockerStatus.message || 'Docker is required to initialize a Directus project.');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
// Create GitHub service
|
|
168
|
+
const github = createGitHub();
|
|
169
|
+
// If no dir is provided, ask for it
|
|
170
|
+
await this.promptForTargetDirectory(args);
|
|
171
|
+
const overwriteDir = await this.confirmOverwriteDirectory(flags);
|
|
172
|
+
// 1. Fetch available templates (now returns Array<{id: string, name: string, description?: string}>)
|
|
173
|
+
const availableTemplates = await github.getTemplates();
|
|
174
|
+
// 2. Prompt for template if not provided, then validate it against known templates or a direct URL.
|
|
175
|
+
const template = await this.promptForValidTemplate(flags.template, availableTemplates);
|
|
161
176
|
flags.template = template; // Ensure the flag stores the ID
|
|
162
177
|
// Download the template to a temporary directory to read its configuration
|
|
163
178
|
const tempDir = path.join(os.tmpdir(), `directus-template-${Date.now()}`);
|
|
@@ -170,7 +185,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
170
185
|
// Read template configuration
|
|
171
186
|
const templateInfo = readTemplateConfig(tempDir);
|
|
172
187
|
// 4. If template has frontends and user hasn't specified a valid one, ask from the list
|
|
173
|
-
if (templateInfo?.frontendOptions.length > 0 && (!chosenFrontend || !templateInfo.frontendOptions.
|
|
188
|
+
if (templateInfo?.frontendOptions.length > 0 && (!chosenFrontend || !templateInfo.frontendOptions.some(f => f.id === chosenFrontend))) {
|
|
174
189
|
const frontendResponse = await select({
|
|
175
190
|
message: 'Which frontend framework do you want to use?',
|
|
176
191
|
options: templateInfo.frontendOptions.map(frontend => ({
|
|
@@ -194,24 +209,12 @@ export default class InitCommand extends BaseCommand {
|
|
|
194
209
|
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
195
210
|
}
|
|
196
211
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
process.exit(0);
|
|
204
|
-
}
|
|
205
|
-
const installDeps = installDepsResponse;
|
|
206
|
-
const initGitResponse = await confirm({
|
|
207
|
-
initialValue: true,
|
|
208
|
-
message: 'Initialize a new Git repository?',
|
|
209
|
-
});
|
|
210
|
-
if (isCancel(initGitResponse)) {
|
|
211
|
-
cancel('Project creation cancelled.');
|
|
212
|
-
process.exit(0);
|
|
213
|
-
}
|
|
214
|
-
const initGit = initGitResponse;
|
|
212
|
+
const installDeps = explicitFlags.installDeps
|
|
213
|
+
? flags.installDeps ?? true
|
|
214
|
+
: await this.confirmBooleanFlag('Would you like to install project dependencies automatically?');
|
|
215
|
+
const initGit = explicitFlags.gitInit
|
|
216
|
+
? flags.gitInit ?? true
|
|
217
|
+
: await this.confirmBooleanFlag('Initialize a new Git repository?');
|
|
215
218
|
// Track the command start unless telemetry is disabled
|
|
216
219
|
if (!flags.disableTelemetry) {
|
|
217
220
|
await track({
|
|
@@ -235,7 +238,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
235
238
|
frontend: chosenFrontend,
|
|
236
239
|
gitInit: initGit,
|
|
237
240
|
installDeps,
|
|
238
|
-
overwriteDir
|
|
241
|
+
overwriteDir,
|
|
239
242
|
template,
|
|
240
243
|
},
|
|
241
244
|
});
|
|
@@ -249,7 +252,7 @@ export default class InitCommand extends BaseCommand {
|
|
|
249
252
|
frontend: chosenFrontend,
|
|
250
253
|
gitInit: initGit,
|
|
251
254
|
installDeps,
|
|
252
|
-
overwriteDir
|
|
255
|
+
overwriteDir,
|
|
253
256
|
template,
|
|
254
257
|
},
|
|
255
258
|
lifecycle: 'complete',
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readCollections, readRelations } from '@directus/sdk';
|
|
2
|
+
import { ux } from '@oclif/core';
|
|
3
|
+
import { api } from '../sdk.js';
|
|
4
|
+
import { includesCollection } from '../template-plan/index.js';
|
|
5
|
+
export async function expandDeepPlan(plan) {
|
|
6
|
+
if (!plan.partial || plan.relationStrategy !== 'deep' || !plan.collections)
|
|
7
|
+
return plan;
|
|
8
|
+
const collections = await api.client.request(readCollections());
|
|
9
|
+
const availableCollections = collections
|
|
10
|
+
.filter((collection) => !collection.collection.startsWith('directus_', 0))
|
|
11
|
+
.filter((collection) => collection.schema !== null)
|
|
12
|
+
.map((collection) => collection.collection)
|
|
13
|
+
.filter((collection) => includesCollection(collection, { ...plan, collections: undefined }));
|
|
14
|
+
const available = new Set(availableCollections);
|
|
15
|
+
const missingCollections = plan.collections.filter((collection) => !available.has(collection));
|
|
16
|
+
if (missingCollections.length > 0) {
|
|
17
|
+
ux.warn(`Requested collections not found or excluded: ${missingCollections.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
const selected = new Set(plan.collections.filter((collection) => available.has(collection)));
|
|
20
|
+
const relations = (await api.client.request(readRelations()));
|
|
21
|
+
let changed = true;
|
|
22
|
+
while (changed) {
|
|
23
|
+
changed = false;
|
|
24
|
+
const candidates = [];
|
|
25
|
+
for (const relation of relations) {
|
|
26
|
+
if (selected.has(relation.collection) && relation.related_collection) {
|
|
27
|
+
candidates.push(relation.related_collection);
|
|
28
|
+
}
|
|
29
|
+
if (relation.related_collection && selected.has(relation.related_collection)) {
|
|
30
|
+
candidates.push(relation.collection);
|
|
31
|
+
}
|
|
32
|
+
if (selected.has(relation.collection) && relation.meta?.one_allowed_collections) {
|
|
33
|
+
candidates.push(...relation.meta.one_allowed_collections);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const collection of candidates) {
|
|
37
|
+
if (!available.has(collection))
|
|
38
|
+
continue;
|
|
39
|
+
if (selected.has(collection))
|
|
40
|
+
continue;
|
|
41
|
+
selected.add(collection);
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const expandedCollections = [...selected];
|
|
46
|
+
const addedCollections = expandedCollections.filter((collection) => !plan.collections?.includes(collection));
|
|
47
|
+
if (addedCollections.length > 0) {
|
|
48
|
+
ux.warn(`Deep relation strategy expanded collections: ${addedCollections.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
...plan,
|
|
52
|
+
collections: expandedCollections,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readCollections, readRelations } from '@directus/sdk';
|
|
2
|
+
import { ux } from '@oclif/core';
|
|
3
|
+
import { api } from '../sdk.js';
|
|
4
|
+
import { includesCollection } from '../template-plan/index.js';
|
|
5
|
+
export async function expandSchemaPlan(plan) {
|
|
6
|
+
if (!plan.partial || !plan.collections)
|
|
7
|
+
return plan;
|
|
8
|
+
const collections = (await api.client.request(readCollections()));
|
|
9
|
+
const availableCollections = collections
|
|
10
|
+
.filter((collection) => !collection.collection.startsWith('directus_', 0))
|
|
11
|
+
.map((collection) => collection.collection)
|
|
12
|
+
.filter((collection) => includesCollection(collection, { ...plan, collections: undefined }));
|
|
13
|
+
const collectionMap = new Map(collections.map((collection) => [collection.collection, collection]));
|
|
14
|
+
const available = new Set(availableCollections);
|
|
15
|
+
const selected = new Set(plan.collections.filter((collection) => available.has(collection)));
|
|
16
|
+
const relations = (await api.client.request(readRelations()));
|
|
17
|
+
let changed = true;
|
|
18
|
+
while (changed) {
|
|
19
|
+
changed = false;
|
|
20
|
+
const candidates = [];
|
|
21
|
+
for (const collection of selected) {
|
|
22
|
+
const group = collectionMap.get(collection)?.meta?.group;
|
|
23
|
+
if (group)
|
|
24
|
+
candidates.push(group);
|
|
25
|
+
}
|
|
26
|
+
for (const relation of relations) {
|
|
27
|
+
if (selected.has(relation.collection) && relation.related_collection) {
|
|
28
|
+
candidates.push(relation.related_collection);
|
|
29
|
+
}
|
|
30
|
+
if (relation.related_collection && selected.has(relation.related_collection)) {
|
|
31
|
+
candidates.push(relation.collection);
|
|
32
|
+
}
|
|
33
|
+
if (selected.has(relation.collection) && relation.meta?.one_allowed_collections) {
|
|
34
|
+
candidates.push(...relation.meta.one_allowed_collections);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const collection of candidates) {
|
|
38
|
+
if (!available.has(collection))
|
|
39
|
+
continue;
|
|
40
|
+
if (selected.has(collection))
|
|
41
|
+
continue;
|
|
42
|
+
selected.add(collection);
|
|
43
|
+
changed = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const schemaCollections = [...selected];
|
|
47
|
+
const addedCollections = schemaCollections.filter((collection) => !plan.collections?.includes(collection));
|
|
48
|
+
if (addedCollections.length > 0) {
|
|
49
|
+
ux.warn(`Schema scope expanded collections: ${addedCollections.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...plan,
|
|
53
|
+
schemaCollections,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -5,10 +5,13 @@ import path from 'pathe';
|
|
|
5
5
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
6
6
|
import { api } from '../sdk.js';
|
|
7
7
|
import catchError from '../utils/catch-error.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// Keep asset pages conservative because each item may trigger a binary download.
|
|
9
|
+
const PAGE_SIZE = 100;
|
|
10
|
+
async function getAssetPage(page) {
|
|
11
|
+
return api.client.request(readFiles({ limit: PAGE_SIZE, page }));
|
|
10
12
|
}
|
|
11
13
|
async function downloadFile(file, dir) {
|
|
14
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
12
15
|
const response = await api.client.request(() => ({
|
|
13
16
|
method: 'GET',
|
|
14
17
|
path: `/assets/${file.id}`,
|
|
@@ -29,10 +32,20 @@ export async function downloadAllFiles(dir) {
|
|
|
29
32
|
if (path && !fs.existsSync(fullPath)) {
|
|
30
33
|
fs.mkdirSync(fullPath, { recursive: true });
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
let page = 1;
|
|
36
|
+
while (true) {
|
|
37
|
+
ux.action.status = `Downloading assets page ${page}`;
|
|
38
|
+
// Page asset metadata sequentially and finish each page before fetching the next, to avoid queuing all downloads at once.
|
|
39
|
+
// eslint-disable-next-line no-await-in-loop
|
|
40
|
+
const fileList = await getAssetPage(page);
|
|
41
|
+
// eslint-disable-next-line no-await-in-loop
|
|
42
|
+
await Promise.all(fileList.map((file) => downloadFile(file, dir).catch((error) => {
|
|
43
|
+
catchError(`Error downloading ${file.filename_disk}: ${error.message}`);
|
|
44
|
+
})));
|
|
45
|
+
if (fileList.length < PAGE_SIZE)
|
|
46
|
+
break;
|
|
47
|
+
page++;
|
|
48
|
+
}
|
|
36
49
|
}
|
|
37
50
|
catch (error) {
|
|
38
51
|
catchError(error);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Extract collections from the Directus instance
|
|
3
4
|
*/
|
|
4
|
-
export default function extractCollections(dir: string): Promise<void>;
|
|
5
|
+
export default function extractCollections(dir: string, plan?: TemplatePlan): Promise<void>;
|
|
@@ -2,16 +2,19 @@ import { readCollections } from '@directus/sdk';
|
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
3
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
4
4
|
import { api } from '../sdk.js';
|
|
5
|
+
import { includesSchemaCollection } from '../template-plan/index.js';
|
|
5
6
|
import catchError from '../utils/catch-error.js';
|
|
6
7
|
import writeToFile from '../utils/write-to-file.js';
|
|
7
8
|
/**
|
|
8
9
|
* Extract collections from the Directus instance
|
|
9
10
|
*/
|
|
10
|
-
export default async function extractCollections(dir) {
|
|
11
|
+
export default async function extractCollections(dir, plan) {
|
|
11
12
|
ux.action.start(ux.colorize(DIRECTUS_PINK, 'Extracting collections'));
|
|
12
13
|
try {
|
|
13
14
|
const response = await api.client.request(readCollections());
|
|
14
|
-
const collections = response
|
|
15
|
+
const collections = response
|
|
16
|
+
.filter((collection) => !collection.collection.startsWith('directus_'))
|
|
17
|
+
.filter((collection) => includesSchemaCollection(collection.collection, plan));
|
|
15
18
|
await writeToFile('collections', collections, dir);
|
|
16
19
|
}
|
|
17
20
|
catch (error) {
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type TemplatePlan, type TemplateWarning } from '../template-plan/index.js';
|
|
2
|
+
export declare function extractContent(dir: string, plan?: TemplatePlan): Promise<TemplateWarning[]>;
|
|
@@ -1,33 +1,128 @@
|
|
|
1
|
-
import { readCollections, readItems } from '@directus/sdk';
|
|
2
|
-
import { ux } from '@oclif/core';
|
|
1
|
+
import { readCollections, readItems, readRelations } from '@directus/sdk';
|
|
2
|
+
import { Errors, ux } from '@oclif/core';
|
|
3
3
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
4
4
|
import { api } from '../sdk.js';
|
|
5
|
+
import { getBrokenJunctionCollections, includesCollection, } from '../template-plan/index.js';
|
|
5
6
|
import catchError from '../utils/catch-error.js';
|
|
6
7
|
import writeToFile from '../utils/write-to-file.js';
|
|
7
|
-
|
|
8
|
+
// Content items are JSON-only, so pages can be larger than asset download pages.
|
|
9
|
+
const PAGE_SIZE = 500;
|
|
10
|
+
async function getCollections(relations, plan) {
|
|
8
11
|
const response = await api.client.request(readCollections());
|
|
12
|
+
const brokenJunctions = getBrokenJunctionCollections(relations, plan);
|
|
9
13
|
return response
|
|
10
|
-
.filter(item => !item.collection.startsWith('directus_', 0))
|
|
11
|
-
.filter(item => item.schema
|
|
12
|
-
.map(i => i.collection)
|
|
14
|
+
.filter((item) => !item.collection.startsWith('directus_', 0))
|
|
15
|
+
.filter((item) => item.schema !== null)
|
|
16
|
+
.map((i) => i.collection)
|
|
17
|
+
.filter((collection) => includesCollection(collection, plan))
|
|
18
|
+
.filter((collection) => !brokenJunctions.has(collection));
|
|
13
19
|
}
|
|
14
|
-
async function
|
|
20
|
+
async function getCollectionItems(collection) {
|
|
21
|
+
const items = [];
|
|
22
|
+
let page = 1;
|
|
23
|
+
while (true) {
|
|
24
|
+
// eslint-disable-next-line no-await-in-loop
|
|
25
|
+
const response = (await api.client.request(readItems(collection, { limit: PAGE_SIZE, page })));
|
|
26
|
+
items.push(...response);
|
|
27
|
+
if (response.length < PAGE_SIZE)
|
|
28
|
+
break;
|
|
29
|
+
page++;
|
|
30
|
+
}
|
|
31
|
+
return items;
|
|
32
|
+
}
|
|
33
|
+
function getExcludedRelationFields(collection, relations, plan) {
|
|
34
|
+
if (!plan?.partial || plan.relationStrategy === 'deep')
|
|
35
|
+
return [];
|
|
36
|
+
const m2oFields = relations
|
|
37
|
+
.filter((relation) => relation.collection === collection)
|
|
38
|
+
.filter((relation) => Boolean(relation.related_collection))
|
|
39
|
+
.filter((relation) => !includesCollection(relation.related_collection, plan))
|
|
40
|
+
.map((relation) => ({
|
|
41
|
+
field: relation.field,
|
|
42
|
+
relatedCollection: relation.related_collection,
|
|
43
|
+
type: 'm2o',
|
|
44
|
+
}));
|
|
45
|
+
const aliasFields = relations
|
|
46
|
+
.filter((relation) => relation.related_collection === collection)
|
|
47
|
+
.filter((relation) => Boolean(relation.meta?.one_field))
|
|
48
|
+
.filter((relation) => !includesCollection(relation.collection, plan))
|
|
49
|
+
.map((relation) => ({
|
|
50
|
+
field: relation.meta.one_field,
|
|
51
|
+
relatedCollection: relation.collection,
|
|
52
|
+
type: 'alias',
|
|
53
|
+
}));
|
|
54
|
+
return [...m2oFields, ...aliasFields];
|
|
55
|
+
}
|
|
56
|
+
function hasValue(value) {
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.length > 0;
|
|
59
|
+
return value !== null && value !== undefined;
|
|
60
|
+
}
|
|
61
|
+
function emptyExcludedRelations(items, relations) {
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
for (const relation of relations) {
|
|
64
|
+
if (!(relation.field in item))
|
|
65
|
+
continue;
|
|
66
|
+
if (relation.type === 'alias') {
|
|
67
|
+
delete item[relation.field];
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
item[relation.field] = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getBrokenRelationWarnings(collection, items, relations) {
|
|
76
|
+
return relations
|
|
77
|
+
.map((relation) => ({
|
|
78
|
+
collection,
|
|
79
|
+
count: items.filter((item) => hasValue(item[relation.field])).length,
|
|
80
|
+
field: relation.field,
|
|
81
|
+
relatedCollection: relation.relatedCollection,
|
|
82
|
+
type: 'excluded_relation',
|
|
83
|
+
}))
|
|
84
|
+
.filter((warning) => warning.count > 0);
|
|
85
|
+
}
|
|
86
|
+
async function getDataFromCollection(collection, dir, relations, plan) {
|
|
15
87
|
try {
|
|
16
|
-
|
|
88
|
+
ux.action.status = `Extracting content: ${collection}`;
|
|
89
|
+
const response = await getCollectionItems(collection);
|
|
90
|
+
const excludedRelations = getExcludedRelationFields(collection, relations, plan);
|
|
91
|
+
if (plan?.relationStrategy === 'empty') {
|
|
92
|
+
emptyExcludedRelations(response, excludedRelations);
|
|
93
|
+
}
|
|
94
|
+
const warnings = plan?.relationStrategy === 'preserve' ? getBrokenRelationWarnings(collection, response, excludedRelations) : [];
|
|
17
95
|
await writeToFile(`${collection}`, response, `${dir}/content/`);
|
|
96
|
+
return warnings;
|
|
18
97
|
}
|
|
19
98
|
catch (error) {
|
|
20
|
-
catchError(error
|
|
99
|
+
catchError(error, {
|
|
100
|
+
context: { collection, function: 'getDataFromCollection' },
|
|
101
|
+
fatal: true,
|
|
102
|
+
});
|
|
21
103
|
}
|
|
22
104
|
}
|
|
23
|
-
export async function extractContent(dir) {
|
|
105
|
+
export async function extractContent(dir, plan) {
|
|
24
106
|
ux.action.start(ux.colorize(DIRECTUS_PINK, 'Extracting content'));
|
|
107
|
+
const warnings = [];
|
|
25
108
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
109
|
+
const relations = (await api.client.request(readRelations()));
|
|
110
|
+
const collections = await getCollections(relations, plan);
|
|
111
|
+
for (const collection of collections) {
|
|
112
|
+
// Keep extraction sequential so the shared ux.action.status reflects the active collection.
|
|
113
|
+
// eslint-disable-next-line no-await-in-loop
|
|
114
|
+
const collectionWarnings = await getDataFromCollection(collection, dir, relations, plan);
|
|
115
|
+
warnings.push(...collectionWarnings);
|
|
116
|
+
}
|
|
28
117
|
}
|
|
29
118
|
catch (error) {
|
|
30
|
-
|
|
119
|
+
if (error instanceof Errors.CLIError)
|
|
120
|
+
throw error;
|
|
121
|
+
catchError(error, {
|
|
122
|
+
context: { function: 'extractContent' },
|
|
123
|
+
fatal: true,
|
|
124
|
+
});
|
|
31
125
|
}
|
|
32
126
|
ux.action.stop();
|
|
127
|
+
return warnings;
|
|
33
128
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Extract fields from the Directus instance
|
|
3
4
|
*/
|
|
4
|
-
export default function extractFields(dir: string): Promise<void>;
|
|
5
|
+
export default function extractFields(dir: string, plan?: TemplatePlan): Promise<void>;
|
|
@@ -2,12 +2,13 @@ import { readFields } from '@directus/sdk';
|
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
3
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
4
4
|
import { api } from '../sdk.js';
|
|
5
|
+
import { includesSchemaCollection } from '../template-plan/index.js';
|
|
5
6
|
import catchError from '../utils/catch-error.js';
|
|
6
7
|
import writeToFile from '../utils/write-to-file.js';
|
|
7
8
|
/**
|
|
8
9
|
* Extract fields from the Directus instance
|
|
9
10
|
*/
|
|
10
|
-
export default async function extractFields(dir) {
|
|
11
|
+
export default async function extractFields(dir, plan) {
|
|
11
12
|
ux.action.start(ux.colorize(DIRECTUS_PINK, 'Extracting fields'));
|
|
12
13
|
try {
|
|
13
14
|
const response = await api.client.request(readFields());
|
|
@@ -15,10 +16,9 @@ export default async function extractFields(dir) {
|
|
|
15
16
|
throw new TypeError('Unexpected response format');
|
|
16
17
|
}
|
|
17
18
|
const fields = response
|
|
18
|
-
.filter(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.map(i => {
|
|
19
|
+
.filter((i) => i.meta && !i.meta.system)
|
|
20
|
+
.filter((i) => includesSchemaCollection(i.collection, plan))
|
|
21
|
+
.map((i) => {
|
|
22
22
|
if (i.meta) {
|
|
23
23
|
delete i.meta.id;
|
|
24
24
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Extract relations from the Directus instance
|
|
3
4
|
*/
|
|
4
|
-
export default function extractRelations(dir: string): Promise<void>;
|
|
5
|
+
export default function extractRelations(dir: string, plan?: TemplatePlan): Promise<void>;
|