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
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Object Extension Command
|
|
3
|
+
* Creates system object custom attribute definitions in migrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const {
|
|
11
|
+
listMigrations,
|
|
12
|
+
getLatestMigration,
|
|
13
|
+
ensureDir,
|
|
14
|
+
writeFile,
|
|
15
|
+
} = require('../lib/utils');
|
|
16
|
+
const { generateSystemObjectExtension } = require('../lib/templates');
|
|
17
|
+
const { mergeSystemObjectExtension } = require('../lib/merge');
|
|
18
|
+
|
|
19
|
+
const ATTRIBUTE_TYPES = [
|
|
20
|
+
{ name: 'String', value: 'string' },
|
|
21
|
+
{ name: 'Text (multi-line)', value: 'text' },
|
|
22
|
+
{ name: 'HTML', value: 'html' },
|
|
23
|
+
{ name: 'Integer', value: 'int' },
|
|
24
|
+
{ name: 'Double/Decimal', value: 'double' },
|
|
25
|
+
{ name: 'Boolean', value: 'boolean' },
|
|
26
|
+
{ name: 'Date', value: 'date' },
|
|
27
|
+
{ name: 'Date & Time', value: 'datetime' },
|
|
28
|
+
{ name: 'Enum (String)', value: 'enum-of-string' },
|
|
29
|
+
{ name: 'Enum (Integer)', value: 'enum-of-int' },
|
|
30
|
+
{ name: 'Set of Strings', value: 'set-of-string' },
|
|
31
|
+
{ name: 'Image', value: 'image' },
|
|
32
|
+
{ name: 'Password', value: 'password' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Common SFCC system objects
|
|
36
|
+
const SYSTEM_OBJECTS = [
|
|
37
|
+
{ name: 'Order', value: 'Order' },
|
|
38
|
+
{ name: 'Product', value: 'Product' },
|
|
39
|
+
{ name: 'Category', value: 'Category' },
|
|
40
|
+
{ name: 'Customer', value: 'Profile' },
|
|
41
|
+
{ name: 'Basket', value: 'Basket' },
|
|
42
|
+
{ name: 'ProductLineItem', value: 'ProductLineItem' },
|
|
43
|
+
{ name: 'ShippingLineItem', value: 'ShippingLineItem' },
|
|
44
|
+
{ name: 'PaymentInstrument', value: 'PaymentInstrument' },
|
|
45
|
+
{ name: 'OrderPaymentInstrument', value: 'OrderPaymentInstrument' },
|
|
46
|
+
{ name: 'Shipment', value: 'Shipment' },
|
|
47
|
+
{ name: 'Store', value: 'Store' },
|
|
48
|
+
{ name: 'Content', value: 'Content' },
|
|
49
|
+
{ name: 'Folder', value: 'Folder' },
|
|
50
|
+
{ name: 'Catalog', value: 'Catalog' },
|
|
51
|
+
{ name: 'PriceBook', value: 'PriceBook' },
|
|
52
|
+
{ name: 'SourceCodeGroup', value: 'SourceCodeGroup' },
|
|
53
|
+
{ name: 'Campaign', value: 'Campaign' },
|
|
54
|
+
{ name: 'Promotion', value: 'Promotion' },
|
|
55
|
+
{ name: 'Coupon', value: 'Coupon' },
|
|
56
|
+
{ name: 'GiftCertificate', value: 'GiftCertificate' },
|
|
57
|
+
{ name: 'SitePreferences', value: 'SitePreferences' },
|
|
58
|
+
{ name: 'OrganizationPreferences', value: 'OrganizationPreferences' },
|
|
59
|
+
new inquirer.Separator(),
|
|
60
|
+
{ name: '+ Enter custom object type', value: '__custom__' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
command: 'system-object',
|
|
65
|
+
aliases: ['so', 'sysobj', 'extend'],
|
|
66
|
+
desc: 'Extend a system object with custom attributes',
|
|
67
|
+
builder: (yargs) => {
|
|
68
|
+
return yargs
|
|
69
|
+
.option('migration', {
|
|
70
|
+
alias: 'm',
|
|
71
|
+
type: 'string',
|
|
72
|
+
description:
|
|
73
|
+
'Target migration folder (uses latest if not specified)',
|
|
74
|
+
})
|
|
75
|
+
.option('object-type', {
|
|
76
|
+
alias: 'o',
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'System object type ID (e.g., Order, Product)',
|
|
79
|
+
})
|
|
80
|
+
.option('attribute-id', {
|
|
81
|
+
alias: 'a',
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Attribute ID',
|
|
84
|
+
})
|
|
85
|
+
.option('display-name', {
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: 'Display name for the attribute',
|
|
88
|
+
})
|
|
89
|
+
.option('description', {
|
|
90
|
+
alias: 'd',
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Description for the attribute',
|
|
93
|
+
})
|
|
94
|
+
.option('type', {
|
|
95
|
+
alias: 't',
|
|
96
|
+
type: 'string',
|
|
97
|
+
choices: [
|
|
98
|
+
'string',
|
|
99
|
+
'text',
|
|
100
|
+
'html',
|
|
101
|
+
'int',
|
|
102
|
+
'double',
|
|
103
|
+
'boolean',
|
|
104
|
+
'date',
|
|
105
|
+
'datetime',
|
|
106
|
+
'enum-of-string',
|
|
107
|
+
'set-of-string',
|
|
108
|
+
],
|
|
109
|
+
default: 'string',
|
|
110
|
+
description: 'Attribute type',
|
|
111
|
+
})
|
|
112
|
+
.option('group', {
|
|
113
|
+
alias: 'g',
|
|
114
|
+
type: 'string',
|
|
115
|
+
description: 'Attribute group ID',
|
|
116
|
+
})
|
|
117
|
+
.option('interactive', {
|
|
118
|
+
alias: 'i',
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
default: true,
|
|
121
|
+
description: 'Interactive mode with prompts',
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
handler: async (argv) => {
|
|
125
|
+
const migrationsDir = path.resolve(argv.migrationsDir);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const existingMigrations = listMigrations(migrationsDir);
|
|
129
|
+
|
|
130
|
+
if (existingMigrations.length === 0) {
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.yellow(
|
|
133
|
+
'\nNo migrations found. Creating a new migration first...',
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
const createCmd = require('./create-migration');
|
|
137
|
+
await createCmd.handler({ ...argv, interactive: true });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let config = {
|
|
142
|
+
objectTypeId: argv.objectType,
|
|
143
|
+
attributeId: argv.attributeId,
|
|
144
|
+
displayName: argv.displayName,
|
|
145
|
+
description: argv.description,
|
|
146
|
+
type: argv.type,
|
|
147
|
+
groupId: argv.group,
|
|
148
|
+
existingGroupAttributes: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let targetMigration =
|
|
152
|
+
argv.migration || getLatestMigration(migrationsDir);
|
|
153
|
+
|
|
154
|
+
if (argv.interactive) {
|
|
155
|
+
console.log(chalk.cyan('\n=== System Object Extension ===\n'));
|
|
156
|
+
|
|
157
|
+
// Select target migration
|
|
158
|
+
const migrationAnswer = await inquirer.prompt([
|
|
159
|
+
{
|
|
160
|
+
type: 'list',
|
|
161
|
+
name: 'migration',
|
|
162
|
+
message: 'Select target migration:',
|
|
163
|
+
choices: [
|
|
164
|
+
...existingMigrations.slice(-10).reverse(),
|
|
165
|
+
new inquirer.Separator(),
|
|
166
|
+
{
|
|
167
|
+
name: '+ Create new migration',
|
|
168
|
+
value: '__new__',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
default: targetMigration,
|
|
172
|
+
},
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
if (migrationAnswer.migration === '__new__') {
|
|
176
|
+
const createCmd = require('./create-migration');
|
|
177
|
+
await createCmd.handler({ ...argv, interactive: true });
|
|
178
|
+
targetMigration = getLatestMigration(migrationsDir);
|
|
179
|
+
} else {
|
|
180
|
+
targetMigration = migrationAnswer.migration;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Parse existing groups from migrations
|
|
184
|
+
const existingGroups = await parseExistingGroups(migrationsDir);
|
|
185
|
+
|
|
186
|
+
// Select system object type
|
|
187
|
+
const objectAnswer = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: 'list',
|
|
190
|
+
name: 'objectTypeId',
|
|
191
|
+
message: 'Select system object type:',
|
|
192
|
+
choices: SYSTEM_OBJECTS,
|
|
193
|
+
when: !config.objectTypeId,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: 'input',
|
|
197
|
+
name: 'customObjectType',
|
|
198
|
+
message: 'Enter object type ID:',
|
|
199
|
+
when: (answers) =>
|
|
200
|
+
answers.objectTypeId === '__custom__',
|
|
201
|
+
validate: (input) =>
|
|
202
|
+
input ? true : 'Object type ID is required',
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
config.objectTypeId =
|
|
207
|
+
objectAnswer.customObjectType ||
|
|
208
|
+
objectAnswer.objectTypeId ||
|
|
209
|
+
config.objectTypeId;
|
|
210
|
+
|
|
211
|
+
// Basic info
|
|
212
|
+
const basicInfo = await inquirer.prompt([
|
|
213
|
+
{
|
|
214
|
+
type: 'input',
|
|
215
|
+
name: 'attributeId',
|
|
216
|
+
message: 'Attribute ID (e.g., customAttribute):',
|
|
217
|
+
validate: (input) => {
|
|
218
|
+
if (!input) return 'Attribute ID is required';
|
|
219
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input)) {
|
|
220
|
+
return 'Invalid attribute ID format';
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
},
|
|
224
|
+
when: !config.attributeId,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: 'input',
|
|
228
|
+
name: 'displayName',
|
|
229
|
+
message: 'Display name:',
|
|
230
|
+
default: (answers) => {
|
|
231
|
+
const id =
|
|
232
|
+
answers.attributeId || config.attributeId;
|
|
233
|
+
return id
|
|
234
|
+
.replace(/_/g, ' ')
|
|
235
|
+
.replace(/([A-Z])/g, ' $1')
|
|
236
|
+
.trim()
|
|
237
|
+
.split(' ')
|
|
238
|
+
.map(
|
|
239
|
+
(w) =>
|
|
240
|
+
w.charAt(0).toUpperCase() + w.slice(1),
|
|
241
|
+
)
|
|
242
|
+
.join(' ');
|
|
243
|
+
},
|
|
244
|
+
when: !config.displayName,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: 'input',
|
|
248
|
+
name: 'description',
|
|
249
|
+
message: 'Description (optional):',
|
|
250
|
+
when: !config.description,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: 'list',
|
|
254
|
+
name: 'type',
|
|
255
|
+
message: 'Attribute type:',
|
|
256
|
+
choices: ATTRIBUTE_TYPES,
|
|
257
|
+
default: config.type || 'string',
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
type: 'confirm',
|
|
261
|
+
name: 'localizable',
|
|
262
|
+
message: 'Is localizable?',
|
|
263
|
+
default: false,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: 'confirm',
|
|
267
|
+
name: 'mandatory',
|
|
268
|
+
message: 'Is mandatory?',
|
|
269
|
+
default: false,
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
config = { ...config, ...basicInfo };
|
|
274
|
+
|
|
275
|
+
// Type-specific prompts
|
|
276
|
+
if (
|
|
277
|
+
config.type === 'enum-of-string' ||
|
|
278
|
+
config.type === 'enum-of-int'
|
|
279
|
+
) {
|
|
280
|
+
const enumConfig = await inquirer.prompt([
|
|
281
|
+
{
|
|
282
|
+
type: 'confirm',
|
|
283
|
+
name: 'multiSelect',
|
|
284
|
+
message: 'Allow multiple selections?',
|
|
285
|
+
default: false,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: 'input',
|
|
289
|
+
name: 'enumValues',
|
|
290
|
+
message:
|
|
291
|
+
'Enum values (comma-separated, format: value:display or just value):',
|
|
292
|
+
filter: (input) => {
|
|
293
|
+
return input
|
|
294
|
+
.split(',')
|
|
295
|
+
.map((v) => {
|
|
296
|
+
const parts = v.trim().split(':');
|
|
297
|
+
return {
|
|
298
|
+
value: parts[0].trim(),
|
|
299
|
+
display: parts[1]
|
|
300
|
+
? parts[1].trim()
|
|
301
|
+
: parts[0].trim(),
|
|
302
|
+
};
|
|
303
|
+
})
|
|
304
|
+
.filter((v) => v.value);
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
config.multiSelect = enumConfig.multiSelect;
|
|
309
|
+
config.enumValues = enumConfig.enumValues;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Default value
|
|
313
|
+
const defaultPrompt = await inquirer.prompt([
|
|
314
|
+
{
|
|
315
|
+
type: config.type === 'boolean' ? 'list' : 'input',
|
|
316
|
+
name: 'defaultValue',
|
|
317
|
+
message: 'Default value:',
|
|
318
|
+
choices:
|
|
319
|
+
config.type === 'boolean'
|
|
320
|
+
? [
|
|
321
|
+
{ name: 'None', value: null },
|
|
322
|
+
{ name: 'True', value: 'true' },
|
|
323
|
+
{ name: 'False', value: 'false' },
|
|
324
|
+
]
|
|
325
|
+
: undefined,
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
config.defaultValue = defaultPrompt.defaultValue || null;
|
|
330
|
+
|
|
331
|
+
// Group selection
|
|
332
|
+
const objectGroups = existingGroups[config.objectTypeId] || {};
|
|
333
|
+
const existingObjectGroupIds = Object.keys(objectGroups);
|
|
334
|
+
const groupChoices = [
|
|
335
|
+
{
|
|
336
|
+
name: `${config.objectTypeId}_Custom`,
|
|
337
|
+
value: `${config.objectTypeId}_Custom`,
|
|
338
|
+
},
|
|
339
|
+
...existingObjectGroupIds.map((g) => ({
|
|
340
|
+
name: g,
|
|
341
|
+
value: g,
|
|
342
|
+
})),
|
|
343
|
+
new inquirer.Separator(),
|
|
344
|
+
{ name: '+ Create new group', value: '__new__' },
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
// Remove duplicates
|
|
348
|
+
const uniqueChoices = groupChoices.filter(
|
|
349
|
+
(choice, index, self) =>
|
|
350
|
+
choice.type === 'separator' ||
|
|
351
|
+
index ===
|
|
352
|
+
self.findIndex((c) => c.value === choice.value),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const groupAnswer = await inquirer.prompt([
|
|
356
|
+
{
|
|
357
|
+
type: 'list',
|
|
358
|
+
name: 'groupId',
|
|
359
|
+
message: 'Select attribute group:',
|
|
360
|
+
choices: uniqueChoices,
|
|
361
|
+
default: `${config.objectTypeId}_Custom`,
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
type: 'input',
|
|
365
|
+
name: 'newGroupId',
|
|
366
|
+
message: 'New group ID:',
|
|
367
|
+
when: (answers) => answers.groupId === '__new__',
|
|
368
|
+
validate: (input) =>
|
|
369
|
+
input ? true : 'Group ID is required',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
type: 'input',
|
|
373
|
+
name: 'newGroupDisplayName',
|
|
374
|
+
message: 'New group display name:',
|
|
375
|
+
when: (answers) => answers.groupId === '__new__',
|
|
376
|
+
default: (answers) => answers.newGroupId,
|
|
377
|
+
},
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
if (groupAnswer.groupId === '__new__') {
|
|
381
|
+
config.groupId = groupAnswer.newGroupId;
|
|
382
|
+
config.groupDisplayName = groupAnswer.newGroupDisplayName;
|
|
383
|
+
config.existingGroupAttributes = [];
|
|
384
|
+
} else {
|
|
385
|
+
config.groupId = groupAnswer.groupId;
|
|
386
|
+
config.groupDisplayName = groupAnswer.groupId;
|
|
387
|
+
config.existingGroupAttributes =
|
|
388
|
+
objectGroups[groupAnswer.groupId] || [];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Generate XML
|
|
393
|
+
const xml = generateSystemObjectExtension(config);
|
|
394
|
+
|
|
395
|
+
// Write to file
|
|
396
|
+
const metaPath = path.join(migrationsDir, targetMigration, 'meta');
|
|
397
|
+
const filePath = path.join(
|
|
398
|
+
metaPath,
|
|
399
|
+
'system-objecttype-extensions.xml',
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
ensureDir(metaPath);
|
|
403
|
+
|
|
404
|
+
// Check if file exists
|
|
405
|
+
if (fs.existsSync(filePath)) {
|
|
406
|
+
console.log(
|
|
407
|
+
chalk.yellow(
|
|
408
|
+
`\nNote: ${chalk.bold('system-objecttype-extensions.xml')} already exists.`,
|
|
409
|
+
),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const { action } = await inquirer.prompt([
|
|
413
|
+
{
|
|
414
|
+
type: 'list',
|
|
415
|
+
name: 'action',
|
|
416
|
+
message: 'What would you like to do?',
|
|
417
|
+
choices: [
|
|
418
|
+
{
|
|
419
|
+
name: 'Merge into existing file',
|
|
420
|
+
value: 'merge',
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: 'Show generated XML (copy manually)',
|
|
424
|
+
value: 'show',
|
|
425
|
+
},
|
|
426
|
+
{ name: 'Overwrite file', value: 'overwrite' },
|
|
427
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
428
|
+
],
|
|
429
|
+
default: 'merge',
|
|
430
|
+
},
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
if (action === 'cancel') {
|
|
434
|
+
console.log(chalk.yellow('\nOperation cancelled.'));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (action === 'show') {
|
|
439
|
+
console.log(chalk.cyan('\n--- Generated XML ---\n'));
|
|
440
|
+
console.log(xml);
|
|
441
|
+
console.log(chalk.cyan('\n--- End XML ---\n'));
|
|
442
|
+
console.log(chalk.gray(`Manually add to: ${filePath}`));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (action === 'merge') {
|
|
447
|
+
const existingXml = fs.readFileSync(filePath, 'utf8');
|
|
448
|
+
const result = mergeSystemObjectExtension(existingXml, {
|
|
449
|
+
objectTypeId: config.objectTypeId,
|
|
450
|
+
attributeId: config.attributeId,
|
|
451
|
+
displayName: config.displayName,
|
|
452
|
+
description: config.description,
|
|
453
|
+
type: config.type,
|
|
454
|
+
localizable: config.localizable,
|
|
455
|
+
mandatory: config.mandatory,
|
|
456
|
+
externallyManaged: false,
|
|
457
|
+
defaultValue: config.defaultValue,
|
|
458
|
+
groupId: config.groupId,
|
|
459
|
+
groupDisplayName: config.groupDisplayName,
|
|
460
|
+
enumValues: config.enumValues,
|
|
461
|
+
multiSelect: config.multiSelect,
|
|
462
|
+
minLength: config.minLength || 0,
|
|
463
|
+
});
|
|
464
|
+
writeFile(filePath, result.xml);
|
|
465
|
+
console.log(
|
|
466
|
+
chalk.green(
|
|
467
|
+
`\n✓ Merged ${config.objectTypeId} extension: ${config.attributeId}`,
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
console.log(chalk.cyan(` File: ${filePath}`));
|
|
471
|
+
console.log(chalk.gray(` Group: ${config.groupId}`));
|
|
472
|
+
|
|
473
|
+
if (result.groupExisted) {
|
|
474
|
+
console.log(
|
|
475
|
+
chalk.yellow(
|
|
476
|
+
`\n⚠ Warning: Group "${config.groupId}" already existed in this file.`,
|
|
477
|
+
),
|
|
478
|
+
);
|
|
479
|
+
console.log(
|
|
480
|
+
chalk.yellow(
|
|
481
|
+
' Review the group-definitions to ensure all attributes are listed.',
|
|
482
|
+
),
|
|
483
|
+
);
|
|
484
|
+
console.log(
|
|
485
|
+
chalk.gray(
|
|
486
|
+
' The merge only adds the new attribute reference, existing ones are preserved.',
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
writeFile(filePath, xml);
|
|
495
|
+
|
|
496
|
+
console.log(
|
|
497
|
+
chalk.green(
|
|
498
|
+
`\n✓ Created ${config.objectTypeId} extension: ${config.attributeId}`,
|
|
499
|
+
),
|
|
500
|
+
);
|
|
501
|
+
console.log(chalk.cyan(` File: ${filePath}`));
|
|
502
|
+
console.log(chalk.gray(` Group: ${config.groupId}`));
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Parses existing group attributes from migration files
|
|
512
|
+
* @param {string} migrationsDir - Migrations directory
|
|
513
|
+
* @returns {Promise<Object>} Map of object types to groups to attribute arrays
|
|
514
|
+
*/
|
|
515
|
+
async function parseExistingGroups(migrationsDir) {
|
|
516
|
+
const objectGroups = {};
|
|
517
|
+
const migrations = fs.readdirSync(migrationsDir);
|
|
518
|
+
|
|
519
|
+
for (const migration of migrations) {
|
|
520
|
+
const filePath = path.join(
|
|
521
|
+
migrationsDir,
|
|
522
|
+
migration,
|
|
523
|
+
'meta',
|
|
524
|
+
'system-objecttype-extensions.xml',
|
|
525
|
+
);
|
|
526
|
+
if (fs.existsSync(filePath)) {
|
|
527
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
528
|
+
|
|
529
|
+
// Extract type extensions
|
|
530
|
+
const typeMatches = content.matchAll(
|
|
531
|
+
/<type-extension type-id="([^"]+)">([\s\S]*?)<\/type-extension>/g,
|
|
532
|
+
);
|
|
533
|
+
for (const typeMatch of typeMatches) {
|
|
534
|
+
const typeId = typeMatch[1];
|
|
535
|
+
const typeContent = typeMatch[2];
|
|
536
|
+
|
|
537
|
+
if (!objectGroups[typeId]) {
|
|
538
|
+
objectGroups[typeId] = {};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Extract group definitions
|
|
542
|
+
const groupMatches = typeContent.matchAll(
|
|
543
|
+
/<attribute-group group-id="([^"]+)">([\s\S]*?)<\/attribute-group>/g,
|
|
544
|
+
);
|
|
545
|
+
for (const groupMatch of groupMatches) {
|
|
546
|
+
const groupId = groupMatch[1];
|
|
547
|
+
const groupContent = groupMatch[2];
|
|
548
|
+
|
|
549
|
+
if (!objectGroups[typeId][groupId]) {
|
|
550
|
+
objectGroups[typeId][groupId] = [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Extract attribute IDs
|
|
554
|
+
const attrMatches = groupContent.matchAll(
|
|
555
|
+
/<attribute attribute-id="([^"]+)"\/>/g,
|
|
556
|
+
);
|
|
557
|
+
for (const attrMatch of attrMatches) {
|
|
558
|
+
if (
|
|
559
|
+
!objectGroups[typeId][groupId].includes(
|
|
560
|
+
attrMatch[1],
|
|
561
|
+
)
|
|
562
|
+
) {
|
|
563
|
+
objectGroups[typeId][groupId].push(attrMatch[1]);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return objectGroups;
|
|
572
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migration Helper CLI
|
|
4
|
+
* Companion tool to b2c-tools for creating SFCC migrations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const yargs = require('yargs');
|
|
8
|
+
const { hideBin } = require('yargs/helpers');
|
|
9
|
+
|
|
10
|
+
const createMigrationCommand = require('./commands/create-migration');
|
|
11
|
+
const customObjectCommand = require('./commands/custom-object');
|
|
12
|
+
const sitePreferenceCommand = require('./commands/site-preference');
|
|
13
|
+
const systemObjectCommand = require('./commands/system-object');
|
|
14
|
+
|
|
15
|
+
yargs(hideBin(process.argv))
|
|
16
|
+
.scriptName('migration-helper')
|
|
17
|
+
.usage('$0 <command> [options]')
|
|
18
|
+
.command(createMigrationCommand)
|
|
19
|
+
.command(customObjectCommand)
|
|
20
|
+
.command(sitePreferenceCommand)
|
|
21
|
+
.command(systemObjectCommand)
|
|
22
|
+
.demandCommand(1, 'You must specify a command')
|
|
23
|
+
.option('migrations-dir', {
|
|
24
|
+
alias: 'm',
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Path to migrations directory',
|
|
27
|
+
default: './migrations',
|
|
28
|
+
})
|
|
29
|
+
.help()
|
|
30
|
+
.alias('h', 'help')
|
|
31
|
+
.version()
|
|
32
|
+
.alias('v', 'version')
|
|
33
|
+
.epilogue('For more information, see the b2c-tools documentation')
|
|
34
|
+
.parse();
|