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.
Files changed (53) hide show
  1. package/README.md +134 -37
  2. package/dist/commands/apply.d.ts +5 -0
  3. package/dist/commands/apply.js +32 -68
  4. package/dist/commands/extract.d.ts +30 -0
  5. package/dist/commands/extract.js +14 -5
  6. package/dist/lib/extract/expand-deep-plan.d.ts +2 -0
  7. package/dist/lib/extract/expand-deep-plan.js +54 -0
  8. package/dist/lib/extract/expand-schema-plan.d.ts +2 -0
  9. package/dist/lib/extract/expand-schema-plan.js +55 -0
  10. package/dist/lib/extract/extract-assets.js +19 -6
  11. package/dist/lib/extract/extract-collections.d.ts +2 -1
  12. package/dist/lib/extract/extract-collections.js +5 -2
  13. package/dist/lib/extract/extract-content.d.ts +2 -1
  14. package/dist/lib/extract/extract-content.js +105 -12
  15. package/dist/lib/extract/extract-fields.d.ts +2 -1
  16. package/dist/lib/extract/extract-fields.js +5 -5
  17. package/dist/lib/extract/extract-relations.d.ts +2 -1
  18. package/dist/lib/extract/extract-relations.js +6 -4
  19. package/dist/lib/extract/index.d.ts +2 -1
  20. package/dist/lib/extract/index.js +67 -29
  21. package/dist/lib/load/apply-flags.d.ts +5 -2
  22. package/dist/lib/load/apply-flags.js +0 -50
  23. package/dist/lib/load/finalize-collections.d.ts +2 -0
  24. package/dist/lib/load/finalize-collections.js +28 -0
  25. package/dist/lib/load/finalize-fields.d.ts +2 -0
  26. package/dist/lib/load/finalize-fields.js +25 -0
  27. package/dist/lib/load/index.js +36 -19
  28. package/dist/lib/load/load-collections.d.ts +2 -1
  29. package/dist/lib/load/load-collections.js +17 -30
  30. package/dist/lib/load/load-data.d.ts +2 -1
  31. package/dist/lib/load/load-data.js +46 -34
  32. package/dist/lib/load/load-files.js +8 -8
  33. package/dist/lib/load/load-relations.d.ts +2 -1
  34. package/dist/lib/load/load-relations.js +17 -7
  35. package/dist/lib/template-plan/collections.d.ts +4 -0
  36. package/dist/lib/template-plan/collections.js +26 -0
  37. package/dist/lib/template-plan/flags.d.ts +18 -0
  38. package/dist/lib/template-plan/flags.js +61 -0
  39. package/dist/lib/template-plan/index.d.ts +16 -0
  40. package/dist/lib/template-plan/index.js +77 -0
  41. package/dist/lib/template-plan/junctions.d.ts +10 -0
  42. package/dist/lib/template-plan/junctions.js +19 -0
  43. package/dist/lib/template-plan/metadata-plan.d.ts +2 -0
  44. package/dist/lib/template-plan/metadata-plan.js +33 -0
  45. package/dist/lib/template-plan/metadata.d.ts +5 -0
  46. package/dist/lib/template-plan/metadata.js +39 -0
  47. package/dist/lib/template-plan/types.d.ts +34 -0
  48. package/dist/lib/template-plan/types.js +1 -0
  49. package/dist/services/github.js +1 -1
  50. package/oclif.manifest.json +173 -16
  51. package/package.json +1 -2
  52. package/dist/lib/load/update-required-fields.d.ts +0 -1
  53. package/dist/lib/load/update-required-fields.js +0 -20
@@ -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 isTemplateOk = await checkTemplate(source);
23
- if (!isTemplateOk) {
24
- 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...');
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 (flags.schema) {
27
- await loadCollections(source);
28
- await loadRelations(source);
31
+ else if (metadata.partial) {
32
+ ux.warn('Template metadata indicates this is a partial template.');
29
33
  }
30
- if (flags.permissions || flags.users) {
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 (flags.users) {
54
+ if (components.users) {
35
55
  await loadUsers(source);
36
56
  }
37
57
  await loadAccess(source);
38
58
  }
39
- if (flags.files) {
59
+ if (components.files) {
40
60
  await loadFolders(source);
41
61
  await loadFiles(source);
42
62
  }
43
- if (flags.content) {
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 (flags.dashboards) {
66
+ if (components.dashboards) {
50
67
  await loadDashboards(source);
51
68
  }
52
- if (flags.flows) {
69
+ if (components.flows) {
53
70
  await loadFlows(source);
54
71
  }
55
- if (flags.settings) {
72
+ if (components.settings) {
56
73
  await loadSettings(source);
57
74
  await loadTranslations(source);
58
75
  await loadPresets(source);
59
76
  }
60
- if (flags.extensions) {
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, updateCollection, } from '@directus/sdk';
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 ? addNewFieldsToExistingCollection(collection.collection, fieldsToAdd, existingFields) : addNewCollectionWithFields(collection, fieldsToAdd));
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.filter(field => field.collection === collection.collection)
48
- .map(field => removeRequiredorIsNullable(field));
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.filter(field => field.collection === collectionName)
59
- .map(field => removeRequiredorIsNullable(field));
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
- export default function loadData(dir: string): Promise<void>;
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 = readFile('collections', dir);
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
- async function loadSkeletonRecords(dir) {
19
- ux.action.status = 'Loading skeleton records';
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 = collections
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
- const batches = chunkArray(newData, BATCH_SIZE).map(batch => batch.map(entry => ({ [primaryKeyField]: entry[primaryKeyField] })));
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 collections = readFile('collections', dir);
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 collections = readFile('collections', dir);
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 { FormData } from 'formdata-node';
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 fileStream = new Blob([readFileSync(assetPath)], { type: asset.type });
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
- if (asset.type)
44
- form.append('type', asset.type);
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.filter(relation => {
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
- }).map(relation => {
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 await (const relation of relations) {
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';