@squiz/dxp-cli-next 5.31.0 → 5.32.0-develop.2
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/lib/cdp/instance/activate/activate.js +6 -7
- package/lib/cdp/instance/activate/activate.spec.js +2 -8
- package/lib/cdp/utils.d.ts +2 -1
- package/lib/cdp/utils.js +4 -2
- package/lib/migration/create/create.js +5 -8
- package/lib/migration/create/create.spec.js +1 -0
- package/lib/migration/index.js +2 -0
- package/lib/migration/pre/pre.d.ts +3 -0
- package/lib/migration/pre/pre.js +54 -0
- package/lib/migration/pre/pre.spec.d.ts +1 -0
- package/lib/migration/pre/pre.spec.js +269 -0
- package/lib/migration/types/common.types.d.ts +10 -0
- package/lib/migration/types/createMigration.types.d.ts +4 -0
- package/lib/migration/types/index.d.ts +1 -0
- package/lib/migration/types/index.js +1 -0
- package/lib/migration/types/preMigration.types.d.ts +10 -0
- package/lib/migration/types/preMigration.types.js +2 -0
- package/lib/migration/utils/common.d.ts +8 -0
- package/lib/migration/utils/common.js +19 -1
- package/lib/migration/utils/createMigration.d.ts +26 -2
- package/lib/migration/utils/createMigration.js +13 -6
- package/lib/migration/utils/loadCctIdsFromFile.d.ts +1 -0
- package/lib/migration/utils/loadCctIdsFromFile.js +32 -0
- package/lib/migration/utils/loadCctIdsFromFile.spec.d.ts +1 -0
- package/lib/migration/utils/loadCctIdsFromFile.spec.js +91 -0
- package/lib/migration/utils/options.d.ts +2 -1
- package/lib/migration/utils/options.js +8 -0
- package/lib/page/layouts/deploy/deploy.js +44 -9
- package/lib/page/layouts/deploy/deploy.spec.js +110 -19
- package/lib/page/layouts/dev/dev.js +18 -4
- package/lib/page/layouts/dev/dev.spec.js +117 -8
- package/lib/page/layouts/validation/index.d.ts +2 -0
- package/lib/page/layouts/validation/index.js +5 -1
- package/lib/page/layouts/validation/property-consistency.d.ts +7 -0
- package/lib/page/layouts/validation/property-consistency.js +92 -0
- package/lib/page/layouts/validation/property-consistency.spec.d.ts +1 -0
- package/lib/page/layouts/validation/property-consistency.spec.js +305 -0
- package/lib/page/layouts/validation/validateLayoutFormat.d.ts +2 -0
- package/lib/page/layouts/validation/validateLayoutFormat.js +27 -0
- package/lib/page/layouts/validation/validateLayoutFormat.spec.d.ts +1 -0
- package/lib/page/layouts/validation/validateLayoutFormat.spec.js +42 -0
- package/lib/page/layouts/validation/zone-consistency.d.ts +1 -1
- package/lib/page/layouts/validation/zone-consistency.js +10 -9
- package/lib/page/layouts/validation/zone-consistency.spec.js +32 -34
- package/lib/page/utils/definitions.d.ts +347 -50
- package/lib/page/utils/definitions.js +103 -22
- package/lib/page/utils/definitions.spec.js +516 -267
- package/lib/page/utils/normalize.d.ts +8 -0
- package/lib/page/utils/normalize.js +61 -0
- package/lib/page/utils/normalize.spec.d.ts +1 -0
- package/lib/page/utils/normalize.spec.js +315 -0
- package/lib/page/utils/parse-args.d.ts +20 -4
- package/lib/page/utils/parse-args.js +48 -13
- package/lib/page/utils/parse-args.spec.js +159 -21
- package/lib/page/utils/render.d.ts +27 -9
- package/lib/page/utils/render.js +66 -12
- package/lib/page/utils/render.spec.js +14 -14
- package/lib/page/utils/server.d.ts +1 -1
- package/lib/page/utils/server.js +2 -2
- package/lib/page/utils/server.spec.js +13 -13
- package/package.json +1 -1
|
@@ -18,20 +18,27 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
18
|
const ApiService_1 = require("../../ApiService");
|
|
19
19
|
const _1 = require(".");
|
|
20
20
|
const child_process_1 = require("child_process");
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new migration or pre-migration using the AI Page Migration service.
|
|
23
|
+
* Pre-migrations are used to generate specs for CCTs so they can be converted and deployed in advance.
|
|
24
|
+
* @param {CreateMigrationInput} input - The input parameters for the migration/pre-migration creation.
|
|
25
|
+
* @returns {Promise<CreateMigrationApiResponse>}
|
|
26
|
+
*/
|
|
27
|
+
function createMigration({ tenant, overrideUrl, assetId, previewAssetId, matrixUrl, cctIds, }) {
|
|
22
28
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23
29
|
const apiService = new ApiService_1.ApiService({
|
|
24
30
|
validateStatus: _1.validateAxiosStatus,
|
|
25
31
|
});
|
|
26
|
-
const migrationUrl = yield (0, _1.buildMigrationUrl)(
|
|
32
|
+
const migrationUrl = yield (0, _1.buildMigrationUrl)(tenant, overrideUrl);
|
|
27
33
|
try {
|
|
28
34
|
const payload = {
|
|
29
|
-
assetId
|
|
30
|
-
previewAssetId
|
|
31
|
-
matrixUrl
|
|
35
|
+
assetId,
|
|
36
|
+
previewAssetId,
|
|
37
|
+
matrixUrl,
|
|
38
|
+
cctIds,
|
|
32
39
|
};
|
|
33
40
|
const response = yield apiService.client.post(`${migrationUrl}`, payload, {
|
|
34
|
-
headers: yield (0, _1.getMigrationHeaders)(
|
|
41
|
+
headers: yield (0, _1.getMigrationHeaders)(tenant),
|
|
35
42
|
});
|
|
36
43
|
if (response.status !== 200 && response.status !== 201) {
|
|
37
44
|
throw new Error(`Migration creation failed with status: ${response.status}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const loadCctIdsFromFile: (filePath?: string) => string[] | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadCctIdsFromFile = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const loadCctIdsFromFile = (filePath) => {
|
|
9
|
+
if (!filePath) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
14
|
+
throw new Error(`CCT IDs file not found: ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
const fileContent = fs_1.default.readFileSync(filePath, 'utf8');
|
|
17
|
+
const cctIds = JSON.parse(fileContent);
|
|
18
|
+
if (!Array.isArray(cctIds) ||
|
|
19
|
+
cctIds.some(id => typeof id !== 'string' || !id.trim()) ||
|
|
20
|
+
cctIds.length === 0) {
|
|
21
|
+
throw new Error('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
22
|
+
}
|
|
23
|
+
return cctIds;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
throw new Error(`Failed to load CCT IDs from file: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Failed to load CCT IDs from file: ${error}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
exports.loadCctIdsFromFile = loadCctIdsFromFile;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const loadCctIdsFromFile_1 = require("./loadCctIdsFromFile");
|
|
8
|
+
jest.mock('fs');
|
|
9
|
+
const mockFs = fs_1.default;
|
|
10
|
+
describe('loadCctIdsFromFile', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
it('should return undefined when no file path is provided', () => {
|
|
15
|
+
const result = (0, loadCctIdsFromFile_1.loadCctIdsFromFile)();
|
|
16
|
+
expect(result).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
it('should load and parse valid JSON file with CCT IDs', () => {
|
|
19
|
+
const mockFilePath = '/test/path/cct-ids.json';
|
|
20
|
+
const mockJsonContent = '["cct-id-1", "cct-id-2", "cct-id-3"]';
|
|
21
|
+
const expectedResult = ['cct-id-1', 'cct-id-2', 'cct-id-3'];
|
|
22
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
23
|
+
mockFs.readFileSync.mockReturnValue(mockJsonContent);
|
|
24
|
+
const result = (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath);
|
|
25
|
+
expect(mockFs.existsSync).toHaveBeenCalledWith(mockFilePath);
|
|
26
|
+
expect(mockFs.readFileSync).toHaveBeenCalledWith(mockFilePath, 'utf8');
|
|
27
|
+
expect(result).toEqual(expectedResult);
|
|
28
|
+
});
|
|
29
|
+
it('should throw error when file does not exist', () => {
|
|
30
|
+
const mockFilePath = '/test/path/nonexistent.json';
|
|
31
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
32
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow(`Failed to load CCT IDs from file: CCT IDs file not found: ${mockFilePath}`);
|
|
33
|
+
});
|
|
34
|
+
it('should throw error when JSON is invalid', () => {
|
|
35
|
+
const mockFilePath = '/test/path/invalid.json';
|
|
36
|
+
const invalidJson = '["invalid": json}';
|
|
37
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
38
|
+
mockFs.readFileSync.mockReturnValue(invalidJson);
|
|
39
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('Failed to load CCT IDs from file:');
|
|
40
|
+
});
|
|
41
|
+
it('should throw error when JSON is not an array', () => {
|
|
42
|
+
const mockFilePath = '/test/path/object.json';
|
|
43
|
+
const objectJson = '{"id": "cct-id-1"}';
|
|
44
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
45
|
+
mockFs.readFileSync.mockReturnValue(objectJson);
|
|
46
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
47
|
+
});
|
|
48
|
+
it('should throw error when array is empty', () => {
|
|
49
|
+
const mockFilePath = '/test/path/empty.json';
|
|
50
|
+
const emptyArrayJson = '[]';
|
|
51
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
52
|
+
mockFs.readFileSync.mockReturnValue(emptyArrayJson);
|
|
53
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
54
|
+
});
|
|
55
|
+
it('should throw error when array contains non-string values', () => {
|
|
56
|
+
const mockFilePath = '/test/path/mixed.json';
|
|
57
|
+
const mixedArrayJson = '["cct-id-1", 123, "cct-id-3"]';
|
|
58
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
59
|
+
mockFs.readFileSync.mockReturnValue(mixedArrayJson);
|
|
60
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
61
|
+
});
|
|
62
|
+
it('should throw error when array contains empty strings', () => {
|
|
63
|
+
const mockFilePath = '/test/path/empty-strings.json';
|
|
64
|
+
const emptyStringsJson = '["cct-id-1", "", "cct-id-3"]';
|
|
65
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
66
|
+
mockFs.readFileSync.mockReturnValue(emptyStringsJson);
|
|
67
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
68
|
+
});
|
|
69
|
+
it('should throw error when array contains whitespace-only strings', () => {
|
|
70
|
+
const mockFilePath = '/test/path/whitespace.json';
|
|
71
|
+
const whitespaceJson = '["cct-id-1", " ", "cct-id-3"]';
|
|
72
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
73
|
+
mockFs.readFileSync.mockReturnValue(whitespaceJson);
|
|
74
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
75
|
+
});
|
|
76
|
+
it('should throw error when JSON is null', () => {
|
|
77
|
+
const mockFilePath = '/test/path/null.json';
|
|
78
|
+
const nullJson = 'null';
|
|
79
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
80
|
+
mockFs.readFileSync.mockReturnValue(nullJson);
|
|
81
|
+
expect(() => (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath)).toThrow('CCT IDs file must contain a valid array of strings representing CCT IDs with at least one CCT ID');
|
|
82
|
+
});
|
|
83
|
+
it('should handle single CCT ID in array', () => {
|
|
84
|
+
const mockFilePath = '/test/path/single.json';
|
|
85
|
+
const singleIdJson = '["cct-id-only"]';
|
|
86
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
87
|
+
mockFs.readFileSync.mockReturnValue(singleIdJson);
|
|
88
|
+
const result = (0, loadCctIdsFromFile_1.loadCctIdsFromFile)(mockFilePath);
|
|
89
|
+
expect(result).toEqual(['cct-id-only']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -8,7 +8,8 @@ export declare enum OptionName {
|
|
|
8
8
|
MATRIX_IDENTIFIER = "matrix-identifier",
|
|
9
9
|
MATRIX_KEY = "matrix-key",
|
|
10
10
|
CONTENT_API_KEY = "content-api-key",
|
|
11
|
-
STAGE_OPTIONS = "stage-options"
|
|
11
|
+
STAGE_OPTIONS = "stage-options",
|
|
12
|
+
CCT_IDS = "cct-ids"
|
|
12
13
|
}
|
|
13
14
|
export declare const getParamOption: (param: OptionName, makeOptionMandatory?: boolean) => Option;
|
|
14
15
|
export declare const addOverrideUrlOption: (command: Command) => void;
|
|
@@ -13,6 +13,7 @@ var OptionName;
|
|
|
13
13
|
OptionName["MATRIX_KEY"] = "matrix-key";
|
|
14
14
|
OptionName["CONTENT_API_KEY"] = "content-api-key";
|
|
15
15
|
OptionName["STAGE_OPTIONS"] = "stage-options";
|
|
16
|
+
OptionName["CCT_IDS"] = "cct-ids";
|
|
16
17
|
})(OptionName = exports.OptionName || (exports.OptionName = {}));
|
|
17
18
|
const params = new Map([
|
|
18
19
|
[
|
|
@@ -78,6 +79,13 @@ const params = new Map([
|
|
|
78
79
|
description: 'Path to a JSON file containing stage options',
|
|
79
80
|
},
|
|
80
81
|
],
|
|
82
|
+
[
|
|
83
|
+
OptionName.CCT_IDS,
|
|
84
|
+
{
|
|
85
|
+
flags: '--cct-ids <string>',
|
|
86
|
+
description: 'The IDs of the CCTs to be converted in a pre-migration, or to be skipped in a migration (already converted)',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
81
89
|
]);
|
|
82
90
|
const getParamOption = (param, makeOptionMandatory = true) => {
|
|
83
91
|
const paramInfo = params.get(param);
|
|
@@ -28,7 +28,7 @@ exports.logger = (0, dx_logger_lib_1.getLogger)({
|
|
|
28
28
|
const createDeployCommand = () => {
|
|
29
29
|
const deployCommand = new commander_1.Command()
|
|
30
30
|
.name('deploy')
|
|
31
|
-
.addOption(new commander_1.Option('--config <string>', 'File path to the page layout config file')
|
|
31
|
+
.addOption(new commander_1.Option('--config <string>', 'File path to the page layout config file'))
|
|
32
32
|
.addOption(new commander_1.Option('-cu, --content-service-url <string>', 'Override the content service url from login')
|
|
33
33
|
.env('CONTENT_SERVICE_URL')
|
|
34
34
|
.hideHelp(true))
|
|
@@ -47,16 +47,25 @@ const createDeployCommand = () => {
|
|
|
47
47
|
tenantId: (_c = options.tenant) !== null && _c !== void 0 ? _c : maybeConfig === null || maybeConfig === void 0 ? void 0 : maybeConfig.tenant,
|
|
48
48
|
});
|
|
49
49
|
const contentServiceUrl = (_d = options.contentServiceUrl) !== null && _d !== void 0 ? _d : new URL('/__dxp/service/components-content', baseUrl).toString();
|
|
50
|
-
|
|
50
|
+
let layoutFile = options.config;
|
|
51
|
+
(0, validation_1.validateLayoutFormat)(exports.logger, layoutFile);
|
|
52
|
+
if (!layoutFile) {
|
|
53
|
+
layoutFile = `./${definitions_1.LAYOUT_MANIFEST_FILE}`;
|
|
54
|
+
}
|
|
51
55
|
exports.logger.info(`Loading layout data from the file ${layoutFile}`);
|
|
52
56
|
try {
|
|
53
57
|
const layout = yield (0, definitions_1.loadLayoutDefinition)(layoutFile);
|
|
54
58
|
if (layout !== undefined) {
|
|
55
|
-
// Pre-deployment validation: check zones match between
|
|
59
|
+
// Pre-deployment validation: check zones match between manifest and template
|
|
56
60
|
const zoneValidationError = (0, validation_1.validateZoneConsistency)(layout);
|
|
57
61
|
if (zoneValidationError) {
|
|
58
62
|
throw new Error(zoneValidationError);
|
|
59
63
|
}
|
|
64
|
+
// Validate Handlebars template only refers to properties if manifest.json is used
|
|
65
|
+
const propertyValidationError = (0, validation_1.validatePropertyConsistency)(layout, layoutFile);
|
|
66
|
+
if (propertyValidationError) {
|
|
67
|
+
throw new Error(propertyValidationError);
|
|
68
|
+
}
|
|
60
69
|
const response = yield uploadLayout(apiService.client, layout, contentServiceUrl, options.dryRun);
|
|
61
70
|
if (!options.dryRun) {
|
|
62
71
|
exports.logger.info(`Layout "${layout.name}" version ${response.data.version} deployed successfully.`);
|
|
@@ -90,8 +99,34 @@ const createDeployCommand = () => {
|
|
|
90
99
|
return deployCommand;
|
|
91
100
|
};
|
|
92
101
|
exports.default = createDeployCommand;
|
|
102
|
+
/**
|
|
103
|
+
* Formats backend error responses into user-friendly error messages.
|
|
104
|
+
* Handles both validation errors (400s) and unknown server errors (500s).
|
|
105
|
+
*
|
|
106
|
+
* @param data - The error response data from the backend
|
|
107
|
+
* @param statusCode - The HTTP status code (optional)
|
|
108
|
+
* @returns Formatted error message string
|
|
109
|
+
*/
|
|
110
|
+
function formatErrorResponse(data, statusCode) {
|
|
111
|
+
// For 500 errors or unknown errors, show a generic message
|
|
112
|
+
if (statusCode && statusCode >= 500) {
|
|
113
|
+
return data.message || data.data || 'An unknown error occurred';
|
|
114
|
+
}
|
|
115
|
+
// For validation errors (400s), format with details
|
|
116
|
+
let errorMessage = data.message || 'Validation failed';
|
|
117
|
+
// Properties/options validation errors come as an object with field keys
|
|
118
|
+
if (data.details &&
|
|
119
|
+
typeof data.details === 'object' &&
|
|
120
|
+
!Array.isArray(data.details)) {
|
|
121
|
+
const details = Object.entries(data.details)
|
|
122
|
+
.map(([field, error]) => ` - ${field}: ${error.message}`)
|
|
123
|
+
.join('\n');
|
|
124
|
+
errorMessage += `\n${details}`;
|
|
125
|
+
}
|
|
126
|
+
return errorMessage;
|
|
127
|
+
}
|
|
93
128
|
function uploadLayout(client, layout, contentServiceUrl, dryRun) {
|
|
94
|
-
var _a
|
|
129
|
+
var _a;
|
|
95
130
|
return __awaiter(this, void 0, void 0, function* () {
|
|
96
131
|
try {
|
|
97
132
|
const queryParam = dryRun ? '?_dryRun=true' : '';
|
|
@@ -101,13 +136,13 @@ function uploadLayout(client, layout, contentServiceUrl, dryRun) {
|
|
|
101
136
|
if (response.status === 200) {
|
|
102
137
|
return response;
|
|
103
138
|
}
|
|
104
|
-
|
|
105
|
-
if ((_b = (_a = response.data.details) === null || _a === void 0 ? void 0 : _a.input) === null || _b === void 0 ? void 0 : _b.message) {
|
|
106
|
-
error.message += `: ${response.data.details.input.message}`;
|
|
107
|
-
}
|
|
108
|
-
throw error;
|
|
139
|
+
throw new Error(formatErrorResponse(response.data, response.status));
|
|
109
140
|
}
|
|
110
141
|
catch (error) {
|
|
142
|
+
// Extract error details from axios error response
|
|
143
|
+
if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) {
|
|
144
|
+
throw new Error(formatErrorResponse(error.response.data, error.response.status));
|
|
145
|
+
}
|
|
111
146
|
throw error;
|
|
112
147
|
}
|
|
113
148
|
});
|
|
@@ -17,10 +17,12 @@ jest.mock('@squiz/dx-logger-lib', () => ({
|
|
|
17
17
|
getLogger: () => {
|
|
18
18
|
return {
|
|
19
19
|
info: mockLoggerInfoFn,
|
|
20
|
+
warn: mockLoggerWarnFn,
|
|
20
21
|
};
|
|
21
22
|
},
|
|
22
23
|
}));
|
|
23
24
|
const mockLoggerInfoFn = jest.fn();
|
|
25
|
+
const mockLoggerWarnFn = jest.fn();
|
|
24
26
|
const nock_1 = __importDefault(require("nock"));
|
|
25
27
|
const deploy_1 = __importDefault(require("./deploy"));
|
|
26
28
|
const definitions_1 = require("../../utils/definitions");
|
|
@@ -58,7 +60,7 @@ describe('deployCommand', () => {
|
|
|
58
60
|
const program = (0, deploy_1.default)();
|
|
59
61
|
yield program.parseAsync(createMockArgs({}));
|
|
60
62
|
const opts = program.opts();
|
|
61
|
-
expect(opts.config).
|
|
63
|
+
expect(opts.config).toBeUndefined();
|
|
62
64
|
expect(opts.tenant).toEqual('myTenant');
|
|
63
65
|
expect(opts.dryRun).toEqual(false);
|
|
64
66
|
}));
|
|
@@ -90,13 +92,17 @@ describe('deployCommand', () => {
|
|
|
90
92
|
expect(logSpy).toHaveBeenCalledWith(`NOTICE: CONTENT_SERVICE_URL is set and will deploy to ${contentServiceUrl}`);
|
|
91
93
|
}));
|
|
92
94
|
it('deploys a layout successfully', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
|
-
const file =
|
|
95
|
+
const file = `./src/__tests__/layouts/${definitions_1.LAYOUT_MANIFEST_FILE}`;
|
|
94
96
|
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
95
97
|
const mockLayout = {
|
|
96
98
|
name: 'test-layout',
|
|
97
|
-
zones:
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
zones: [
|
|
100
|
+
{
|
|
101
|
+
key: 'content',
|
|
102
|
+
displayName: 'Content',
|
|
103
|
+
description: 'Main content',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
100
106
|
template: '<div>{{zones.content}}</div>',
|
|
101
107
|
};
|
|
102
108
|
const mockResponse = { name: 'test-layout', version: '12345' };
|
|
@@ -116,9 +122,13 @@ describe('deployCommand', () => {
|
|
|
116
122
|
const dryRun = true;
|
|
117
123
|
const mockLayout = {
|
|
118
124
|
name: 'test-layout',
|
|
119
|
-
zones:
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
zones: [
|
|
126
|
+
{
|
|
127
|
+
key: 'content',
|
|
128
|
+
displayName: 'Content',
|
|
129
|
+
description: 'Main content',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
122
132
|
template: '<div>{{zones.content}}</div>',
|
|
123
133
|
};
|
|
124
134
|
const mockResponse = { name: 'test-layout', version: '12345' };
|
|
@@ -170,7 +180,7 @@ describe('deployCommand', () => {
|
|
|
170
180
|
const program = (0, deploy_1.default)();
|
|
171
181
|
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
172
182
|
yield program.parseAsync(createMockArgs({ contentServiceUrl }));
|
|
173
|
-
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('
|
|
183
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Internal server error'));
|
|
174
184
|
}));
|
|
175
185
|
it('should log additional details when layout validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
176
186
|
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
@@ -187,8 +197,53 @@ describe('deployCommand', () => {
|
|
|
187
197
|
const program = (0, deploy_1.default)();
|
|
188
198
|
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
189
199
|
yield program.parseAsync(createMockArgs({ dxpBaseUrl }));
|
|
190
|
-
|
|
200
|
+
const errorCall = errorSpy.mock.calls[0][0];
|
|
201
|
+
expect(errorCall).toContain('- input: ERROR: Validation failed: "version" is an excess property and therefore is not allowed');
|
|
191
202
|
}));
|
|
203
|
+
describe('backend property validation error reporting', () => {
|
|
204
|
+
it('should display clean error when single property validation fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
const contentServiceUrl = 'http://localhost:9999';
|
|
206
|
+
(0, nock_1.default)(contentServiceUrl)
|
|
207
|
+
.post('/page-layout')
|
|
208
|
+
.reply(400, {
|
|
209
|
+
message: 'Validation failed',
|
|
210
|
+
details: {
|
|
211
|
+
'input.properties.myBoolean': {
|
|
212
|
+
message: "Property 'myBoolean' with type 'boolean' can not have values specified",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const program = (0, deploy_1.default)();
|
|
217
|
+
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
218
|
+
yield program.parseAsync(createMockArgs({ contentServiceUrl }));
|
|
219
|
+
const errorCall = errorSpy.mock.calls[0][0];
|
|
220
|
+
expect(errorCall).toContain('Validation failed');
|
|
221
|
+
expect(errorCall).toContain("- input.properties.myBoolean: Property 'myBoolean' with type 'boolean' can not have values specified");
|
|
222
|
+
}));
|
|
223
|
+
it('should display multiple property validation errors cleanly', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
const contentServiceUrl = 'http://localhost:9999';
|
|
225
|
+
(0, nock_1.default)(contentServiceUrl)
|
|
226
|
+
.post('/page-layout')
|
|
227
|
+
.reply(400, {
|
|
228
|
+
message: 'Validation failed',
|
|
229
|
+
details: {
|
|
230
|
+
'input.properties.badBoolean': {
|
|
231
|
+
message: "Property 'badBoolean' with type 'boolean' cannot have enum values specified",
|
|
232
|
+
},
|
|
233
|
+
'input.properties.badProperty.type': {
|
|
234
|
+
message: "Property 'badProperty' has invalid type 'INVALID'. Must be one of: string, boolean",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const program = (0, deploy_1.default)();
|
|
239
|
+
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
240
|
+
yield program.parseAsync(createMockArgs({ contentServiceUrl }));
|
|
241
|
+
const errorCall = errorSpy.mock.calls[0][0];
|
|
242
|
+
expect(errorCall).toContain('Validation failed');
|
|
243
|
+
expect(errorCall).toContain("- input.properties.badBoolean: Property 'badBoolean' with type 'boolean' cannot have enum values specified");
|
|
244
|
+
expect(errorCall).toContain("- input.properties.badProperty.type: Property 'badProperty' has invalid type 'INVALID'. Must be one of: string, boolean");
|
|
245
|
+
}));
|
|
246
|
+
});
|
|
192
247
|
describe('zone consistency validation', () => {
|
|
193
248
|
it('should handle zone consistency validation errors where zones are used but not defined in the layout', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
194
249
|
const file = './src/__tests__/layout.yaml';
|
|
@@ -196,33 +251,69 @@ describe('deployCommand', () => {
|
|
|
196
251
|
// Mock layout with zones that don't match the template
|
|
197
252
|
const mockLayout = {
|
|
198
253
|
name: 'test-layout',
|
|
199
|
-
zones:
|
|
200
|
-
|
|
201
|
-
|
|
254
|
+
zones: [
|
|
255
|
+
{
|
|
256
|
+
key: 'col1',
|
|
257
|
+
displayName: 'Column 1',
|
|
258
|
+
description: 'The first column',
|
|
259
|
+
},
|
|
260
|
+
],
|
|
202
261
|
template: '{{zones.col1}} {{zones.col2}}', // col2 is used but not defined in zones
|
|
203
262
|
};
|
|
204
263
|
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
205
264
|
const program = (0, deploy_1.default)();
|
|
206
265
|
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
207
266
|
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
|
|
208
|
-
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones used in template but not defined in
|
|
267
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones used in template but not defined in layout definition: col2/));
|
|
209
268
|
}));
|
|
210
269
|
it('should handle zone consistency validation errors where zones are defined but not used in the template', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
211
270
|
const file = './src/__tests__/layout.yaml';
|
|
212
271
|
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
213
272
|
const mockLayout = {
|
|
214
273
|
name: 'test-layout',
|
|
215
|
-
zones:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
274
|
+
zones: [
|
|
275
|
+
{
|
|
276
|
+
key: 'col1',
|
|
277
|
+
displayName: 'Column 1',
|
|
278
|
+
description: 'The first column',
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
key: 'col2',
|
|
282
|
+
displayName: 'Column 2',
|
|
283
|
+
description: 'The second column',
|
|
284
|
+
}, // col2 is defined but not used in the template
|
|
285
|
+
],
|
|
219
286
|
template: '{{zones.col1}}',
|
|
220
287
|
};
|
|
221
288
|
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
222
289
|
const program = (0, deploy_1.default)();
|
|
223
290
|
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
224
291
|
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
|
|
225
|
-
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones defined in
|
|
292
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Zone consistency validation failed[\s\S]*Zones defined in layout definition but not used in template: col2/));
|
|
293
|
+
}));
|
|
294
|
+
});
|
|
295
|
+
describe('property consistency validation', () => {
|
|
296
|
+
it('should handle property consistency validation errors where properties are used but not defined in the layout', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
297
|
+
const file = './src/__tests__/manifest.json';
|
|
298
|
+
const dxpBaseUrl = 'http://dxp-base-url.com';
|
|
299
|
+
// Mock layout with properties that don't match the template
|
|
300
|
+
const mockLayout = {
|
|
301
|
+
name: 'test-layout',
|
|
302
|
+
zones: [],
|
|
303
|
+
properties: {
|
|
304
|
+
title: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
title: 'Title',
|
|
307
|
+
description: 'Page title',
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
template: '{{properties.title}} {{properties.undefined}}', // undefined is used but not defined in properties
|
|
311
|
+
};
|
|
312
|
+
definitions_1.loadLayoutDefinition.mockResolvedValue(mockLayout);
|
|
313
|
+
const program = (0, deploy_1.default)();
|
|
314
|
+
errorSpy = jest.spyOn(program, 'error').mockImplementation();
|
|
315
|
+
yield program.parseAsync(createMockArgs({ config: file, dxpBaseUrl }));
|
|
316
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Property consistency validation failed[\s\S]*Properties used in template but not defined in layout definition: undefined/));
|
|
226
317
|
}));
|
|
227
318
|
});
|
|
228
319
|
});
|
|
@@ -19,6 +19,7 @@ const definitions_1 = require("../../utils/definitions");
|
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const server_1 = require("../../utils/server");
|
|
21
21
|
const parse_args_1 = require("../../utils/parse-args");
|
|
22
|
+
const validation_1 = require("../validation");
|
|
22
23
|
exports.logger = (0, dx_logger_lib_1.getLogger)({
|
|
23
24
|
name: 'layout-dev',
|
|
24
25
|
format: 'human',
|
|
@@ -27,13 +28,13 @@ const createDevCommand = () => {
|
|
|
27
28
|
const devCommand = new commander_1.Command()
|
|
28
29
|
.name('dev')
|
|
29
30
|
.description('Start a development server for page layouts')
|
|
30
|
-
.option('--config <string>', 'File path to the page layout config file',
|
|
31
|
+
.option('--config <string>', 'File path to the page layout config file', `./${definitions_1.LAYOUT_MANIFEST_FILE}`)
|
|
31
32
|
.option('--port <number>', 'Port to run the development server on', '4040')
|
|
32
33
|
.option('--no-open', 'Do not automatically open browser')
|
|
33
34
|
.option('--stylesheet <path>', 'Path to CSS file to include')
|
|
34
35
|
.option('--zones <items>', 'Zone content mappings', parse_args_1.parseZonesList, {})
|
|
35
|
-
.option('--
|
|
36
|
-
.allowUnknownOption(
|
|
36
|
+
.option('--properties <items>', 'Layout properties', parse_args_1.parsePropertiesList, {})
|
|
37
|
+
.allowUnknownOption(false)
|
|
37
38
|
.allowExcessArguments(true)
|
|
38
39
|
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
40
|
try {
|
|
@@ -43,14 +44,27 @@ const createDevCommand = () => {
|
|
|
43
44
|
if (!rawLayoutDefinition) {
|
|
44
45
|
throw new Error(`Failed to load layout definition from ${options.config}`);
|
|
45
46
|
}
|
|
47
|
+
(0, validation_1.validateLayoutFormat)(exports.logger, options.config);
|
|
48
|
+
// Check zones match between manifest and template
|
|
49
|
+
const zoneValidationError = (0, validation_1.validateZoneConsistency)(rawLayoutDefinition);
|
|
50
|
+
if (zoneValidationError) {
|
|
51
|
+
throw new Error(zoneValidationError);
|
|
52
|
+
}
|
|
53
|
+
// Validate Handlebars template only refers to properties if manifest.json is used
|
|
54
|
+
const propertyValidationError = (0, validation_1.validatePropertyConsistency)(rawLayoutDefinition, options.config);
|
|
55
|
+
if (propertyValidationError) {
|
|
56
|
+
throw new Error(propertyValidationError);
|
|
57
|
+
}
|
|
46
58
|
// Confirm for entry property
|
|
47
59
|
const layoutDefinition = Object.assign({}, rawLayoutDefinition);
|
|
60
|
+
// Normalize properties to convert string booleans to actual booleans
|
|
61
|
+
const normalizedProperties = (0, parse_args_1.normalizeLayoutProperties)(options.properties, layoutDefinition);
|
|
48
62
|
exports.logger.info('Starting development server...');
|
|
49
63
|
yield (0, server_1.startDevServer)({
|
|
50
64
|
configPath: path_1.default.resolve(options.config),
|
|
51
65
|
layoutDefinition,
|
|
52
66
|
zoneContent: options.zones,
|
|
53
|
-
|
|
67
|
+
layoutProperties: normalizedProperties,
|
|
54
68
|
stylesheet: options.stylesheet
|
|
55
69
|
? path_1.default.resolve(options.stylesheet)
|
|
56
70
|
: undefined,
|