directus-template-cli 0.7.6 → 0.8.0-partials.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/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 +105 -12
- 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/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 +36 -19
- 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 +2 -1
- package/dist/lib/load/load-data.js +46 -34
- package/dist/lib/load/load-files.js +8 -8
- 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 +33 -0
- package/dist/lib/template-plan/metadata.d.ts +5 -0
- package/dist/lib/template-plan/metadata.js +39 -0
- package/dist/lib/template-plan/types.d.ts +34 -0
- package/dist/lib/template-plan/types.js +1 -0
- package/dist/services/github.js +1 -1
- package/oclif.manifest.json +173 -16
- package/package.json +1 -2
- package/dist/lib/load/update-required-fields.d.ts +0 -1
- package/dist/lib/load/update-required-fields.js +0 -20
|
@@ -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,126 @@
|
|
|
1
|
-
import { readCollections, readItems } from '@directus/sdk';
|
|
1
|
+
import { readCollections, readItems, readRelations } 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 { 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
|
+
});
|
|
103
|
+
throw error;
|
|
21
104
|
}
|
|
22
105
|
}
|
|
23
|
-
export async function extractContent(dir) {
|
|
106
|
+
export async function extractContent(dir, plan) {
|
|
24
107
|
ux.action.start(ux.colorize(DIRECTUS_PINK, 'Extracting content'));
|
|
108
|
+
const warnings = [];
|
|
25
109
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
110
|
+
const relations = (await api.client.request(readRelations()));
|
|
111
|
+
const collections = await getCollections(relations, plan);
|
|
112
|
+
for (const collection of collections) {
|
|
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
|
-
catchError(error
|
|
119
|
+
catchError(error, {
|
|
120
|
+
context: { function: 'extractContent' },
|
|
121
|
+
fatal: true,
|
|
122
|
+
});
|
|
31
123
|
}
|
|
32
124
|
ux.action.stop();
|
|
125
|
+
return warnings;
|
|
33
126
|
}
|
|
@@ -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>;
|
|
@@ -2,12 +2,13 @@ import { readFields, readRelations } 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 { includesRelation } 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 relations from the Directus instance
|
|
9
10
|
*/
|
|
10
|
-
export default async function extractRelations(dir) {
|
|
11
|
+
export default async function extractRelations(dir, plan) {
|
|
11
12
|
ux.action.start(ux.colorize(DIRECTUS_PINK, 'Extracting relations'));
|
|
12
13
|
try {
|
|
13
14
|
const response = await api.client.request(readRelations());
|
|
@@ -16,9 +17,10 @@ export default async function extractRelations(dir) {
|
|
|
16
17
|
const customFields = fields.filter((i) => !i.meta?.system);
|
|
17
18
|
const relations = response
|
|
18
19
|
// Filter out relations where the collection starts with 'directus_' && the field is not within the customFields array
|
|
19
|
-
.filter((i) => !i.collection.startsWith('directus_', 0)
|
|
20
|
-
|
|
21
|
-
.
|
|
20
|
+
.filter((i) => !i.collection.startsWith('directus_', 0) ||
|
|
21
|
+
customFields.some((f) => f.collection === i.collection && f.field === i.field))
|
|
22
|
+
.filter((i) => includesRelation(i.collection, i.related_collection, plan))
|
|
23
|
+
.map((i) => {
|
|
22
24
|
delete i.meta.id;
|
|
23
25
|
return i;
|
|
24
26
|
});
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
2
|
+
export default function extract(dir: string, plan?: TemplatePlan): Promise<{}>;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
|
-
import fs from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { buildTemplatePlan, writeTemplateMetadata, } from '../template-plan/index.js';
|
|
4
|
+
import catchError from '../utils/catch-error.js';
|
|
5
|
+
import { expandDeepPlan } from './expand-deep-plan.js';
|
|
6
|
+
import { expandSchemaPlan } from './expand-schema-plan.js';
|
|
3
7
|
import extractAccess from './extract-access.js';
|
|
4
8
|
import { downloadAllFiles } from './extract-assets.js';
|
|
5
9
|
import extractCollections from './extract-collections.js';
|
|
@@ -19,34 +23,68 @@ import extractSchema from './extract-schema.js';
|
|
|
19
23
|
import extractSettings from './extract-settings.js';
|
|
20
24
|
import extractTranslations from './extract-translations.js';
|
|
21
25
|
import extractUsers from './extract-users.js';
|
|
22
|
-
export default async function extract(dir) {
|
|
23
|
-
// Get the destination directory for the actual files
|
|
26
|
+
export default async function extract(dir, plan = buildTemplatePlan()) {
|
|
24
27
|
const destination = `${dir}/src`;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
fs.
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
const schemaPlan = await expandSchemaPlan(plan);
|
|
29
|
+
const effectivePlan = await expandDeepPlan(schemaPlan);
|
|
30
|
+
try {
|
|
31
|
+
await fs.mkdir(destination, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
catchError(error, { context: { destination }, fatal: true });
|
|
35
|
+
}
|
|
36
|
+
if (effectivePlan.components.schema) {
|
|
37
|
+
await extractSchema(destination);
|
|
38
|
+
await extractCollections(destination, effectivePlan);
|
|
39
|
+
await extractFields(destination, effectivePlan);
|
|
40
|
+
await extractRelations(destination, effectivePlan);
|
|
41
|
+
}
|
|
42
|
+
if (effectivePlan.components.files) {
|
|
43
|
+
await extractFolders(destination);
|
|
44
|
+
await extractFiles(destination);
|
|
45
|
+
await downloadAllFiles(destination);
|
|
46
|
+
}
|
|
47
|
+
if (effectivePlan.components.users || effectivePlan.components.permissions) {
|
|
48
|
+
await extractRoles(destination);
|
|
49
|
+
await extractPermissions(destination);
|
|
50
|
+
await extractPolicies(destination);
|
|
51
|
+
if (effectivePlan.components.users) {
|
|
52
|
+
await extractUsers(destination);
|
|
53
|
+
}
|
|
54
|
+
await extractAccess(destination);
|
|
55
|
+
}
|
|
56
|
+
if (effectivePlan.components.settings) {
|
|
57
|
+
await extractPresets(destination);
|
|
58
|
+
await extractTranslations(destination);
|
|
59
|
+
await extractSettings(destination);
|
|
60
|
+
}
|
|
61
|
+
if (effectivePlan.components.flows) {
|
|
62
|
+
await extractFlows(destination);
|
|
63
|
+
await extractOperations(destination);
|
|
64
|
+
}
|
|
65
|
+
if (effectivePlan.components.dashboards) {
|
|
66
|
+
await extractDashboards(destination);
|
|
67
|
+
await extractPanels(destination);
|
|
68
|
+
}
|
|
69
|
+
if (effectivePlan.components.extensions) {
|
|
70
|
+
await extractExtensions(destination);
|
|
71
|
+
}
|
|
72
|
+
const warnings = [];
|
|
73
|
+
if (effectivePlan.components.content) {
|
|
74
|
+
const contentWarnings = await extractContent(destination, effectivePlan);
|
|
75
|
+
warnings.push(...contentWarnings);
|
|
76
|
+
}
|
|
77
|
+
for (const warning of warnings) {
|
|
78
|
+
ux.warn(`Excluded relation: ${warning.collection}.${warning.field} -> ${warning.relatedCollection} (${warning.count} records)`);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await writeTemplateMetadata(destination, effectivePlan, warnings);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
catchError(error, {
|
|
85
|
+
context: { function: 'writeTemplateMetadata' },
|
|
86
|
+
fatal: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
51
89
|
return {};
|
|
52
90
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
export interface ApplyFlags {
|
|
2
|
+
allowBrokenRelations?: boolean;
|
|
3
|
+
collections?: string;
|
|
2
4
|
content: boolean;
|
|
3
5
|
dashboards: boolean;
|
|
4
6
|
directusToken: string;
|
|
5
7
|
directusUrl: string;
|
|
6
8
|
disableTelemetry?: boolean;
|
|
9
|
+
excludeCollections?: string;
|
|
7
10
|
extensions: boolean;
|
|
8
11
|
files: boolean;
|
|
9
12
|
flows: boolean;
|
|
13
|
+
noAssets?: boolean;
|
|
10
14
|
noExit?: boolean;
|
|
11
15
|
partial: boolean;
|
|
12
16
|
permissions: boolean;
|
|
13
17
|
programmatic: boolean;
|
|
18
|
+
relationStrategy?: 'deep' | 'empty' | 'preserve';
|
|
14
19
|
schema: boolean;
|
|
15
20
|
settings: boolean;
|
|
16
21
|
templateLocation: string;
|
|
@@ -19,6 +24,4 @@ export interface ApplyFlags {
|
|
|
19
24
|
userPassword: string;
|
|
20
25
|
users?: boolean;
|
|
21
26
|
}
|
|
22
|
-
export declare const loadFlags: readonly ["content", "dashboards", "extensions", "files", "flows", "permissions", "schema", "settings", "users"];
|
|
23
27
|
export declare function validateProgrammaticFlags(flags: ApplyFlags): ApplyFlags;
|
|
24
|
-
export declare function validateInteractiveFlags(flags: ApplyFlags): ApplyFlags;
|
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
|
-
import catchError from '../utils/catch-error.js';
|
|
3
|
-
export const loadFlags = [
|
|
4
|
-
'content',
|
|
5
|
-
'dashboards',
|
|
6
|
-
'extensions',
|
|
7
|
-
'files',
|
|
8
|
-
'flows',
|
|
9
|
-
'permissions',
|
|
10
|
-
'schema',
|
|
11
|
-
'settings',
|
|
12
|
-
'users',
|
|
13
|
-
];
|
|
14
2
|
export function validateProgrammaticFlags(flags) {
|
|
15
3
|
const { directusToken, directusUrl, templateLocation, userEmail, userPassword } = flags;
|
|
16
4
|
if (!directusUrl)
|
|
@@ -19,43 +7,5 @@ export function validateProgrammaticFlags(flags) {
|
|
|
19
7
|
ux.error('Either Directus token or email and password are required for programmatic mode.');
|
|
20
8
|
if (!templateLocation)
|
|
21
9
|
ux.error('Template location is required for programmatic mode.');
|
|
22
|
-
return flags.partial ? handlePartialFlags(flags) : setAllFlagsTrue(flags);
|
|
23
|
-
}
|
|
24
|
-
export function validateInteractiveFlags(flags) {
|
|
25
|
-
return flags.partial ? handlePartialFlags(flags) : setAllFlagsTrue(flags);
|
|
26
|
-
}
|
|
27
|
-
function handlePartialFlags(flags) {
|
|
28
|
-
const enabledFlags = loadFlags.filter(flag => flags[flag] === true);
|
|
29
|
-
const disabledFlags = loadFlags.filter(flag => flags[flag] === false);
|
|
30
|
-
if (enabledFlags.length > 0) {
|
|
31
|
-
for (const flag of loadFlags)
|
|
32
|
-
flags[flag] = enabledFlags.includes(flag);
|
|
33
|
-
}
|
|
34
|
-
else if (disabledFlags.length > 0) {
|
|
35
|
-
for (const flag of loadFlags)
|
|
36
|
-
flags[flag] = !disabledFlags.includes(flag);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
setAllFlagsTrue(flags);
|
|
40
|
-
}
|
|
41
|
-
handleDependencies(flags);
|
|
42
|
-
if (!loadFlags.some(flag => flags[flag])) {
|
|
43
|
-
catchError(new Error('When using --partial, at least one component must be loaded.'), { fatal: true });
|
|
44
|
-
}
|
|
45
|
-
return flags;
|
|
46
|
-
}
|
|
47
|
-
function handleDependencies(flags) {
|
|
48
|
-
if (flags.content && (!flags.schema || !flags.files)) {
|
|
49
|
-
flags.schema = flags.files = true;
|
|
50
|
-
ux.warn('Content loading requires schema and files. Enabling schema and files flags.');
|
|
51
|
-
}
|
|
52
|
-
if (flags.users && !flags.permissions) {
|
|
53
|
-
flags.permissions = true;
|
|
54
|
-
ux.warn('User loading requires permissions. Enabling permissions flag.');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function setAllFlagsTrue(flags) {
|
|
58
|
-
for (const flag of loadFlags)
|
|
59
|
-
flags[flag] = true;
|
|
60
10
|
return flags;
|
|
61
11
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { updateCollection } from '@directus/sdk';
|
|
2
|
+
import { ux } from '@oclif/core';
|
|
3
|
+
import { DIRECTUS_PINK } from '../constants.js';
|
|
4
|
+
import { api } from '../sdk.js';
|
|
5
|
+
import { includesSchemaCollection } from '../template-plan/index.js';
|
|
6
|
+
import catchError from '../utils/catch-error.js';
|
|
7
|
+
import readFile from '../utils/read-file.js';
|
|
8
|
+
export default async function finalizeCollections(dir, plan) {
|
|
9
|
+
const collections = readFile('collections', dir)
|
|
10
|
+
.filter((collection) => includesSchemaCollection(collection.collection, plan))
|
|
11
|
+
.filter((collection) => !collection.collection.startsWith('directus_'));
|
|
12
|
+
ux.action.start(ux.colorize(DIRECTUS_PINK, `Finalizing metadata for ${collections.length} collections`));
|
|
13
|
+
const collectionNames = new Set(collections.map((collection) => collection.collection));
|
|
14
|
+
for await (const collection of collections) {
|
|
15
|
+
const meta = { ...collection.meta };
|
|
16
|
+
if (meta.group && !collectionNames.has(meta.group)) {
|
|
17
|
+
ux.warn(`Skipping missing group "${meta.group}" for collection "${collection.collection}"`);
|
|
18
|
+
delete meta.group;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
await api.client.request(updateCollection(collection.collection, { meta }));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
catchError(error, { context: { collection: collection.collection, group: meta.group } });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
ux.action.stop();
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { updateField } from '@directus/sdk';
|
|
2
|
+
import { ux } from '@oclif/core';
|
|
3
|
+
import { DIRECTUS_PINK } from '../constants.js';
|
|
4
|
+
import { api } from '../sdk.js';
|
|
5
|
+
import { includesSchemaCollection } from '../template-plan/index.js';
|
|
6
|
+
import catchError from '../utils/catch-error.js';
|
|
7
|
+
import readFile from '../utils/read-file.js';
|
|
8
|
+
export default async function finalizeFields(dir, plan) {
|
|
9
|
+
const fields = readFile('fields', dir)
|
|
10
|
+
.filter((field) => includesSchemaCollection(field.collection, plan))
|
|
11
|
+
.filter((field) => field.schema);
|
|
12
|
+
ux.action.start(ux.colorize(DIRECTUS_PINK, `Finalizing metadata for ${fields.length} fields`));
|
|
13
|
+
for await (const field of fields) {
|
|
14
|
+
try {
|
|
15
|
+
await api.client.request(updateField(field.collection, field.field, {
|
|
16
|
+
meta: field.meta ? { ...field.meta } : undefined,
|
|
17
|
+
schema: field.schema ? { ...field.schema } : undefined,
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
catchError(error, { context: { collection: field.collection, field: field.field } });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
ux.action.stop();
|
|
25
|
+
}
|