sfcc-metadata-cli 0.0.1 → 1.0.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/.github/workflows/check.yml +80 -0
- package/AGENTS.md +82 -0
- package/LICENSE +661 -0
- package/README.md +249 -0
- package/biome.json +32 -0
- package/commands/create-migration.js +157 -0
- package/commands/custom-object.js +426 -0
- package/commands/site-preference.js +503 -0
- package/commands/system-object.js +572 -0
- package/index.js +34 -0
- package/lib/merge.js +271 -0
- package/lib/templates.js +315 -0
- package/lib/utils.js +188 -0
- package/package.json +24 -15
- package/test/merge.test.js +84 -0
- package/test/templates.test.js +133 -0
- package/test/utils.test.js +79 -0
package/lib/merge.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML Merge utilities for SFCC migrations
|
|
3
|
+
* Handles merging new attributes into existing XML files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Merges a new attribute definition into an existing system-objecttype-extensions.xml
|
|
10
|
+
* @param {string} existingXml - Existing XML content
|
|
11
|
+
* @param {Object} config - New attribute configuration
|
|
12
|
+
* @returns {{xml: string, groupExisted: boolean, typeExtensionExisted: boolean}} Merged XML content and metadata
|
|
13
|
+
*/
|
|
14
|
+
function mergeSystemObjectExtension(existingXml, config) {
|
|
15
|
+
const {
|
|
16
|
+
objectTypeId,
|
|
17
|
+
attributeId,
|
|
18
|
+
displayName,
|
|
19
|
+
description,
|
|
20
|
+
type,
|
|
21
|
+
localizable = false,
|
|
22
|
+
mandatory = false,
|
|
23
|
+
externallyManaged = false,
|
|
24
|
+
defaultValue,
|
|
25
|
+
groupId,
|
|
26
|
+
groupDisplayName,
|
|
27
|
+
enumValues = [],
|
|
28
|
+
multiSelect = false,
|
|
29
|
+
minLength = 0,
|
|
30
|
+
} = config;
|
|
31
|
+
|
|
32
|
+
let groupExisted = false;
|
|
33
|
+
let typeExtensionExisted = false;
|
|
34
|
+
|
|
35
|
+
// Generate the attribute definition XML
|
|
36
|
+
const attrDefXml = generateAttributeDefinitionXml({
|
|
37
|
+
id: attributeId,
|
|
38
|
+
displayName,
|
|
39
|
+
description,
|
|
40
|
+
type,
|
|
41
|
+
localizable,
|
|
42
|
+
mandatory,
|
|
43
|
+
externallyManaged,
|
|
44
|
+
defaultValue,
|
|
45
|
+
enumValues,
|
|
46
|
+
multiSelect,
|
|
47
|
+
minLength,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Check if type-extension for this object already exists
|
|
51
|
+
const typeExtRegex = new RegExp(
|
|
52
|
+
`<type-extension type-id="${objectTypeId}">([\\s\\S]*?)</type-extension>`,
|
|
53
|
+
);
|
|
54
|
+
const typeExtMatch = existingXml.match(typeExtRegex);
|
|
55
|
+
|
|
56
|
+
if (typeExtMatch) {
|
|
57
|
+
// Type extension exists - merge into it
|
|
58
|
+
typeExtensionExisted = true;
|
|
59
|
+
let typeExtContent = typeExtMatch[1];
|
|
60
|
+
|
|
61
|
+
// Add attribute definition
|
|
62
|
+
const attrDefsEndMatch = typeExtContent.match(
|
|
63
|
+
/<\/custom-attribute-definitions>/,
|
|
64
|
+
);
|
|
65
|
+
if (attrDefsEndMatch) {
|
|
66
|
+
typeExtContent = typeExtContent.replace(
|
|
67
|
+
'</custom-attribute-definitions>',
|
|
68
|
+
`${attrDefXml}\n </custom-attribute-definitions>`,
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
// No custom-attribute-definitions section, add it
|
|
72
|
+
typeExtContent = `
|
|
73
|
+
<custom-attribute-definitions>
|
|
74
|
+
${attrDefXml}
|
|
75
|
+
</custom-attribute-definitions>${typeExtContent}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add attribute to group
|
|
79
|
+
const finalGroupId = groupId || `${objectTypeId}_Custom`;
|
|
80
|
+
const groupRegex = new RegExp(
|
|
81
|
+
`<attribute-group group-id="${escapeRegex(finalGroupId)}">([\\s\\S]*?)</attribute-group>`,
|
|
82
|
+
);
|
|
83
|
+
const groupMatch = typeExtContent.match(groupRegex);
|
|
84
|
+
|
|
85
|
+
if (groupMatch) {
|
|
86
|
+
// Group exists - add attribute reference
|
|
87
|
+
groupExisted = true;
|
|
88
|
+
const groupContent = groupMatch[1];
|
|
89
|
+
if (!groupContent.includes(`attribute-id="${attributeId}"`)) {
|
|
90
|
+
typeExtContent = typeExtContent.replace(
|
|
91
|
+
'</attribute-group>',
|
|
92
|
+
` <attribute attribute-id="${attributeId}"/>\n </attribute-group>`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Group doesn't exist - add it
|
|
97
|
+
const groupDefsEndMatch = typeExtContent.match(
|
|
98
|
+
/<\/group-definitions>/,
|
|
99
|
+
);
|
|
100
|
+
const newGroup = ` <attribute-group group-id="${finalGroupId}">
|
|
101
|
+
<display-name xml:lang="x-default">${escapeXml(groupDisplayName || finalGroupId)}</display-name>
|
|
102
|
+
<attribute attribute-id="${attributeId}"/>
|
|
103
|
+
</attribute-group>`;
|
|
104
|
+
|
|
105
|
+
if (groupDefsEndMatch) {
|
|
106
|
+
typeExtContent = typeExtContent.replace(
|
|
107
|
+
'</group-definitions>',
|
|
108
|
+
`${newGroup}\n </group-definitions>`,
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
// No group-definitions section
|
|
112
|
+
typeExtContent += `
|
|
113
|
+
<group-definitions>
|
|
114
|
+
${newGroup}
|
|
115
|
+
</group-definitions>`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Replace the type extension in the original XML
|
|
120
|
+
const mergedXml = existingXml.replace(
|
|
121
|
+
typeExtRegex,
|
|
122
|
+
`<type-extension type-id="${objectTypeId}">${typeExtContent}</type-extension>`,
|
|
123
|
+
);
|
|
124
|
+
return { xml: mergedXml, groupExisted, typeExtensionExisted };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Type extension doesn't exist - add it before </metadata>
|
|
128
|
+
const finalGroupId2 = groupId || `${objectTypeId}_Custom`;
|
|
129
|
+
const newTypeExt = ` <type-extension type-id="${objectTypeId}">
|
|
130
|
+
<custom-attribute-definitions>
|
|
131
|
+
${attrDefXml}
|
|
132
|
+
</custom-attribute-definitions>
|
|
133
|
+
<group-definitions>
|
|
134
|
+
<attribute-group group-id="${finalGroupId2}">
|
|
135
|
+
<display-name xml:lang="x-default">${escapeXml(groupDisplayName || finalGroupId2)}</display-name>
|
|
136
|
+
<attribute attribute-id="${attributeId}"/>
|
|
137
|
+
</attribute-group>
|
|
138
|
+
</group-definitions>
|
|
139
|
+
</type-extension>`;
|
|
140
|
+
|
|
141
|
+
const mergedXml = existingXml.replace(
|
|
142
|
+
'</metadata>',
|
|
143
|
+
`${newTypeExt}\n</metadata>`,
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
xml: mergedXml,
|
|
147
|
+
groupExisted: false,
|
|
148
|
+
typeExtensionExisted: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Merges a new custom object type into an existing custom-objecttype-definitions.xml
|
|
154
|
+
* @param {string} existingXml - Existing XML content
|
|
155
|
+
* @param {string} newTypeXml - New custom type XML (inner content only)
|
|
156
|
+
* @returns {string} Merged XML content
|
|
157
|
+
*/
|
|
158
|
+
function mergeCustomObjectDefinition(existingXml, newTypeXml) {
|
|
159
|
+
// Add before </metadata>
|
|
160
|
+
return existingXml.replace('</metadata>', `${newTypeXml}\n</metadata>`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generates attribute definition XML (without wrapper)
|
|
165
|
+
* @param {Object} attr - Attribute configuration
|
|
166
|
+
* @returns {string} XML string
|
|
167
|
+
*/
|
|
168
|
+
function generateAttributeDefinitionXml(attr) {
|
|
169
|
+
const {
|
|
170
|
+
id,
|
|
171
|
+
displayName = id,
|
|
172
|
+
description = '',
|
|
173
|
+
type = 'string',
|
|
174
|
+
localizable = false,
|
|
175
|
+
mandatory = false,
|
|
176
|
+
externallyManaged = false,
|
|
177
|
+
defaultValue = null,
|
|
178
|
+
minLength = 0,
|
|
179
|
+
enumValues = [],
|
|
180
|
+
multiSelect = false,
|
|
181
|
+
} = attr;
|
|
182
|
+
|
|
183
|
+
let typeSpecificXml = '';
|
|
184
|
+
|
|
185
|
+
switch (type) {
|
|
186
|
+
case 'string':
|
|
187
|
+
case 'text':
|
|
188
|
+
case 'html':
|
|
189
|
+
case 'password':
|
|
190
|
+
typeSpecificXml = `
|
|
191
|
+
<min-length>${minLength}</min-length>`;
|
|
192
|
+
break;
|
|
193
|
+
case 'int':
|
|
194
|
+
case 'double':
|
|
195
|
+
if (attr.minValue !== undefined) {
|
|
196
|
+
typeSpecificXml += `
|
|
197
|
+
<min-value>${attr.minValue}</min-value>`;
|
|
198
|
+
}
|
|
199
|
+
if (attr.maxValue !== undefined) {
|
|
200
|
+
typeSpecificXml += `
|
|
201
|
+
<max-value>${attr.maxValue}</max-value>`;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case 'enum-of-string':
|
|
205
|
+
case 'enum-of-int':
|
|
206
|
+
typeSpecificXml = `
|
|
207
|
+
<select-multiple-flag>${multiSelect}</select-multiple-flag>
|
|
208
|
+
<value-definitions>
|
|
209
|
+
${enumValues
|
|
210
|
+
.map(
|
|
211
|
+
(v) => ` <value-definition>
|
|
212
|
+
<display xml:lang="x-default">${v.display || v.value}</display>
|
|
213
|
+
<value>${v.value}</value>
|
|
214
|
+
</value-definition>`,
|
|
215
|
+
)
|
|
216
|
+
.join('\n')}
|
|
217
|
+
</value-definitions>`;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const defaultValueXml =
|
|
222
|
+
defaultValue !== null
|
|
223
|
+
? `
|
|
224
|
+
<default-value>${defaultValue}</default-value>`
|
|
225
|
+
: '';
|
|
226
|
+
|
|
227
|
+
const descriptionXml = description
|
|
228
|
+
? `
|
|
229
|
+
<description xml:lang="x-default">${escapeXml(description)}</description>`
|
|
230
|
+
: '';
|
|
231
|
+
|
|
232
|
+
return ` <attribute-definition attribute-id="${id}">
|
|
233
|
+
<display-name xml:lang="x-default">${escapeXml(displayName)}</display-name>${descriptionXml}
|
|
234
|
+
<type>${type}</type>
|
|
235
|
+
<localizable-flag>${localizable}</localizable-flag>
|
|
236
|
+
<mandatory-flag>${mandatory}</mandatory-flag>
|
|
237
|
+
<externally-managed-flag>${externallyManaged}</externally-managed-flag>${typeSpecificXml}${defaultValueXml}
|
|
238
|
+
</attribute-definition>`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Escapes special XML characters
|
|
243
|
+
* @param {string} str - String to escape
|
|
244
|
+
* @returns {string} Escaped string
|
|
245
|
+
*/
|
|
246
|
+
function escapeXml(str) {
|
|
247
|
+
if (!str) return '';
|
|
248
|
+
return str
|
|
249
|
+
.replace(/&/g, '&')
|
|
250
|
+
.replace(/</g, '<')
|
|
251
|
+
.replace(/>/g, '>')
|
|
252
|
+
.replace(/"/g, '"')
|
|
253
|
+
.replace(/'/g, ''');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Escapes special regex characters
|
|
258
|
+
* @param {string} str - String to escape
|
|
259
|
+
* @returns {string} Escaped string
|
|
260
|
+
*/
|
|
261
|
+
function escapeRegex(str) {
|
|
262
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
mergeSystemObjectExtension,
|
|
267
|
+
mergeCustomObjectDefinition,
|
|
268
|
+
generateAttributeDefinitionXml,
|
|
269
|
+
escapeXml,
|
|
270
|
+
escapeRegex,
|
|
271
|
+
};
|
package/lib/templates.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML Templates for SFCC migrations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
6
|
+
const METADATA_NAMESPACE =
|
|
7
|
+
'http://www.demandware.com/xml/impex/metadata/2006-10-31';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wraps content in metadata root element
|
|
11
|
+
* @param {string} content - Inner XML content
|
|
12
|
+
* @returns {string} Complete XML document
|
|
13
|
+
*/
|
|
14
|
+
function wrapInMetadata(content) {
|
|
15
|
+
return `${XML_HEADER}
|
|
16
|
+
<metadata xmlns="${METADATA_NAMESPACE}">
|
|
17
|
+
${content}
|
|
18
|
+
</metadata>
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generates attribute definition XML based on type
|
|
24
|
+
* @param {Object} attr - Attribute configuration
|
|
25
|
+
* @returns {string} XML string
|
|
26
|
+
*/
|
|
27
|
+
function generateAttributeDefinition(attr) {
|
|
28
|
+
const {
|
|
29
|
+
id,
|
|
30
|
+
displayName = id,
|
|
31
|
+
description = '',
|
|
32
|
+
type = 'string',
|
|
33
|
+
localizable = false,
|
|
34
|
+
mandatory = false,
|
|
35
|
+
externallyManaged = false,
|
|
36
|
+
defaultValue = null,
|
|
37
|
+
minLength = 0,
|
|
38
|
+
enumValues = [],
|
|
39
|
+
} = attr;
|
|
40
|
+
|
|
41
|
+
let typeSpecificXml = '';
|
|
42
|
+
|
|
43
|
+
switch (type) {
|
|
44
|
+
case 'string':
|
|
45
|
+
case 'text':
|
|
46
|
+
case 'html':
|
|
47
|
+
case 'password':
|
|
48
|
+
typeSpecificXml = `
|
|
49
|
+
<min-length>${minLength}</min-length>`;
|
|
50
|
+
break;
|
|
51
|
+
case 'int':
|
|
52
|
+
case 'double':
|
|
53
|
+
if (attr.minValue !== undefined) {
|
|
54
|
+
typeSpecificXml += `
|
|
55
|
+
<min-value>${attr.minValue}</min-value>`;
|
|
56
|
+
}
|
|
57
|
+
if (attr.maxValue !== undefined) {
|
|
58
|
+
typeSpecificXml += `
|
|
59
|
+
<max-value>${attr.maxValue}</max-value>`;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case 'enum-of-string':
|
|
63
|
+
case 'enum-of-int':
|
|
64
|
+
typeSpecificXml = `
|
|
65
|
+
<select-multiple-flag>${attr.multiSelect || false}</select-multiple-flag>
|
|
66
|
+
<value-definitions>
|
|
67
|
+
${enumValues
|
|
68
|
+
.map(
|
|
69
|
+
(v) => ` <value-definition>
|
|
70
|
+
<display xml:lang="x-default">${v.display || v.value}</display>
|
|
71
|
+
<value>${v.value}</value>
|
|
72
|
+
</value-definition>`,
|
|
73
|
+
)
|
|
74
|
+
.join('\n')}
|
|
75
|
+
</value-definitions>`;
|
|
76
|
+
break;
|
|
77
|
+
case 'set-of-string':
|
|
78
|
+
case 'set-of-int':
|
|
79
|
+
case 'set-of-double':
|
|
80
|
+
break;
|
|
81
|
+
case 'boolean':
|
|
82
|
+
break;
|
|
83
|
+
case 'date':
|
|
84
|
+
case 'datetime':
|
|
85
|
+
break;
|
|
86
|
+
case 'image':
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultValueXml =
|
|
91
|
+
defaultValue !== null
|
|
92
|
+
? `
|
|
93
|
+
<default-value>${defaultValue}</default-value>`
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
const descriptionXml = description
|
|
97
|
+
? `
|
|
98
|
+
<description xml:lang="x-default">${escapeXml(description)}</description>`
|
|
99
|
+
: '';
|
|
100
|
+
|
|
101
|
+
return ` <attribute-definition attribute-id="${id}">
|
|
102
|
+
<display-name xml:lang="x-default">${escapeXml(displayName)}</display-name>${descriptionXml}
|
|
103
|
+
<type>${type}</type>
|
|
104
|
+
<localizable-flag>${localizable}</localizable-flag>
|
|
105
|
+
<mandatory-flag>${mandatory}</mandatory-flag>
|
|
106
|
+
<externally-managed-flag>${externallyManaged}</externally-managed-flag>${typeSpecificXml}${defaultValueXml}
|
|
107
|
+
</attribute-definition>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generates custom object type definition XML
|
|
112
|
+
* @param {Object} config - Custom object configuration
|
|
113
|
+
* @returns {string} XML document
|
|
114
|
+
*/
|
|
115
|
+
function generateCustomObjectDefinition(config) {
|
|
116
|
+
const {
|
|
117
|
+
typeId,
|
|
118
|
+
displayName = typeId,
|
|
119
|
+
description = '',
|
|
120
|
+
stagingMode = 'source-to-target',
|
|
121
|
+
storageScope = 'site',
|
|
122
|
+
keyDefinition,
|
|
123
|
+
attributes = [],
|
|
124
|
+
groupId = typeId,
|
|
125
|
+
} = config;
|
|
126
|
+
|
|
127
|
+
const keyDef = keyDefinition || {
|
|
128
|
+
id: 'key',
|
|
129
|
+
type: 'string',
|
|
130
|
+
minLength: 0,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const keyDescriptionXml = keyDef.description
|
|
134
|
+
? `
|
|
135
|
+
<description xml:lang="x-default">${escapeXml(keyDef.description)}</description>`
|
|
136
|
+
: '';
|
|
137
|
+
|
|
138
|
+
const keyDisplayNameXml = keyDef.displayName
|
|
139
|
+
? `
|
|
140
|
+
<display-name xml:lang="x-default">${escapeXml(keyDef.displayName)}</display-name>`
|
|
141
|
+
: '';
|
|
142
|
+
|
|
143
|
+
const descriptionXml = description
|
|
144
|
+
? `
|
|
145
|
+
<description xml:lang="x-default">${escapeXml(description)}</description>`
|
|
146
|
+
: '';
|
|
147
|
+
|
|
148
|
+
const attributeDefinitions =
|
|
149
|
+
attributes.length > 0
|
|
150
|
+
? `
|
|
151
|
+
<attribute-definitions>
|
|
152
|
+
${attributes.map((attr) => generateAttributeDefinition(attr)).join('\n')}
|
|
153
|
+
</attribute-definitions>`
|
|
154
|
+
: '';
|
|
155
|
+
|
|
156
|
+
const allAttributeIds = [keyDef.id, ...attributes.map((a) => a.id)];
|
|
157
|
+
const groupAttributes = allAttributeIds
|
|
158
|
+
.map((id) => ` <attribute attribute-id="${id}"/>`)
|
|
159
|
+
.join('\n');
|
|
160
|
+
|
|
161
|
+
return wrapInMetadata(` <custom-type type-id="${typeId}">
|
|
162
|
+
<display-name xml:lang="x-default">${escapeXml(displayName)}</display-name>${descriptionXml}
|
|
163
|
+
<staging-mode>${stagingMode}</staging-mode>
|
|
164
|
+
<storage-scope>${storageScope}</storage-scope>
|
|
165
|
+
<key-definition attribute-id="${keyDef.id}">${keyDisplayNameXml}${keyDescriptionXml}
|
|
166
|
+
<type>${keyDef.type || 'string'}</type>
|
|
167
|
+
<min-length>${keyDef.minLength || 0}</min-length>
|
|
168
|
+
</key-definition>${attributeDefinitions}
|
|
169
|
+
<group-definitions>
|
|
170
|
+
<attribute-group group-id="${groupId}">
|
|
171
|
+
<display-name xml:lang="x-default">${escapeXml(displayName)}</display-name>
|
|
172
|
+
${groupAttributes}
|
|
173
|
+
</attribute-group>
|
|
174
|
+
</group-definitions>
|
|
175
|
+
</custom-type>`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generates system object type extension XML for site preferences
|
|
180
|
+
* @param {Object} config - Site preference configuration
|
|
181
|
+
* @returns {string} XML document
|
|
182
|
+
*/
|
|
183
|
+
function generateSitePreference(config) {
|
|
184
|
+
const {
|
|
185
|
+
attributeId,
|
|
186
|
+
displayName = attributeId,
|
|
187
|
+
description = '',
|
|
188
|
+
type = 'string',
|
|
189
|
+
mandatory = false,
|
|
190
|
+
externallyManaged = false,
|
|
191
|
+
defaultValue = null,
|
|
192
|
+
groupId = 'Custom',
|
|
193
|
+
groupDisplayName = groupId,
|
|
194
|
+
existingGroupAttributes = [],
|
|
195
|
+
enumValues = [],
|
|
196
|
+
multiSelect = false,
|
|
197
|
+
minLength = 0,
|
|
198
|
+
} = config;
|
|
199
|
+
|
|
200
|
+
const attr = {
|
|
201
|
+
id: attributeId,
|
|
202
|
+
displayName,
|
|
203
|
+
description,
|
|
204
|
+
type,
|
|
205
|
+
localizable: false,
|
|
206
|
+
mandatory,
|
|
207
|
+
externallyManaged,
|
|
208
|
+
defaultValue,
|
|
209
|
+
enumValues,
|
|
210
|
+
multiSelect,
|
|
211
|
+
minLength,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const allAttributes = [...existingGroupAttributes, attributeId];
|
|
215
|
+
const groupAttributes = allAttributes
|
|
216
|
+
.map((id) => ` <attribute attribute-id="${id}"/>`)
|
|
217
|
+
.join('\n');
|
|
218
|
+
|
|
219
|
+
return wrapInMetadata(` <type-extension type-id="SitePreferences">
|
|
220
|
+
<custom-attribute-definitions>
|
|
221
|
+
${generateAttributeDefinition(attr)}
|
|
222
|
+
</custom-attribute-definitions>
|
|
223
|
+
<group-definitions>
|
|
224
|
+
<attribute-group group-id="${groupId}">
|
|
225
|
+
<display-name xml:lang="x-default">${escapeXml(groupDisplayName)}</display-name>
|
|
226
|
+
${groupAttributes}
|
|
227
|
+
</attribute-group>
|
|
228
|
+
</group-definitions>
|
|
229
|
+
</type-extension>`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Generates system object extension XML
|
|
234
|
+
* @param {Object} config - System object extension configuration
|
|
235
|
+
* @returns {string} XML document
|
|
236
|
+
*/
|
|
237
|
+
function generateSystemObjectExtension(config) {
|
|
238
|
+
const {
|
|
239
|
+
objectTypeId,
|
|
240
|
+
attributeId,
|
|
241
|
+
displayName = attributeId,
|
|
242
|
+
description = '',
|
|
243
|
+
type = 'string',
|
|
244
|
+
mandatory = false,
|
|
245
|
+
externallyManaged = false,
|
|
246
|
+
defaultValue = null,
|
|
247
|
+
groupId,
|
|
248
|
+
groupDisplayName,
|
|
249
|
+
existingGroupAttributes = [],
|
|
250
|
+
enumValues = [],
|
|
251
|
+
multiSelect = false,
|
|
252
|
+
minLength = 0,
|
|
253
|
+
localizable = false,
|
|
254
|
+
} = config;
|
|
255
|
+
|
|
256
|
+
const attr = {
|
|
257
|
+
id: attributeId,
|
|
258
|
+
displayName,
|
|
259
|
+
description,
|
|
260
|
+
type,
|
|
261
|
+
localizable,
|
|
262
|
+
mandatory,
|
|
263
|
+
externallyManaged,
|
|
264
|
+
defaultValue,
|
|
265
|
+
enumValues,
|
|
266
|
+
multiSelect,
|
|
267
|
+
minLength,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const allAttributes = [...existingGroupAttributes, attributeId];
|
|
271
|
+
const groupAttributes = allAttributes
|
|
272
|
+
.map((id) => ` <attribute attribute-id="${id}"/>`)
|
|
273
|
+
.join('\n');
|
|
274
|
+
|
|
275
|
+
const finalGroupId = groupId || `${objectTypeId}_Custom`;
|
|
276
|
+
const finalGroupDisplayName = groupDisplayName || finalGroupId;
|
|
277
|
+
|
|
278
|
+
return wrapInMetadata(` <type-extension type-id="${objectTypeId}">
|
|
279
|
+
<custom-attribute-definitions>
|
|
280
|
+
${generateAttributeDefinition(attr)}
|
|
281
|
+
</custom-attribute-definitions>
|
|
282
|
+
<group-definitions>
|
|
283
|
+
<attribute-group group-id="${finalGroupId}">
|
|
284
|
+
<display-name xml:lang="x-default">${escapeXml(finalGroupDisplayName)}</display-name>
|
|
285
|
+
${groupAttributes}
|
|
286
|
+
</attribute-group>
|
|
287
|
+
</group-definitions>
|
|
288
|
+
</type-extension>`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Escapes special XML characters
|
|
293
|
+
* @param {string} str - String to escape
|
|
294
|
+
* @returns {string} Escaped string
|
|
295
|
+
*/
|
|
296
|
+
function escapeXml(str) {
|
|
297
|
+
if (!str) return '';
|
|
298
|
+
return str
|
|
299
|
+
.replace(/&/g, '&')
|
|
300
|
+
.replace(/</g, '<')
|
|
301
|
+
.replace(/>/g, '>')
|
|
302
|
+
.replace(/"/g, '"')
|
|
303
|
+
.replace(/'/g, ''');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
XML_HEADER,
|
|
308
|
+
METADATA_NAMESPACE,
|
|
309
|
+
wrapInMetadata,
|
|
310
|
+
generateAttributeDefinition,
|
|
311
|
+
generateCustomObjectDefinition,
|
|
312
|
+
generateSitePreference,
|
|
313
|
+
generateSystemObjectExtension,
|
|
314
|
+
escapeXml,
|
|
315
|
+
};
|