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
package/dist/lib/load/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ux } from '@oclif/core';
|
|
2
|
+
import { applyMetadataToPlan, buildTemplatePlan, readTemplateMetadata } from '../template-plan/index.js';
|
|
2
3
|
import checkTemplate from '../utils/check-template.js';
|
|
4
|
+
import finalizeCollections from './finalize-collections.js';
|
|
5
|
+
import finalizeFields from './finalize-fields.js';
|
|
3
6
|
import loadAccess from './load-access.js';
|
|
4
7
|
import loadCollections from './load-collections.js';
|
|
5
8
|
import loadDashboards from './load-dashboards.js';
|
|
@@ -16,48 +19,62 @@ import loadRoles from './load-roles.js';
|
|
|
16
19
|
import loadSettings from './load-settings.js';
|
|
17
20
|
import loadTranslations from './load-translations.js';
|
|
18
21
|
import loadUsers from './load-users.js';
|
|
19
|
-
import updateRequiredFields from './update-required-fields.js';
|
|
20
22
|
export default async function apply(dir, flags) {
|
|
21
23
|
const source = `${dir}/src`;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const metadata = readTemplateMetadata(source);
|
|
25
|
+
const requestedPlan = buildTemplatePlan(flags);
|
|
26
|
+
const effectivePlan = applyMetadataToPlan(requestedPlan, metadata);
|
|
27
|
+
const { components } = effectivePlan;
|
|
28
|
+
if (!metadata) {
|
|
29
|
+
ux.warn('No template-meta.json found. Treating as a full template — relation integrity check skipped.');
|
|
25
30
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
await loadRelations(source);
|
|
31
|
+
else if (metadata.partial) {
|
|
32
|
+
ux.warn('Template metadata indicates this is a partial template.');
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
const brokenRelationWarnings = metadata?.warnings?.filter((warning) => warning.type === 'excluded_relation') || [];
|
|
35
|
+
if (components.content && brokenRelationWarnings.length > 0 && !effectivePlan.allowBrokenRelations) {
|
|
36
|
+
ux.error('This partial template contains excluded relation references. Re-run with --allow-broken-relations to apply anyway.');
|
|
37
|
+
}
|
|
38
|
+
if (!metadata || components.schema) {
|
|
39
|
+
const isTemplateOk = await checkTemplate(source);
|
|
40
|
+
if (!isTemplateOk) {
|
|
41
|
+
ux.error('The template is missing the collections, fields, or relations files. Older templates are not supported in v0.4 of directus-template-cli. Try using v0.3 to load older templates npx directus-template-cli@0.3 apply or extract the template using latest version before applying. Exiting...');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (components.schema) {
|
|
45
|
+
await loadCollections(source, effectivePlan);
|
|
46
|
+
await loadRelations(source, effectivePlan);
|
|
47
|
+
await finalizeCollections(source, effectivePlan);
|
|
48
|
+
await finalizeFields(source, effectivePlan);
|
|
49
|
+
}
|
|
50
|
+
if (components.permissions || components.users) {
|
|
31
51
|
await loadRoles(source);
|
|
32
52
|
await loadPolicies(source);
|
|
33
53
|
await loadPermissions(source);
|
|
34
|
-
if (
|
|
54
|
+
if (components.users) {
|
|
35
55
|
await loadUsers(source);
|
|
36
56
|
}
|
|
37
57
|
await loadAccess(source);
|
|
38
58
|
}
|
|
39
|
-
if (
|
|
59
|
+
if (components.files) {
|
|
40
60
|
await loadFolders(source);
|
|
41
61
|
await loadFiles(source);
|
|
42
62
|
}
|
|
43
|
-
if (
|
|
44
|
-
await loadData(source);
|
|
45
|
-
}
|
|
46
|
-
if (flags.schema) {
|
|
47
|
-
await updateRequiredFields(source);
|
|
63
|
+
if (components.content) {
|
|
64
|
+
await loadData(source, effectivePlan);
|
|
48
65
|
}
|
|
49
|
-
if (
|
|
66
|
+
if (components.dashboards) {
|
|
50
67
|
await loadDashboards(source);
|
|
51
68
|
}
|
|
52
|
-
if (
|
|
69
|
+
if (components.flows) {
|
|
53
70
|
await loadFlows(source);
|
|
54
71
|
}
|
|
55
|
-
if (
|
|
72
|
+
if (components.settings) {
|
|
56
73
|
await loadSettings(source);
|
|
57
74
|
await loadTranslations(source);
|
|
58
75
|
await loadPresets(source);
|
|
59
76
|
}
|
|
60
|
-
if (
|
|
77
|
+
if (components.extensions) {
|
|
61
78
|
await loadExtensions(source);
|
|
62
79
|
}
|
|
63
80
|
return {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Load collections into the Directus instance
|
|
3
4
|
* @param dir - The directory to read the collections and fields from
|
|
4
5
|
* @returns {Promise<void>} - Returns nothing
|
|
5
6
|
*/
|
|
6
|
-
export default function loadCollections(dir: string): Promise<void>;
|
|
7
|
+
export default function loadCollections(dir: string, plan?: TemplatePlan): Promise<void>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createCollection, createField, readCollections, readFields
|
|
1
|
+
import { createCollection, createField, readCollections, 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 readFile from '../utils/read-file.js';
|
|
7
8
|
/**
|
|
@@ -9,12 +10,11 @@ import readFile from '../utils/read-file.js';
|
|
|
9
10
|
* @param dir - The directory to read the collections and fields from
|
|
10
11
|
* @returns {Promise<void>} - Returns nothing
|
|
11
12
|
*/
|
|
12
|
-
export default async function loadCollections(dir) {
|
|
13
|
-
const collectionsToAdd = readFile('collections', dir);
|
|
14
|
-
const fieldsToAdd = readFile('fields', dir);
|
|
13
|
+
export default async function loadCollections(dir, plan) {
|
|
14
|
+
const collectionsToAdd = readFile('collections', dir).filter((collection) => includesSchemaCollection(collection.collection, plan));
|
|
15
|
+
const fieldsToAdd = readFile('fields', dir).filter((field) => includesSchemaCollection(field.collection, plan));
|
|
15
16
|
ux.action.start(ux.colorize(DIRECTUS_PINK, `Loading ${collectionsToAdd.length} collections and ${fieldsToAdd.length} fields`));
|
|
16
17
|
await processCollections(collectionsToAdd, fieldsToAdd);
|
|
17
|
-
await updateCollections(collectionsToAdd);
|
|
18
18
|
await addCustomFieldsOnSystemCollections(fieldsToAdd);
|
|
19
19
|
ux.action.stop();
|
|
20
20
|
}
|
|
@@ -24,10 +24,12 @@ async function processCollections(collectionsToAdd, fieldsToAdd) {
|
|
|
24
24
|
for await (const collection of collectionsToAdd) {
|
|
25
25
|
try {
|
|
26
26
|
const existingCollection = existingCollections.find((c) => c.collection === collection.collection);
|
|
27
|
-
await (existingCollection
|
|
27
|
+
await (existingCollection
|
|
28
|
+
? addNewFieldsToExistingCollection(collection.collection, fieldsToAdd, existingFields)
|
|
29
|
+
: addNewCollectionWithFields(collection, fieldsToAdd));
|
|
28
30
|
}
|
|
29
31
|
catch (error) {
|
|
30
|
-
catchError(error);
|
|
32
|
+
catchError(error, { context: { collection: collection.collection } });
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -44,8 +46,9 @@ const removeRequiredorIsNullable = (field) => {
|
|
|
44
46
|
return field;
|
|
45
47
|
};
|
|
46
48
|
async function addNewCollectionWithFields(collection, allFields) {
|
|
47
|
-
const collectionFields = allFields
|
|
48
|
-
.
|
|
49
|
+
const collectionFields = allFields
|
|
50
|
+
.filter((field) => field.collection === collection.collection)
|
|
51
|
+
.map((field) => removeRequiredorIsNullable(field));
|
|
49
52
|
const collectionWithoutGroup = {
|
|
50
53
|
...collection,
|
|
51
54
|
fields: collectionFields,
|
|
@@ -55,8 +58,9 @@ async function addNewCollectionWithFields(collection, allFields) {
|
|
|
55
58
|
await api.client.request(createCollection(collectionWithoutGroup));
|
|
56
59
|
}
|
|
57
60
|
async function addNewFieldsToExistingCollection(collectionName, fieldsToAdd, existingFields) {
|
|
58
|
-
const collectionFieldsToAdd = fieldsToAdd
|
|
59
|
-
.
|
|
61
|
+
const collectionFieldsToAdd = fieldsToAdd
|
|
62
|
+
.filter((field) => field.collection === collectionName)
|
|
63
|
+
.map((field) => removeRequiredorIsNullable(field));
|
|
60
64
|
const existingCollectionFields = existingFields.filter((field) => field.collection === collectionName);
|
|
61
65
|
for await (const field of collectionFieldsToAdd) {
|
|
62
66
|
if (!existingCollectionFields.some((existingField) => existingField.field === field.field)) {
|
|
@@ -65,28 +69,11 @@ async function addNewFieldsToExistingCollection(collectionName, fieldsToAdd, exi
|
|
|
65
69
|
await api.client.request(createField(collectionName, field));
|
|
66
70
|
}
|
|
67
71
|
catch (error) {
|
|
68
|
-
catchError(error);
|
|
72
|
+
catchError(error, { context: { collection: collectionName, field: field.field } });
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
|
-
async function updateCollections(collections) {
|
|
74
|
-
for await (const collection of collections) {
|
|
75
|
-
try {
|
|
76
|
-
if (collection.meta.group) {
|
|
77
|
-
const pl = {
|
|
78
|
-
meta: {
|
|
79
|
-
group: collection.meta.group,
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
await api.client.request(updateCollection(collection.collection, pl));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
catchError(error);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
77
|
async function addCustomFieldsOnSystemCollections(fields) {
|
|
91
78
|
const customFields = fields.filter((field) => field.collection.startsWith('directus_'));
|
|
92
79
|
const existingFields = await api.client.request(readFields());
|
|
@@ -99,7 +86,7 @@ async function addCustomFieldsOnSystemCollections(fields) {
|
|
|
99
86
|
}
|
|
100
87
|
}
|
|
101
88
|
catch (error) {
|
|
102
|
-
catchError(error);
|
|
89
|
+
catchError(error, { context: { collection: field.collection, field: field.field } });
|
|
103
90
|
}
|
|
104
91
|
}
|
|
105
92
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
2
|
+
export default function loadData(dir: string, plan?: TemplatePlan): Promise<void>;
|
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
import { createItems, readItems, updateItemsBatch, updateSingleton } from '@directus/sdk';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import path from 'pathe';
|
|
4
5
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
5
6
|
import { api } from '../sdk.js';
|
|
7
|
+
import { getBrokenJunctionCollections, includesCollection } from '../template-plan/index.js';
|
|
6
8
|
import catchError from '../utils/catch-error.js';
|
|
7
9
|
import { chunkArray } from '../utils/chunk-array.js';
|
|
8
10
|
import readFile from '../utils/read-file.js';
|
|
9
11
|
const BATCH_SIZE = 50;
|
|
10
|
-
export default async function loadData(dir) {
|
|
11
|
-
const collections =
|
|
12
|
+
export default async function loadData(dir, plan) {
|
|
13
|
+
const collections = getUserCollections(dir, plan);
|
|
12
14
|
ux.action.start(ux.colorize(DIRECTUS_PINK, `Loading data for ${collections.length} collections`));
|
|
13
|
-
await loadSkeletonRecords(dir);
|
|
14
|
-
await loadFullData(dir);
|
|
15
|
-
await loadSingletons(dir);
|
|
15
|
+
await loadSkeletonRecords(dir, plan);
|
|
16
|
+
await loadFullData(dir, plan);
|
|
17
|
+
await loadSingletons(dir, plan);
|
|
16
18
|
ux.action.stop();
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
function getContentCollections(dir) {
|
|
21
|
+
const contentDir = path.resolve(dir, 'content');
|
|
22
|
+
if (!fs.existsSync(contentDir))
|
|
23
|
+
return new Set();
|
|
24
|
+
return new Set(fs
|
|
25
|
+
.readdirSync(contentDir)
|
|
26
|
+
.filter((file) => file.endsWith('.json'))
|
|
27
|
+
.map((file) => path.basename(file, '.json')));
|
|
28
|
+
}
|
|
29
|
+
function getUserCollections(dir, plan) {
|
|
30
|
+
const contentCollections = getContentCollections(dir);
|
|
20
31
|
const collections = readFile('collections', dir);
|
|
32
|
+
const relations = plan?.partial ? readFile('relations', dir) : [];
|
|
33
|
+
const brokenJunctions = getBrokenJunctionCollections(relations, plan);
|
|
34
|
+
if (brokenJunctions.size > 0) {
|
|
35
|
+
ux.warn(`Skipping junction collections with excluded FK targets: ${[...brokenJunctions].join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
return collections
|
|
38
|
+
.filter((item) => contentCollections.has(item.collection))
|
|
39
|
+
.filter((item) => !item.collection.startsWith('directus_', 0))
|
|
40
|
+
.filter((item) => item.schema !== null)
|
|
41
|
+
.filter((item) => includesCollection(item.collection, plan))
|
|
42
|
+
.filter((item) => !brokenJunctions.has(item.collection));
|
|
43
|
+
}
|
|
44
|
+
async function loadSkeletonRecords(dir, plan) {
|
|
45
|
+
ux.action.status = 'Loading skeleton records';
|
|
21
46
|
const primaryKeyMap = await getCollectionPrimaryKeys(dir);
|
|
22
|
-
const userCollections =
|
|
23
|
-
.filter(item => !item.collection.startsWith('directus_', 0))
|
|
24
|
-
.filter(item => item.schema !== null)
|
|
25
|
-
.filter(item => !item.meta.singleton);
|
|
47
|
+
const userCollections = getUserCollections(dir, plan).filter((item) => !item.meta.singleton);
|
|
26
48
|
await Promise.all(userCollections.map(async (collection) => {
|
|
27
49
|
const name = collection.collection;
|
|
28
50
|
const primaryKeyField = getPrimaryKey(primaryKeyMap, name);
|
|
@@ -31,14 +53,11 @@ async function loadSkeletonRecords(dir) {
|
|
|
31
53
|
// Fetch existing primary keys
|
|
32
54
|
const existingPrimaryKeys = await getExistingPrimaryKeys(name, primaryKeyField);
|
|
33
55
|
// Filter out existing records
|
|
34
|
-
const newData = data.filter(entry => !existingPrimaryKeys.has(entry[primaryKeyField]));
|
|
35
|
-
if (newData.length === 0)
|
|
36
|
-
// ux.stdout(`${ux.colorize('dim', '--')} Skipping ${name}: No new records to add`)
|
|
56
|
+
const newData = data.filter((entry) => !existingPrimaryKeys.has(entry[primaryKeyField]));
|
|
57
|
+
if (newData.length === 0)
|
|
37
58
|
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await Promise.all(batches.map(batch => uploadBatch(name, batch, createItems)));
|
|
41
|
-
// ux.stdout(`${ux.colorize('dim', '--')} Added ${newData.length} new skeleton records to ${name}`)
|
|
59
|
+
const batches = chunkArray(newData, BATCH_SIZE).map((batch) => batch.map((entry) => ({ [primaryKeyField]: entry[primaryKeyField] })));
|
|
60
|
+
await Promise.all(batches.map((batch) => uploadBatch(name, batch, createItems)));
|
|
42
61
|
}));
|
|
43
62
|
ux.action.status = 'Loaded skeleton records';
|
|
44
63
|
}
|
|
@@ -63,7 +82,7 @@ async function getExistingPrimaryKeys(collection, primaryKeyField) {
|
|
|
63
82
|
page++;
|
|
64
83
|
}
|
|
65
84
|
catch (error) {
|
|
66
|
-
catchError(error);
|
|
85
|
+
catchError(error, { context: { collection, page } });
|
|
67
86
|
break;
|
|
68
87
|
}
|
|
69
88
|
}
|
|
@@ -74,31 +93,24 @@ async function uploadBatch(collection, batch, method) {
|
|
|
74
93
|
await api.client.request(method(collection, batch));
|
|
75
94
|
}
|
|
76
95
|
catch (error) {
|
|
77
|
-
catchError(error);
|
|
96
|
+
catchError(error, { context: { batchSize: batch.length, collection } });
|
|
78
97
|
}
|
|
79
98
|
}
|
|
80
|
-
async function loadFullData(dir) {
|
|
99
|
+
async function loadFullData(dir, plan) {
|
|
81
100
|
ux.action.status = 'Updating records with full data';
|
|
82
|
-
const
|
|
83
|
-
const userCollections = collections
|
|
84
|
-
.filter(item => !item.collection.startsWith('directus_', 0))
|
|
85
|
-
.filter(item => item.schema !== null)
|
|
86
|
-
.filter(item => !item.meta.singleton);
|
|
101
|
+
const userCollections = getUserCollections(dir, plan).filter((item) => !item.meta.singleton);
|
|
87
102
|
await Promise.all(userCollections.map(async (collection) => {
|
|
88
103
|
const name = collection.collection;
|
|
89
104
|
const sourceDir = path.resolve(dir, 'content');
|
|
90
105
|
const data = readFile(name, sourceDir);
|
|
91
|
-
const batches = chunkArray(data, BATCH_SIZE).map(batch => batch.map(({ user_created, user_updated, ...cleanedRow }) => cleanedRow));
|
|
92
|
-
await Promise.all(batches.map(batch => uploadBatch(name, batch, updateItemsBatch)));
|
|
106
|
+
const batches = chunkArray(data, BATCH_SIZE).map((batch) => batch.map(({ user_created, user_updated, ...cleanedRow }) => cleanedRow));
|
|
107
|
+
await Promise.all(batches.map((batch) => uploadBatch(name, batch, updateItemsBatch)));
|
|
93
108
|
}));
|
|
94
109
|
ux.action.status = 'Updated records with full data';
|
|
95
110
|
}
|
|
96
|
-
async function loadSingletons(dir) {
|
|
111
|
+
async function loadSingletons(dir, plan) {
|
|
97
112
|
ux.action.status = 'Loading data for singleton collections';
|
|
98
|
-
const
|
|
99
|
-
const singletonCollections = collections
|
|
100
|
-
.filter(item => !item.collection.startsWith('directus_', 0))
|
|
101
|
-
.filter(item => item.meta.singleton);
|
|
113
|
+
const singletonCollections = getUserCollections(dir, plan).filter((item) => item.meta.singleton);
|
|
102
114
|
await Promise.all(singletonCollections.map(async (collection) => {
|
|
103
115
|
const name = collection.collection;
|
|
104
116
|
const sourceDir = path.resolve(dir, 'content');
|
|
@@ -108,7 +120,7 @@ async function loadSingletons(dir) {
|
|
|
108
120
|
await api.client.request(updateSingleton(name, cleanedData));
|
|
109
121
|
}
|
|
110
122
|
catch (error) {
|
|
111
|
-
catchError(error);
|
|
123
|
+
catchError(error, { context: { collection: name } });
|
|
112
124
|
}
|
|
113
125
|
}));
|
|
114
126
|
ux.action.status = 'Loaded data for singleton collections';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFiles, uploadFiles } from '@directus/sdk';
|
|
2
2
|
import { ux } from '@oclif/core';
|
|
3
|
-
import {
|
|
3
|
+
import { File } from 'node:buffer';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import path from 'pathe';
|
|
6
6
|
import { DIRECTUS_PINK } from '../constants.js';
|
|
@@ -17,9 +17,9 @@ export default async function loadFiles(dir) {
|
|
|
17
17
|
fields: ['id', 'filename_disk'],
|
|
18
18
|
limit: -1,
|
|
19
19
|
}));
|
|
20
|
-
const existingFileIds = new Set(existingFiles.map(file => file.id));
|
|
21
|
-
const existingFileNames = new Set(existingFiles.map(file => file.filename_disk));
|
|
22
|
-
const filesToUpload = files.filter(file => {
|
|
20
|
+
const existingFileIds = new Set(existingFiles.map((file) => file.id));
|
|
21
|
+
const existingFileNames = new Set(existingFiles.map((file) => file.filename_disk));
|
|
22
|
+
const filesToUpload = files.filter((file) => {
|
|
23
23
|
if (existingFileIds.has(file.id)) {
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
@@ -31,7 +31,8 @@ export default async function loadFiles(dir) {
|
|
|
31
31
|
await Promise.all(filesToUpload.map(async (asset) => {
|
|
32
32
|
const fileName = asset.filename_disk;
|
|
33
33
|
const assetPath = path.resolve(dir, 'assets', fileName);
|
|
34
|
-
const
|
|
34
|
+
const mimeType = asset.type || 'application/octet-stream';
|
|
35
|
+
const file = new File([readFileSync(assetPath)], fileName, { type: mimeType });
|
|
35
36
|
const form = new FormData();
|
|
36
37
|
form.append('id', asset.id);
|
|
37
38
|
if (asset.title)
|
|
@@ -40,9 +41,8 @@ export default async function loadFiles(dir) {
|
|
|
40
41
|
form.append('description', asset.description);
|
|
41
42
|
if (asset.folder)
|
|
42
43
|
form.append('folder', asset.folder);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
form.append('file', fileStream, fileName);
|
|
44
|
+
form.append('type', mimeType);
|
|
45
|
+
form.append('file', file);
|
|
46
46
|
try {
|
|
47
47
|
await api.client.request(uploadFiles(form));
|
|
48
48
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type TemplatePlan } from '../template-plan/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Load relationships into the Directus instance
|
|
3
4
|
* @param dir - The directory to read the relations from
|
|
4
5
|
* @returns {Promise<void>} - Returns nothing
|
|
5
6
|
*/
|
|
6
|
-
export default function loadRelations(dir: string): Promise<void>;
|
|
7
|
+
export default function loadRelations(dir: string, plan?: TemplatePlan): Promise<void>;
|
|
@@ -2,6 +2,7 @@ import { createRelation, 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 readFile from '../utils/read-file.js';
|
|
7
8
|
/**
|
|
@@ -9,20 +10,22 @@ import readFile from '../utils/read-file.js';
|
|
|
9
10
|
* @param dir - The directory to read the relations from
|
|
10
11
|
* @returns {Promise<void>} - Returns nothing
|
|
11
12
|
*/
|
|
12
|
-
export default async function loadRelations(dir) {
|
|
13
|
-
const relations = readFile('relations', dir);
|
|
13
|
+
export default async function loadRelations(dir, plan) {
|
|
14
|
+
const relations = readFile('relations', dir).filter((relation) => includesRelation(relation.collection, relation.related_collection, plan));
|
|
14
15
|
ux.action.start(ux.colorize(DIRECTUS_PINK, `Loading ${relations.length} relations`));
|
|
15
16
|
if (relations && relations.length > 0) {
|
|
16
17
|
// Fetch existing relations
|
|
17
18
|
const existingRelations = await api.client.request(readRelations());
|
|
18
|
-
const existingRelationKeys = new Set(existingRelations.map(relation => `${relation.collection}:${relation.field}:${relation.related_collection}`));
|
|
19
|
-
const relationsToAdd = relations
|
|
19
|
+
const existingRelationKeys = new Set(existingRelations.map((relation) => `${relation.collection}:${relation.field}:${relation.related_collection}`));
|
|
20
|
+
const relationsToAdd = relations
|
|
21
|
+
.filter((relation) => {
|
|
20
22
|
const key = `${relation.collection}:${relation.field}:${relation.related_collection}`;
|
|
21
23
|
if (existingRelationKeys.has(key)) {
|
|
22
24
|
return false;
|
|
23
25
|
}
|
|
24
26
|
return true;
|
|
25
|
-
})
|
|
27
|
+
})
|
|
28
|
+
.map((relation) => {
|
|
26
29
|
const cleanRelation = { ...relation };
|
|
27
30
|
cleanRelation.meta.id = undefined;
|
|
28
31
|
return cleanRelation;
|
|
@@ -32,12 +35,19 @@ export default async function loadRelations(dir) {
|
|
|
32
35
|
ux.action.stop();
|
|
33
36
|
}
|
|
34
37
|
async function addRelations(relations) {
|
|
35
|
-
for
|
|
38
|
+
for (const relation of relations) {
|
|
36
39
|
try {
|
|
40
|
+
// eslint-disable-next-line no-await-in-loop
|
|
37
41
|
await api.client.request(createRelation(relation));
|
|
38
42
|
}
|
|
39
43
|
catch (error) {
|
|
40
|
-
catchError(error
|
|
44
|
+
catchError(error, {
|
|
45
|
+
context: {
|
|
46
|
+
collection: relation.collection,
|
|
47
|
+
field: relation.field,
|
|
48
|
+
related: relation.related_collection,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TemplatePlan } from './types.js';
|
|
2
|
+
export declare function includesCollection(collection: string, plan?: TemplatePlan): boolean;
|
|
3
|
+
export declare function includesSchemaCollection(collection: string, plan?: TemplatePlan): boolean;
|
|
4
|
+
export declare function includesRelation(collection: string, relatedCollection?: null | string, plan?: TemplatePlan): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function includesCollection(collection, plan) {
|
|
2
|
+
if (plan?.collections && !plan.collections.includes(collection))
|
|
3
|
+
return false;
|
|
4
|
+
if (plan?.excludeCollections?.includes(collection))
|
|
5
|
+
return false;
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
export function includesSchemaCollection(collection, plan) {
|
|
9
|
+
const schemaCollections = plan?.schemaCollections || plan?.collections;
|
|
10
|
+
if (schemaCollections && !schemaCollections.includes(collection))
|
|
11
|
+
return false;
|
|
12
|
+
if (plan?.excludeCollections?.includes(collection))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
export function includesRelation(collection, relatedCollection, plan) {
|
|
17
|
+
if (!includesSchemaCollection(collection, plan))
|
|
18
|
+
return false;
|
|
19
|
+
if (!relatedCollection)
|
|
20
|
+
return true;
|
|
21
|
+
if (plan?.excludeCollections?.includes(relatedCollection))
|
|
22
|
+
return plan.relationStrategy === 'preserve';
|
|
23
|
+
if (relatedCollection.startsWith('directus_'))
|
|
24
|
+
return true;
|
|
25
|
+
return includesSchemaCollection(relatedCollection, plan);
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const componentNames: readonly ["schema", "content", "files", "flows", "dashboards", "permissions", "settings", "extensions", "users"];
|
|
2
|
+
export declare const partial: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
3
|
+
export declare const componentFlags: {
|
|
4
|
+
content: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
5
|
+
dashboards: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
extensions: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
files: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
flows: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
permissions: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
schema: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
settings: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
users: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
export declare const collections: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
export declare const excludeCollections: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
export declare const relationStrategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
export declare const allowBrokenRelations: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
export declare const noAssets: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
export const componentNames = [
|
|
3
|
+
'schema',
|
|
4
|
+
'content',
|
|
5
|
+
'files',
|
|
6
|
+
'flows',
|
|
7
|
+
'dashboards',
|
|
8
|
+
'permissions',
|
|
9
|
+
'settings',
|
|
10
|
+
'extensions',
|
|
11
|
+
'users',
|
|
12
|
+
];
|
|
13
|
+
export const partial = Flags.boolean({
|
|
14
|
+
description: 'Enable partial template mode',
|
|
15
|
+
summary: 'Enable partial template mode',
|
|
16
|
+
});
|
|
17
|
+
export const componentFlags = {
|
|
18
|
+
content: Flags.boolean({ allowNo: true, default: undefined, description: 'Include content/data' }),
|
|
19
|
+
dashboards: Flags.boolean({ allowNo: true, default: undefined, description: 'Include dashboards and panels' }),
|
|
20
|
+
extensions: Flags.boolean({ allowNo: true, default: undefined, description: 'Include extensions' }),
|
|
21
|
+
files: Flags.boolean({ allowNo: true, default: undefined, description: 'Include files, folders, and assets' }),
|
|
22
|
+
flows: Flags.boolean({ allowNo: true, default: undefined, description: 'Include flows and operations' }),
|
|
23
|
+
permissions: Flags.boolean({
|
|
24
|
+
allowNo: true,
|
|
25
|
+
default: undefined,
|
|
26
|
+
description: 'Include permissions, roles, policies, and access',
|
|
27
|
+
}),
|
|
28
|
+
schema: Flags.boolean({
|
|
29
|
+
allowNo: true,
|
|
30
|
+
default: undefined,
|
|
31
|
+
description: 'Include schema, collections, fields, and relations',
|
|
32
|
+
}),
|
|
33
|
+
settings: Flags.boolean({
|
|
34
|
+
allowNo: true,
|
|
35
|
+
default: undefined,
|
|
36
|
+
description: 'Include settings, translations, and presets',
|
|
37
|
+
}),
|
|
38
|
+
users: Flags.boolean({ allowNo: true, default: undefined, description: 'Include users' }),
|
|
39
|
+
};
|
|
40
|
+
export const collections = Flags.string({
|
|
41
|
+
description: 'Only include these comma-separated collections',
|
|
42
|
+
});
|
|
43
|
+
export const excludeCollections = Flags.string({
|
|
44
|
+
aliases: ['exclude-collections'],
|
|
45
|
+
description: 'Exclude these comma-separated collections',
|
|
46
|
+
});
|
|
47
|
+
export const relationStrategy = Flags.string({
|
|
48
|
+
aliases: ['relation-strategy'],
|
|
49
|
+
description: 'How to handle relations to omitted data',
|
|
50
|
+
options: ['empty', 'preserve', 'deep'],
|
|
51
|
+
});
|
|
52
|
+
export const allowBrokenRelations = Flags.boolean({
|
|
53
|
+
aliases: ['allow-broken-relations'],
|
|
54
|
+
default: false,
|
|
55
|
+
description: 'Allow intentionally incomplete relation references',
|
|
56
|
+
});
|
|
57
|
+
export const noAssets = Flags.boolean({
|
|
58
|
+
aliases: ['no-assets'],
|
|
59
|
+
default: undefined,
|
|
60
|
+
description: 'Shorthand for --no-files and --exclude-collections directus_files',
|
|
61
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TemplateComponent, TemplatePlan } from './types.js';
|
|
2
|
+
type BuildFlags = Partial<Record<TemplateComponent, boolean>> & {
|
|
3
|
+
allowBrokenRelations?: boolean;
|
|
4
|
+
collections?: string | string[];
|
|
5
|
+
excludeCollections?: string | string[];
|
|
6
|
+
noAssets?: boolean;
|
|
7
|
+
partial?: boolean;
|
|
8
|
+
relationStrategy?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function buildTemplatePlan(flags?: BuildFlags): TemplatePlan;
|
|
11
|
+
export * from './collections.js';
|
|
12
|
+
export * from './flags.js';
|
|
13
|
+
export * from './junctions.js';
|
|
14
|
+
export * from './metadata-plan.js';
|
|
15
|
+
export * from './metadata.js';
|
|
16
|
+
export * from './types.js';
|