Skip to content

Commit 39bdddf

Browse files
authored
feat(content-item): add tree command, improved circular dependency import (#91)
This command allows you to print a dependency tree for any folder of content items on your system. The input directory should contain content items in the same format that the export command generates. It does not require repo/folder layouts, just that all dependent content is contained within the parent directory. dc-cli content-item tree <dir> This uses the same dependency tree class as the import task, which uses it to import the deepest content items first so that references can be properly resolved. Here, the "levels" of content items are maintained, and the highest level (most layers of dependencies) is printed first, followed by the lower levels that have not been printed yet.
1 parent 0d6c43c commit 39bdddf

8 files changed

Lines changed: 819 additions & 26 deletions

File tree

src/commands/configure.spec.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('configure command', function() {
3030
jest.spyOn(fs, 'mkdirSync').mockReturnValueOnce(undefined);
3131
jest.spyOn(fs, 'writeFileSync').mockReturnValueOnce(undefined);
3232

33-
handler({ ...yargArgs, ...configFixture });
33+
handler({ ...yargArgs, ...configFixture, config: CONFIG_FILENAME() });
3434

3535
expect(fs.existsSync).toHaveBeenCalledWith(expect.stringMatching(/\.amplience$/));
3636
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.stringMatching(/\.amplience$/), { recursive: true });
@@ -48,7 +48,7 @@ describe('configure command', function() {
4848
jest.spyOn(fs, 'mkdirSync');
4949
jest.spyOn(fs, 'writeFileSync').mockReturnValueOnce(undefined);
5050

51-
handler({ ...yargArgs, ...configFixture });
51+
handler({ ...yargArgs, ...configFixture, config: CONFIG_FILENAME() });
5252

5353
expect(fs.existsSync).toHaveBeenCalledWith(expect.stringMatching(/\.amplience$/));
5454
expect(fs.mkdirSync).not.toHaveBeenCalled();
@@ -58,6 +58,24 @@ describe('configure command', function() {
5858
);
5959
});
6060

61+
it('should write a config file and use the specified file', () => {
62+
jest
63+
.spyOn(fs, 'existsSync')
64+
.mockReturnValueOnce(false)
65+
.mockReturnValueOnce(false);
66+
jest.spyOn(fs, 'mkdirSync').mockReturnValueOnce(undefined);
67+
jest.spyOn(fs, 'writeFileSync').mockReturnValueOnce(undefined);
68+
69+
handler({ ...yargArgs, ...configFixture, config: 'subdirectory/custom-config.json' });
70+
71+
expect(fs.existsSync).toHaveBeenCalledWith(expect.stringMatching(/subdirectory$/));
72+
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.stringMatching(/subdirectory$/), { recursive: true });
73+
expect(fs.writeFileSync).toHaveBeenCalledWith(
74+
expect.stringMatching(new RegExp('subdirectory/custom-config.json$')),
75+
JSON.stringify(configFixture)
76+
);
77+
});
78+
6179
it('should report an error if its not possible to create the .amplience dir', () => {
6280
jest
6381
.spyOn(fs, 'existsSync')
@@ -69,7 +87,7 @@ describe('configure command', function() {
6987
jest.spyOn(fs, 'writeFileSync').mockReturnValueOnce(undefined);
7088

7189
expect(() => {
72-
handler({ ...yargArgs, ...configFixture });
90+
handler({ ...yargArgs, ...configFixture, config: CONFIG_FILENAME() });
7391
}).toThrowError(/^Unable to create dir ".*". Reason: .*/);
7492

7593
expect(fs.existsSync).toHaveBeenCalledWith(expect.stringMatching(/\.amplience$/));
@@ -88,7 +106,7 @@ describe('configure command', function() {
88106
});
89107

90108
expect(() => {
91-
handler({ ...yargArgs, ...configFixture });
109+
handler({ ...yargArgs, ...configFixture, config: CONFIG_FILENAME() });
92110
}).toThrowError(/^Unable to write config file ".*". Reason: .*/);
93111

94112
expect(fs.existsSync).toHaveBeenCalledWith(expect.stringMatching(/\.amplience$/));
@@ -104,7 +122,7 @@ describe('configure command', function() {
104122
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(JSON.stringify(configFixture));
105123
jest.spyOn(fs, 'writeFileSync');
106124

107-
handler({ ...yargArgs, ...configFixture });
125+
handler({ ...yargArgs, ...configFixture, config: CONFIG_FILENAME() });
108126

109127
expect(fs.writeFileSync).not.toHaveBeenCalled();
110128
});

src/commands/configure.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export type ConfigurationParameters = {
1717
hubId: string;
1818
};
1919

20+
type ConfigArgument = {
21+
config: string;
22+
};
23+
2024
export const configureCommandOptions: CommandOptions = {
2125
clientId: { type: 'string', demandOption: true },
2226
clientSecret: { type: 'string', demandOption: true },
@@ -43,14 +47,14 @@ const writeConfigFile = (configFile: string, parameters: ConfigurationParameters
4347
export const readConfigFile = (configFile: string): object =>
4448
fs.existsSync(configFile) ? JSON.parse(fs.readFileSync(configFile, 'utf-8')) : {};
4549

46-
export const handler = (argv: Arguments<ConfigurationParameters>): void => {
50+
export const handler = (argv: Arguments<ConfigurationParameters & ConfigArgument>): void => {
4751
const { clientId, clientSecret, hubId } = argv;
48-
const storedConfig = readConfigFile(CONFIG_FILENAME());
52+
const storedConfig = readConfigFile(argv.config);
4953

5054
if (isEqual(storedConfig, { clientId, clientSecret, hubId })) {
5155
console.log('Config file up-to-date. Please use `--help` for command usage.');
5256
return;
5357
}
54-
writeConfigFile(CONFIG_FILENAME(), { clientId, clientSecret, hubId });
58+
writeConfigFile(argv.config, { clientId, clientSecret, hubId });
5559
console.log('Config file updated.');
5660
};

src/commands/content-item/__mocks__/dependant-content-helper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { ContentDependancy } from '../../../common/content-item/content-dependan
33
function dependancy(id: string): ContentDependancy {
44
return {
55
_meta: {
6-
schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/content-link'
6+
schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/content-link',
7+
name: 'content-link'
78
},
89
contentType: 'https://dev-solutions.s3.amazonaws.com/DynamicContentTypes/Accelerators/blog.json',
910
id: id
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`content-item tree command handler tests should detect and print circular dependencies with a double line indicator 1`] = `
4+
"=== LEVEL 2 (1) ===
5+
item6
6+
└─ item5
7+
8+
=== LEVEL 1 (3) ===
9+
item3
10+
11+
item7
12+
13+
=== CIRCULAR (3) ===
14+
item1 ═════════════════╗
15+
├─ item2 ║
16+
│ └─ item4 ║
17+
│ └─ *** (item1) ══╝
18+
└─ (item3)
19+
20+
Finished. Circular Dependencies printed: 1"
21+
`;
22+
23+
exports[`content-item tree command handler tests should detect intertwined circular dependencies with multiple lines with different position 1`] = `
24+
"=== CIRCULAR (6) ===
25+
item5 ══════════════╗
26+
└─ item6 ║
27+
└─ *** (item5) ══╝
28+
29+
item1 ══════════════════════╗
30+
└─ item2 ═════════════════╗ ║
31+
└─ item3 ║ ║
32+
├─ *** (item2) ═════╝ ║
33+
└─ item4 ║
34+
├─ *** (item1) ════╝
35+
└─ (item5)
36+
37+
Finished. Circular Dependencies printed: 2"
38+
`;
39+
40+
exports[`content-item tree command handler tests should print a single content item by itself 1`] = `
41+
"=== LEVEL 1 (1) ===
42+
item1
43+
44+
Finished. Circular Dependencies printed: 0"
45+
`;
46+
47+
exports[`content-item tree command handler tests should print a tree of content items 1`] = `
48+
"=== LEVEL 4 (1) ===
49+
item1
50+
├─ item2
51+
│ ├─ item4
52+
│ └─ item6
53+
│ └─ item5
54+
└─ item3
55+
56+
=== LEVEL 3 (1) ===
57+
=== LEVEL 2 (1) ===
58+
=== LEVEL 1 (3) ===
59+
Finished. Circular Dependencies printed: 0"
60+
`;
61+
62+
exports[`content-item tree command handler tests should print an error when invalid json is found 1`] = `
63+
"=== LEVEL 1 (1) ===
64+
item1
65+
66+
Finished. Circular Dependencies printed: 0"
67+
`;
68+
69+
exports[`content-item tree command handler tests should print multiple disjoint trees of content items 1`] = `
70+
"=== LEVEL 3 (1) ===
71+
item1
72+
├─ item2
73+
│ └─ item4
74+
└─ item3
75+
76+
=== LEVEL 2 (2) ===
77+
item6
78+
└─ item5
79+
80+
=== LEVEL 1 (4) ===
81+
item7
82+
83+
Finished. Circular Dependencies printed: 0"
84+
`;
85+
86+
exports[`content-item tree command handler tests should print nothing if no content is present 1`] = `"Finished. Circular Dependencies printed: 0"`;

src/commands/content-item/import.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
ItemContentDependancies,
2727
ContentDependancyInfo
2828
} from '../../common/content-item/content-dependancy-tree';
29+
import { Body } from '../../common/content-item/body';
2930

3031
import { AmplienceSchemaValidator, defaultSchemaLookup } from '../../common/content-item/amplience-schema-validator';
3132
import { createLog, getDefaultLogPath } from '../../common/log-helpers';
@@ -675,12 +676,23 @@ const prepareContentForImport = async (
675676
return tree;
676677
};
677678

678-
const rewriteDependancy = (dep: ContentDependancyInfo, mapping: ContentMapping): void => {
679-
const id = mapping.getContentItem(dep.dependancy.id) || dep.dependancy.id;
679+
const rewriteDependancy = (dep: ContentDependancyInfo, mapping: ContentMapping, allowNull: boolean): void => {
680+
let id = mapping.getContentItem(dep.dependancy.id);
681+
682+
if (id == null && !allowNull) {
683+
id = dep.dependancy.id;
684+
}
685+
680686
if (dep.dependancy._meta.schema === '_hierarchy') {
681687
dep.owner.content.body._meta.hierarchy.parentId = id;
682-
} else {
683-
dep.dependancy.id = id;
688+
} else if (dep.parent) {
689+
const parent = dep.parent as Body;
690+
if (id == null) {
691+
delete parent[dep.index];
692+
} else {
693+
parent[dep.index] = dep.dependancy;
694+
dep.dependancy.id = id;
695+
}
684696
}
685697
};
686698

@@ -706,7 +718,7 @@ const importTree = async (
706718

707719
// Replace any dependancies with the existing mapping.
708720
item.dependancies.forEach(dep => {
709-
rewriteDependancy(dep, mapping);
721+
rewriteDependancy(dep, mapping, false);
710722
});
711723

712724
const originalId = content.id;
@@ -781,7 +793,7 @@ const importTree = async (
781793
const content = item.owner.content;
782794

783795
item.dependancies.forEach(dep => {
784-
rewriteDependancy(dep, mapping);
796+
rewriteDependancy(dep, mapping, pass === 0);
785797
});
786798

787799
const originalId = content.id;
@@ -815,6 +827,7 @@ const importTree = async (
815827

816828
newDependants[i] = newItem;
817829
mapping.registerContentItem(originalId as string, newItem.id as string);
830+
mapping.registerContentItem(newItem.id as string, newItem.id as string);
818831
} else {
819832
if (itemShouldPublish(content) && (newItem.version != oldVersion || argv.republish)) {
820833
publishable.push({ item: newItem, node: item });

0 commit comments

Comments
 (0)