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,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Object Command
|
|
3
|
+
* Creates custom object type 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 { generateCustomObjectDefinition } = require('../lib/templates');
|
|
17
|
+
|
|
18
|
+
const ATTRIBUTE_TYPES = [
|
|
19
|
+
{ name: 'String', value: 'string' },
|
|
20
|
+
{ name: 'Text (multi-line)', value: 'text' },
|
|
21
|
+
{ name: 'HTML', value: 'html' },
|
|
22
|
+
{ name: 'Integer', value: 'int' },
|
|
23
|
+
{ name: 'Double/Decimal', value: 'double' },
|
|
24
|
+
{ name: 'Boolean', value: 'boolean' },
|
|
25
|
+
{ name: 'Date', value: 'date' },
|
|
26
|
+
{ name: 'Date & Time', value: 'datetime' },
|
|
27
|
+
{ name: 'Enum (String)', value: 'enum-of-string' },
|
|
28
|
+
{ name: 'Enum (Integer)', value: 'enum-of-int' },
|
|
29
|
+
{ name: 'Set of Strings', value: 'set-of-string' },
|
|
30
|
+
{ name: 'Image', value: 'image' },
|
|
31
|
+
{ name: 'Password', value: 'password' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const STORAGE_SCOPES = [
|
|
35
|
+
{ name: 'Site (per-site data)', value: 'site' },
|
|
36
|
+
{ name: 'Organization (shared across sites)', value: 'organization' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const STAGING_MODES = [
|
|
40
|
+
{ name: 'Source to Target', value: 'source-to-target' },
|
|
41
|
+
{ name: 'No Staging', value: 'no-staging' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
command: 'custom-object',
|
|
46
|
+
aliases: ['co', 'customobject'],
|
|
47
|
+
desc: 'Create a custom object type definition',
|
|
48
|
+
builder: (yargs) => {
|
|
49
|
+
return yargs
|
|
50
|
+
.option('migration', {
|
|
51
|
+
alias: 'm',
|
|
52
|
+
type: 'string',
|
|
53
|
+
description:
|
|
54
|
+
'Target migration folder (uses latest if not specified)',
|
|
55
|
+
})
|
|
56
|
+
.option('type-id', {
|
|
57
|
+
alias: 't',
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Custom object type ID',
|
|
60
|
+
})
|
|
61
|
+
.option('display-name', {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Display name for the custom object',
|
|
64
|
+
})
|
|
65
|
+
.option('description', {
|
|
66
|
+
alias: 'd',
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Description for the custom object',
|
|
69
|
+
})
|
|
70
|
+
.option('storage-scope', {
|
|
71
|
+
type: 'string',
|
|
72
|
+
choices: ['site', 'organization'],
|
|
73
|
+
default: 'site',
|
|
74
|
+
description: 'Storage scope',
|
|
75
|
+
})
|
|
76
|
+
.option('staging-mode', {
|
|
77
|
+
type: 'string',
|
|
78
|
+
choices: ['source-to-target', 'no-staging'],
|
|
79
|
+
default: 'source-to-target',
|
|
80
|
+
description: 'Staging mode',
|
|
81
|
+
})
|
|
82
|
+
.option('key-id', {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Key attribute ID',
|
|
85
|
+
})
|
|
86
|
+
.option('key-type', {
|
|
87
|
+
type: 'string',
|
|
88
|
+
choices: ['string', 'int'],
|
|
89
|
+
default: 'string',
|
|
90
|
+
description: 'Key attribute type',
|
|
91
|
+
})
|
|
92
|
+
.option('interactive', {
|
|
93
|
+
alias: 'i',
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
default: true,
|
|
96
|
+
description: 'Interactive mode with prompts',
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
handler: async (argv) => {
|
|
100
|
+
const migrationsDir = path.resolve(argv.migrationsDir);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const existingMigrations = listMigrations(migrationsDir);
|
|
104
|
+
|
|
105
|
+
if (existingMigrations.length === 0) {
|
|
106
|
+
console.log(
|
|
107
|
+
chalk.yellow(
|
|
108
|
+
'\nNo migrations found. Creating a new migration first...',
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
const createCmd = require('./create-migration');
|
|
112
|
+
await createCmd.handler({ ...argv, interactive: true });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let config = {
|
|
117
|
+
typeId: argv.typeId,
|
|
118
|
+
displayName: argv.displayName,
|
|
119
|
+
description: argv.description,
|
|
120
|
+
storageScope: argv.storageScope,
|
|
121
|
+
stagingMode: argv.stagingMode,
|
|
122
|
+
keyDefinition: argv.keyId
|
|
123
|
+
? {
|
|
124
|
+
id: argv.keyId,
|
|
125
|
+
type: argv.keyType || 'string',
|
|
126
|
+
}
|
|
127
|
+
: null,
|
|
128
|
+
attributes: [],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let targetMigration =
|
|
132
|
+
argv.migration || getLatestMigration(migrationsDir);
|
|
133
|
+
|
|
134
|
+
if (argv.interactive) {
|
|
135
|
+
console.log(chalk.cyan('\n=== Custom Object Definition ===\n'));
|
|
136
|
+
|
|
137
|
+
// Select target migration
|
|
138
|
+
const migrationAnswer = await inquirer.prompt([
|
|
139
|
+
{
|
|
140
|
+
type: 'list',
|
|
141
|
+
name: 'migration',
|
|
142
|
+
message: 'Select target migration:',
|
|
143
|
+
choices: [
|
|
144
|
+
...existingMigrations.slice(-10).reverse(),
|
|
145
|
+
new inquirer.Separator(),
|
|
146
|
+
{
|
|
147
|
+
name: '+ Create new migration',
|
|
148
|
+
value: '__new__',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
default: targetMigration,
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
if (migrationAnswer.migration === '__new__') {
|
|
156
|
+
const createCmd = require('./create-migration');
|
|
157
|
+
await createCmd.handler({ ...argv, interactive: true });
|
|
158
|
+
targetMigration = getLatestMigration(migrationsDir);
|
|
159
|
+
} else {
|
|
160
|
+
targetMigration = migrationAnswer.migration;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Basic info
|
|
164
|
+
const basicInfo = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: 'input',
|
|
167
|
+
name: 'typeId',
|
|
168
|
+
message: 'Custom object type ID:',
|
|
169
|
+
validate: (input) =>
|
|
170
|
+
input ? true : 'Type ID is required',
|
|
171
|
+
when: !config.typeId,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'displayName',
|
|
176
|
+
message: 'Display name:',
|
|
177
|
+
default: (answers) => answers.typeId || config.typeId,
|
|
178
|
+
when: !config.displayName,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'input',
|
|
182
|
+
name: 'description',
|
|
183
|
+
message: 'Description (optional):',
|
|
184
|
+
when: !config.description,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
type: 'list',
|
|
188
|
+
name: 'storageScope',
|
|
189
|
+
message: 'Storage scope:',
|
|
190
|
+
choices: STORAGE_SCOPES,
|
|
191
|
+
default: config.storageScope,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'list',
|
|
195
|
+
name: 'stagingMode',
|
|
196
|
+
message: 'Staging mode:',
|
|
197
|
+
choices: STAGING_MODES,
|
|
198
|
+
default: config.stagingMode,
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
config = { ...config, ...basicInfo };
|
|
203
|
+
|
|
204
|
+
// Key definition
|
|
205
|
+
console.log(chalk.cyan('\n--- Key Definition ---'));
|
|
206
|
+
const keyInfo = await inquirer.prompt([
|
|
207
|
+
{
|
|
208
|
+
type: 'input',
|
|
209
|
+
name: 'keyId',
|
|
210
|
+
message: 'Key attribute ID:',
|
|
211
|
+
default: 'key',
|
|
212
|
+
validate: (input) =>
|
|
213
|
+
input ? true : 'Key ID is required',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
type: 'input',
|
|
217
|
+
name: 'keyDisplayName',
|
|
218
|
+
message: 'Key display name:',
|
|
219
|
+
default: (answers) => answers.keyId,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'input',
|
|
223
|
+
name: 'keyDescription',
|
|
224
|
+
message: 'Key description (optional):',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: 'list',
|
|
228
|
+
name: 'keyType',
|
|
229
|
+
message: 'Key type:',
|
|
230
|
+
choices: [
|
|
231
|
+
{ name: 'String', value: 'string' },
|
|
232
|
+
{ name: 'Integer', value: 'int' },
|
|
233
|
+
],
|
|
234
|
+
default: 'string',
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
|
|
238
|
+
config.keyDefinition = {
|
|
239
|
+
id: keyInfo.keyId,
|
|
240
|
+
displayName: keyInfo.keyDisplayName,
|
|
241
|
+
description: keyInfo.keyDescription,
|
|
242
|
+
type: keyInfo.keyType,
|
|
243
|
+
minLength: 0,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Attributes
|
|
247
|
+
console.log(chalk.cyan('\n--- Attributes ---'));
|
|
248
|
+
let addMoreAttributes = true;
|
|
249
|
+
|
|
250
|
+
while (addMoreAttributes) {
|
|
251
|
+
const attrAnswer = await inquirer.prompt([
|
|
252
|
+
{
|
|
253
|
+
type: 'confirm',
|
|
254
|
+
name: 'addAttribute',
|
|
255
|
+
message:
|
|
256
|
+
config.attributes.length === 0
|
|
257
|
+
? 'Add an attribute?'
|
|
258
|
+
: 'Add another attribute?',
|
|
259
|
+
default: config.attributes.length === 0,
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
if (!attrAnswer.addAttribute) {
|
|
264
|
+
addMoreAttributes = false;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const attr = await promptForAttribute();
|
|
269
|
+
config.attributes.push(attr);
|
|
270
|
+
console.log(chalk.green(` ✓ Added attribute: ${attr.id}`));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Generate XML
|
|
275
|
+
const xml = generateCustomObjectDefinition(config);
|
|
276
|
+
|
|
277
|
+
// Write to file
|
|
278
|
+
const metaPath = path.join(migrationsDir, targetMigration, 'meta');
|
|
279
|
+
const filePath = path.join(
|
|
280
|
+
metaPath,
|
|
281
|
+
'custom-objecttype-definitions.xml',
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
ensureDir(metaPath);
|
|
285
|
+
|
|
286
|
+
// Check if file exists
|
|
287
|
+
if (fs.existsSync(filePath)) {
|
|
288
|
+
const { overwrite } = await inquirer.prompt([
|
|
289
|
+
{
|
|
290
|
+
type: 'confirm',
|
|
291
|
+
name: 'overwrite',
|
|
292
|
+
message: `${chalk.yellow('custom-objecttype-definitions.xml already exists.')} Overwrite?`,
|
|
293
|
+
default: false,
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
if (!overwrite) {
|
|
298
|
+
console.log(chalk.yellow('\nOperation cancelled.'));
|
|
299
|
+
console.log(chalk.gray('Generated XML:\n'));
|
|
300
|
+
console.log(xml);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
writeFile(filePath, xml);
|
|
306
|
+
|
|
307
|
+
console.log(
|
|
308
|
+
chalk.green(
|
|
309
|
+
`\n✓ Created custom object definition: ${config.typeId}`,
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
console.log(chalk.cyan(` File: ${filePath}`));
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(chalk.red(`\nError: ${error.message}`));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Prompts for attribute configuration
|
|
322
|
+
* @returns {Promise<Object>} Attribute configuration
|
|
323
|
+
*/
|
|
324
|
+
async function promptForAttribute() {
|
|
325
|
+
const attr = await inquirer.prompt([
|
|
326
|
+
{
|
|
327
|
+
type: 'input',
|
|
328
|
+
name: 'id',
|
|
329
|
+
message: ' Attribute ID:',
|
|
330
|
+
validate: (input) => (input ? true : 'Attribute ID is required'),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
type: 'input',
|
|
334
|
+
name: 'displayName',
|
|
335
|
+
message: ' Display name:',
|
|
336
|
+
default: (answers) => answers.id,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: 'input',
|
|
340
|
+
name: 'description',
|
|
341
|
+
message: ' Description (optional):',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
type: 'list',
|
|
345
|
+
name: 'type',
|
|
346
|
+
message: ' Type:',
|
|
347
|
+
choices: ATTRIBUTE_TYPES,
|
|
348
|
+
default: 'string',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: 'confirm',
|
|
352
|
+
name: 'localizable',
|
|
353
|
+
message: ' Localizable?',
|
|
354
|
+
default: false,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
type: 'confirm',
|
|
358
|
+
name: 'mandatory',
|
|
359
|
+
message: ' Mandatory?',
|
|
360
|
+
default: false,
|
|
361
|
+
},
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
// Type-specific prompts
|
|
365
|
+
if (attr.type === 'enum-of-string' || attr.type === 'enum-of-int') {
|
|
366
|
+
const enumConfig = await inquirer.prompt([
|
|
367
|
+
{
|
|
368
|
+
type: 'confirm',
|
|
369
|
+
name: 'multiSelect',
|
|
370
|
+
message: ' Allow multiple selections?',
|
|
371
|
+
default: false,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
type: 'input',
|
|
375
|
+
name: 'enumValues',
|
|
376
|
+
message:
|
|
377
|
+
' Enum values (comma-separated, format: value:display or just value):',
|
|
378
|
+
filter: (input) => {
|
|
379
|
+
return input
|
|
380
|
+
.split(',')
|
|
381
|
+
.map((v) => {
|
|
382
|
+
const parts = v.trim().split(':');
|
|
383
|
+
return {
|
|
384
|
+
value: parts[0].trim(),
|
|
385
|
+
display: parts[1]
|
|
386
|
+
? parts[1].trim()
|
|
387
|
+
: parts[0].trim(),
|
|
388
|
+
};
|
|
389
|
+
})
|
|
390
|
+
.filter((v) => v.value);
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
]);
|
|
394
|
+
attr.multiSelect = enumConfig.multiSelect;
|
|
395
|
+
attr.enumValues = enumConfig.enumValues;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (attr.type === 'string' || attr.type === 'text') {
|
|
399
|
+
const stringConfig = await inquirer.prompt([
|
|
400
|
+
{
|
|
401
|
+
type: 'input',
|
|
402
|
+
name: 'defaultValue',
|
|
403
|
+
message: ' Default value (optional):',
|
|
404
|
+
},
|
|
405
|
+
]);
|
|
406
|
+
attr.defaultValue = stringConfig.defaultValue || null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (attr.type === 'boolean') {
|
|
410
|
+
const boolConfig = await inquirer.prompt([
|
|
411
|
+
{
|
|
412
|
+
type: 'list',
|
|
413
|
+
name: 'defaultValue',
|
|
414
|
+
message: ' Default value:',
|
|
415
|
+
choices: [
|
|
416
|
+
{ name: 'None', value: null },
|
|
417
|
+
{ name: 'True', value: 'true' },
|
|
418
|
+
{ name: 'False', value: 'false' },
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
attr.defaultValue = boolConfig.defaultValue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return attr;
|
|
426
|
+
}
|