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/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, '&amp;')
250
+ .replace(/</g, '&lt;')
251
+ .replace(/>/g, '&gt;')
252
+ .replace(/"/g, '&quot;')
253
+ .replace(/'/g, '&apos;');
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
+ };
@@ -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, '&amp;')
300
+ .replace(/</g, '&lt;')
301
+ .replace(/>/g, '&gt;')
302
+ .replace(/"/g, '&quot;')
303
+ .replace(/'/g, '&apos;');
304
+ }
305
+
306
+ module.exports = {
307
+ XML_HEADER,
308
+ METADATA_NAMESPACE,
309
+ wrapInMetadata,
310
+ generateAttributeDefinition,
311
+ generateCustomObjectDefinition,
312
+ generateSitePreference,
313
+ generateSystemObjectExtension,
314
+ escapeXml,
315
+ };