@contrail/flexplm 1.7.4-alpha.368c8b1 → 1.7.4-alpha.458d29b
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/interfaces/item-family-changes.d.ts +1 -0
- package/lib/interfaces/item-family-changes.js +2 -0
- package/lib/publish/base-process-publish-assortment.js +33 -6
- package/lib/publish/base-process-publish-assortment.spec.js +209 -0
- package/package.json +2 -3
- package/scripts/copy-template.js +5 -9
- package/lib/cli/commands/config-create.d.ts +0 -5
- package/lib/cli/commands/config-create.js +0 -85
- package/lib/cli/commands/config-create.spec.d.ts +0 -1
- package/lib/cli/commands/config-create.spec.js +0 -80
- package/lib/cli/commands/config-upload.d.ts +0 -19
- package/lib/cli/commands/config-upload.js +0 -249
- package/lib/cli/commands/config-upload.spec.d.ts +0 -1
- package/lib/cli/commands/config-upload.spec.js +0 -94
- package/lib/cli/config-index.d.ts +0 -5
- package/lib/cli/config-index.js +0 -61
- package/lib/cli/config-index.spec.d.ts +0 -1
- package/lib/cli/config-index.spec.js +0 -68
- package/lib/cli/template/config-template.json.template +0 -30
|
@@ -12,6 +12,7 @@ export declare class ItemFamilyChanges {
|
|
|
12
12
|
colorDeletes: string[];
|
|
13
13
|
colorUpdates: string[];
|
|
14
14
|
colorUnchanged: string[];
|
|
15
|
+
colorSuppressed: string[];
|
|
15
16
|
assortmentItemFullChangeMap: Map<string, any>;
|
|
16
17
|
assortmentItemDeleteMap: Map<string, any>;
|
|
17
18
|
itemToFederatedIdMapping: Map<string, string>;
|
|
@@ -14,6 +14,7 @@ class ItemFamilyChanges {
|
|
|
14
14
|
this.colorDeletes = [];
|
|
15
15
|
this.colorUpdates = [];
|
|
16
16
|
this.colorUnchanged = [];
|
|
17
|
+
this.colorSuppressed = []; // changed this window, but excluded by the lifecycle filter
|
|
17
18
|
this.assortmentItemFullChangeMap = new Map();
|
|
18
19
|
this.assortmentItemDeleteMap = new Map();
|
|
19
20
|
this.itemToFederatedIdMapping = new Map();
|
|
@@ -32,6 +33,7 @@ class ItemFamilyChanges {
|
|
|
32
33
|
s += ' colorDeletes: [' + this.colorDeletes + ']\n';
|
|
33
34
|
s += ' colorUpdates: [' + this.colorUpdates + ']\n';
|
|
34
35
|
s += ' colorUnchanged: [' + this.colorUnchanged + ']\n';
|
|
36
|
+
s += ' colorSuppressed: [' + this.colorSuppressed + ']\n';
|
|
35
37
|
s += 'assortmentItemFullChangeMap:\n';
|
|
36
38
|
s += 'size: ' + this.assortmentItemFullChangeMap.size + '\n[';
|
|
37
39
|
for (const key of this.assortmentItemFullChangeMap.keys()) {
|
|
@@ -495,13 +495,18 @@ class BaseProcessPublishAssortment {
|
|
|
495
495
|
const itemFamilyId = item?.itemFamilyId;
|
|
496
496
|
if (meetsCriteria) {
|
|
497
497
|
releasedForDevelopmentItemIds.push(itemId);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
498
|
+
}
|
|
499
|
+
// ---- NEW (Step 5): family resolution moved OUT of the meetsCriteria block.
|
|
500
|
+
// A family whose options are ALL pre-development has no other route into this
|
|
501
|
+
// list when it has no assortment-item row of its own. ----
|
|
502
|
+
if (!itemFamilySet.has(itemFamilyId) && itemId !== itemFamilyId) {
|
|
503
|
+
const familyItem = item?.itemFamily;
|
|
504
|
+
if (familyItem) {
|
|
505
|
+
if (this.meetsCriteria({ item: familyItem })
|
|
506
|
+
&& !releasedForDevelopmentItemIds.includes(itemFamilyId)) {
|
|
502
507
|
releasedForDevelopmentItemIds.push(itemFamilyId);
|
|
503
508
|
}
|
|
504
|
-
itemFamilySet.add(itemFamilyId);
|
|
509
|
+
itemFamilySet.add(itemFamilyId); // only mark resolved when we had data to judge
|
|
505
510
|
}
|
|
506
511
|
}
|
|
507
512
|
}
|
|
@@ -730,7 +735,7 @@ class BaseProcessPublishAssortment {
|
|
|
730
735
|
for (const [itemId, aItem] of assortmentItemFullChangeMap) {
|
|
731
736
|
const projectItem = aItem?.projectItem;
|
|
732
737
|
const itemFamilyId = aItem?.item?.itemFamilyId;
|
|
733
|
-
if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)
|
|
738
|
+
if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)) {
|
|
734
739
|
continue;
|
|
735
740
|
}
|
|
736
741
|
const ifc = itemFamilyChanges.get(itemFamilyId) || new item_family_changes_1.ItemFamilyChanges(itemFamilyId, pcd.sinceDate);
|
|
@@ -742,6 +747,20 @@ class BaseProcessPublishAssortment {
|
|
|
742
747
|
if (pcd.itemToFederatedIdMapping.has(itemId)) {
|
|
743
748
|
ifc.itemToFederatedIdMapping.set(itemId, pcd.itemToFederatedIdMapping.get(itemId));
|
|
744
749
|
}
|
|
750
|
+
// ---- NEW Step to enable ItemFamily publish ET-243: option is pre-development. It must not produce an
|
|
751
|
+
// LCSSKUSeasonLink, but a change to it still makes the family publishable. ----
|
|
752
|
+
if (!pcd.releasedForDevelopmentItemIds.includes(itemId)) {
|
|
753
|
+
const changedInWindow = addIds.includes(itemId)
|
|
754
|
+
|| deleteIds.includes(itemId)
|
|
755
|
+
|| updateIds.includes(itemId)
|
|
756
|
+
|| this.updatedSinceDate(projectItem, pcd.sinceDate)
|
|
757
|
+
|| this.updatedSinceDate(aItem?.item, pcd.sinceDate);
|
|
758
|
+
if (changedInWindow) {
|
|
759
|
+
ifc.colorSuppressed.push(itemId);
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
// NEW Step - ends
|
|
745
764
|
if (itemId === itemFamilyId) {
|
|
746
765
|
if (addIds.includes(itemId))
|
|
747
766
|
ifc.familyAdd = true;
|
|
@@ -757,6 +776,12 @@ class BaseProcessPublishAssortment {
|
|
|
757
776
|
ifc.familyUpdate = true;
|
|
758
777
|
}
|
|
759
778
|
}
|
|
779
|
+
// NEW Step to enable ItemFamily publish ET-243: item-level edits (incl. lifecycleStage) must publish
|
|
780
|
+
// the family. Guarded so the "first option added" flow stays suppressed. ----
|
|
781
|
+
if (!ifc.familyItemRemoved && this.updatedSinceDate(aItem?.item, pcd.sinceDate)) {
|
|
782
|
+
ifc.familyUpdate = true;
|
|
783
|
+
}
|
|
784
|
+
// NEW Step - ends
|
|
760
785
|
if (projectItem && !projectItem?.roles && aItem?.item?.roles) {
|
|
761
786
|
projectItem.roles = aItem?.item?.roles;
|
|
762
787
|
}
|
|
@@ -855,8 +880,10 @@ class BaseProcessPublishAssortment {
|
|
|
855
880
|
const LCSSeason = Object.assign({}, seasonFed);
|
|
856
881
|
const assortment = await this.getAssortment(assortmentId);
|
|
857
882
|
const projectItem = this.getProjectItem(itemFamilyChanges, itemFamilyChanges.itemFamilyId);
|
|
883
|
+
// ---- NEW Step to enable ItemFamily publish ET-243: added color suppressed length to existing condition, no other changes on familyAssortmentItem boolean
|
|
858
884
|
const familyAssortmentItem = itemFamilyChanges.familyAdd || itemFamilyChanges.familyUpdate || itemFamilyChanges.familyDelete
|
|
859
885
|
|| itemFamilyChanges.colorAdds.length > 0 || itemFamilyChanges.colorUpdates.length > 0 || itemFamilyChanges.colorDeletes.length > 0
|
|
886
|
+
|| itemFamilyChanges.colorSuppressed.length > 0
|
|
860
887
|
|| (projectItem && this.updatedSinceDate(projectItem, itemFamilyChanges.sinceDate));
|
|
861
888
|
//familyItemRemoved is used when adding the first option to an assortment
|
|
862
889
|
//and will have updates for the family item.
|
|
@@ -1337,6 +1337,151 @@ describe('getItemFamilyChanges', () => {
|
|
|
1337
1337
|
expect(ifc.colorDeletes.length).toEqual(1);
|
|
1338
1338
|
});
|
|
1339
1339
|
});
|
|
1340
|
+
describe('getItemFamilyChanges - lifecycle-suppressed options', () => {
|
|
1341
|
+
const familyId = 'AERfdvdAt1HHEVvQ';
|
|
1342
|
+
const maroonId = 'YwWKVoL_vGN8khLy';
|
|
1343
|
+
const limeId = 'rsmWhWiXRNv_XBJG';
|
|
1344
|
+
const sinceDate = new Date('2026-08-01T00:00:00.000Z');
|
|
1345
|
+
const afterSince = '2026-08-26T00:00:00.000Z';
|
|
1346
|
+
const beforeSince = '2026-05-11T00:00:00.000Z';
|
|
1347
|
+
const noDetailChanges = () => ({ adds: [], deletes: [], updates: [], familyItemsRemoved: [] });
|
|
1348
|
+
const buildPpa = () => {
|
|
1349
|
+
const config = { itemPreDevelopmentLifecycleStages: ['concept'] };
|
|
1350
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1351
|
+
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
1352
|
+
return new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1353
|
+
};
|
|
1354
|
+
const buildPcd = (releasedIds) => {
|
|
1355
|
+
const seasonFed = {
|
|
1356
|
+
entityReference: 'assortment:11EZML5dMn3tREuT',
|
|
1357
|
+
objectClass: 'LCSSeason'
|
|
1358
|
+
};
|
|
1359
|
+
const pcd = new publish_change_data_1.PublishChangeData('11EZML5dMn3tREuT', seasonFed, 'Pg3y2x5IclYxk3hM', sinceDate);
|
|
1360
|
+
pcd.itemToFederatedIdMapping = new Map();
|
|
1361
|
+
releasedIds.forEach(id => pcd.releasedForDevelopmentItemIds.push(id));
|
|
1362
|
+
return pcd;
|
|
1363
|
+
};
|
|
1364
|
+
const familyItem = (updatedOn = beforeSince, lifecycleStage = 'released') => ({
|
|
1365
|
+
id: familyId, itemFamilyId: familyId, lifecycleStage, name: 'Sweatpant', updatedOn
|
|
1366
|
+
});
|
|
1367
|
+
const familyRow = (updatedOn = beforeSince, lifecycleStage = 'released') => ({
|
|
1368
|
+
itemId: familyId,
|
|
1369
|
+
item: familyItem(updatedOn, lifecycleStage),
|
|
1370
|
+
projectItem: { id: 'pi-' + familyId, updatedOn: beforeSince }
|
|
1371
|
+
});
|
|
1372
|
+
const optionRow = (id, updatedOn, lifecycleStage = 'concept') => ({
|
|
1373
|
+
itemId: id,
|
|
1374
|
+
item: { id, itemFamilyId: familyId, lifecycleStage, updatedOn, itemFamily: familyItem() },
|
|
1375
|
+
projectItem: { id: 'pi-' + id, updatedOn: beforeSince },
|
|
1376
|
+
familyProjectItem: { id: 'pi-' + familyId, updatedOn: beforeSince }
|
|
1377
|
+
});
|
|
1378
|
+
it('records changed concept options as suppressed, not as colour updates', () => {
|
|
1379
|
+
const ppa = buildPpa();
|
|
1380
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1381
|
+
assortmentItems: [familyRow(), optionRow(maroonId, afterSince), optionRow(limeId, afterSince)]
|
|
1382
|
+
});
|
|
1383
|
+
const pcd = buildPcd([familyId]);
|
|
1384
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1385
|
+
expect(pcd.itemFamilyChanges.size).toEqual(1);
|
|
1386
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1387
|
+
expect(ifc.colorSuppressed).toEqual([maroonId, limeId]);
|
|
1388
|
+
expect(ifc.colorAdds.length).toEqual(0);
|
|
1389
|
+
expect(ifc.colorUpdates.length).toEqual(0);
|
|
1390
|
+
expect(ifc.colorDeletes.length).toEqual(0);
|
|
1391
|
+
expect(ifc.familyUpdate).toBe(false); // family itself unchanged
|
|
1392
|
+
});
|
|
1393
|
+
it('keeps the family reachable when only concept option rows are in the baseline', () => {
|
|
1394
|
+
const ppa = buildPpa();
|
|
1395
|
+
const assortmentItems = [optionRow(maroonId, afterSince), optionRow(limeId, afterSince)];
|
|
1396
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems });
|
|
1397
|
+
// derive the gate the way production does, rather than seeding it
|
|
1398
|
+
const releasedIds = ppa.getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems }, []);
|
|
1399
|
+
expect(releasedIds).toEqual([familyId]);
|
|
1400
|
+
const pcd = buildPcd(releasedIds);
|
|
1401
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1402
|
+
expect(pcd.itemFamilyChanges.size).toEqual(1);
|
|
1403
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1404
|
+
expect(ifc.itemFamilyObject.id).toEqual(familyId);
|
|
1405
|
+
expect(ifc.colorSuppressed.length).toEqual(2);
|
|
1406
|
+
expect(ppa.getProjectItem(ifc, familyId)).toEqual({ id: 'pi-' + familyId, updatedOn: beforeSince });
|
|
1407
|
+
});
|
|
1408
|
+
it('does not suppress-record an unchanged concept option', () => {
|
|
1409
|
+
const ppa = buildPpa();
|
|
1410
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1411
|
+
assortmentItems: [familyRow(), optionRow(maroonId, beforeSince)]
|
|
1412
|
+
});
|
|
1413
|
+
const pcd = buildPcd([familyId]);
|
|
1414
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1415
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1416
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1417
|
+
});
|
|
1418
|
+
it('picks up a concept option listed in the change detail even when timestamps are stale', () => {
|
|
1419
|
+
const ppa = buildPpa();
|
|
1420
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1421
|
+
assortmentItems: [familyRow(), optionRow(maroonId, beforeSince)]
|
|
1422
|
+
});
|
|
1423
|
+
const details = noDetailChanges();
|
|
1424
|
+
details.updates = [{ id: maroonId }];
|
|
1425
|
+
const pcd = buildPcd([familyId]);
|
|
1426
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
|
|
1427
|
+
expect(pcd.itemFamilyChanges.get(familyId).colorSuppressed).toEqual([maroonId]);
|
|
1428
|
+
});
|
|
1429
|
+
it('skips everything when the family itself is pre-development', () => {
|
|
1430
|
+
const ppa = buildPpa();
|
|
1431
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1432
|
+
assortmentItems: [familyRow(afterSince, 'concept'), optionRow(maroonId, afterSince)]
|
|
1433
|
+
});
|
|
1434
|
+
const pcd = buildPcd([]); // nothing released
|
|
1435
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1436
|
+
expect(pcd.itemFamilyChanges.size).toEqual(0);
|
|
1437
|
+
});
|
|
1438
|
+
it('still classifies released options normally', () => {
|
|
1439
|
+
const ppa = buildPpa();
|
|
1440
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1441
|
+
assortmentItems: [familyRow(), optionRow(maroonId, afterSince, 'released')]
|
|
1442
|
+
});
|
|
1443
|
+
const details = noDetailChanges();
|
|
1444
|
+
details.adds = [{ id: maroonId }];
|
|
1445
|
+
const pcd = buildPcd([familyId, maroonId]);
|
|
1446
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
|
|
1447
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1448
|
+
expect(ifc.colorAdds).toEqual([maroonId]);
|
|
1449
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1450
|
+
});
|
|
1451
|
+
it('publishes a family-only row whose lifecycle just flipped (item-level edit)', () => {
|
|
1452
|
+
const ppa = buildPpa();
|
|
1453
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({
|
|
1454
|
+
assortmentItems: [familyRow(afterSince)] // item.updatedOn moved, projectItem did not
|
|
1455
|
+
});
|
|
1456
|
+
const pcd = buildPcd([familyId]);
|
|
1457
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1458
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1459
|
+
expect(ifc.familyUpdate).toBe(true); // fails without Step 3
|
|
1460
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1461
|
+
});
|
|
1462
|
+
it('leaves an unchanged family-only row with no flags', () => {
|
|
1463
|
+
const ppa = buildPpa();
|
|
1464
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [familyRow()] });
|
|
1465
|
+
const pcd = buildPcd([familyId]);
|
|
1466
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
|
|
1467
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1468
|
+
expect(ifc.familyAdd).toBe(false);
|
|
1469
|
+
expect(ifc.familyUpdate).toBe(false);
|
|
1470
|
+
expect(ifc.familyDelete).toBe(false);
|
|
1471
|
+
expect(ifc.colorSuppressed.length).toEqual(0);
|
|
1472
|
+
});
|
|
1473
|
+
it('does not set familyUpdate when the family item was removed', () => {
|
|
1474
|
+
const ppa = buildPpa();
|
|
1475
|
+
const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [familyRow(afterSince)] });
|
|
1476
|
+
const details = noDetailChanges();
|
|
1477
|
+
details.familyItemsRemoved = [{ itemId: familyId }]; // note: itemId, not id
|
|
1478
|
+
const pcd = buildPcd([familyId]);
|
|
1479
|
+
pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
|
|
1480
|
+
const ifc = pcd.itemFamilyChanges.get(familyId);
|
|
1481
|
+
expect(ifc.familyItemRemoved).toBe(true);
|
|
1482
|
+
expect(ifc.familyUpdate).toBe(false); // fails if the !familyItemRemoved guard is dropped
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1340
1485
|
describe('getProjectItem', () => {
|
|
1341
1486
|
const config = {
|
|
1342
1487
|
identifierAtts: {
|
|
@@ -1500,6 +1645,70 @@ describe('meetsCriteria', () => {
|
|
|
1500
1645
|
expect(results).toBeTruthy();
|
|
1501
1646
|
});
|
|
1502
1647
|
});
|
|
1648
|
+
describe('getReleasedForDevelopmentItemAndFamilyIds', () => {
|
|
1649
|
+
const familyId = 'AERfdvdAt1HHEVvQ';
|
|
1650
|
+
const maroonId = 'YwWKVoL_vGN8khLy';
|
|
1651
|
+
const limeId = 'rsmWhWiXRNv_XBJG';
|
|
1652
|
+
const buildPpa = (preDev = ['concept']) => {
|
|
1653
|
+
const config = {
|
|
1654
|
+
identifierAtts: { LCSSeason: ['flexPLMSeasonName'] },
|
|
1655
|
+
itemPreDevelopmentLifecycleStages: preDev
|
|
1656
|
+
};
|
|
1657
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1658
|
+
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
1659
|
+
return new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1660
|
+
};
|
|
1661
|
+
const familyItem = (lifecycleStage = 'released') => ({
|
|
1662
|
+
id: familyId, itemFamilyId: familyId, lifecycleStage, name: 'Sweatpant'
|
|
1663
|
+
});
|
|
1664
|
+
// option row as the hydrated baseline actually delivers it: itemFamily present
|
|
1665
|
+
const optionRow = (id, lifecycleStage = 'concept', family = familyItem()) => ({
|
|
1666
|
+
itemId: id,
|
|
1667
|
+
item: { id, itemFamilyId: familyId, lifecycleStage, itemFamily: family }
|
|
1668
|
+
});
|
|
1669
|
+
const familyRow = (lifecycleStage = 'released') => ({
|
|
1670
|
+
itemId: familyId,
|
|
1671
|
+
item: familyItem(lifecycleStage)
|
|
1672
|
+
});
|
|
1673
|
+
const run = (ppa, assortmentItems) => ppa.getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems }, []);
|
|
1674
|
+
it('surfaces the family when every option is pre-development', () => {
|
|
1675
|
+
const ppa = buildPpa();
|
|
1676
|
+
expect(run(ppa, [optionRow(maroonId), optionRow(limeId)])).toEqual([familyId]);
|
|
1677
|
+
});
|
|
1678
|
+
it('does not duplicate the family id when a family row and a released option both exist', () => {
|
|
1679
|
+
const ppa = buildPpa();
|
|
1680
|
+
const ids = run(ppa, [familyRow(), optionRow(limeId, 'released')]);
|
|
1681
|
+
expect(ids.filter(id => id === familyId)).toHaveLength(1);
|
|
1682
|
+
});
|
|
1683
|
+
it('still resolves the family when an earlier row lacks itemFamily hydration', () => {
|
|
1684
|
+
const ppa = buildPpa();
|
|
1685
|
+
const noFamilyHydration = {
|
|
1686
|
+
itemId: maroonId,
|
|
1687
|
+
item: { id: maroonId, itemFamilyId: familyId, lifecycleStage: 'concept' }
|
|
1688
|
+
};
|
|
1689
|
+
expect(run(ppa, [noFamilyHydration, optionRow(limeId)])).toEqual([familyId]);
|
|
1690
|
+
});
|
|
1691
|
+
it('returns nothing when the family itself is pre-development', () => {
|
|
1692
|
+
const ppa = buildPpa();
|
|
1693
|
+
expect(run(ppa, [optionRow(maroonId, 'concept', familyItem('concept'))])).toEqual([]);
|
|
1694
|
+
});
|
|
1695
|
+
it('includes a family-only row exactly once', () => {
|
|
1696
|
+
const ppa = buildPpa();
|
|
1697
|
+
expect(run(ppa, [familyRow()])).toEqual([familyId]);
|
|
1698
|
+
});
|
|
1699
|
+
it('includes a released option and its family, excluding the concept option', () => {
|
|
1700
|
+
const ppa = buildPpa();
|
|
1701
|
+
const ids = run(ppa, [optionRow(maroonId, 'concept'), optionRow(limeId, 'released')]);
|
|
1702
|
+
expect(ids).toContain(familyId);
|
|
1703
|
+
expect(ids).toContain(limeId);
|
|
1704
|
+
expect(ids).not.toContain(maroonId);
|
|
1705
|
+
});
|
|
1706
|
+
it('treats the stage list as case-sensitive (documents the Concept vs concept trap)', () => {
|
|
1707
|
+
const ppa = buildPpa(['Concept']); // as mis-configured in praveen-dev-1
|
|
1708
|
+
const ids = run(ppa, [optionRow(maroonId), optionRow(limeId)]);
|
|
1709
|
+
expect(ids).toEqual([maroonId, familyId, limeId]); // filter inert: everything qualifies
|
|
1710
|
+
});
|
|
1711
|
+
});
|
|
1503
1712
|
describe('getEventsForItemFamilyChanges - conditional eventType', () => {
|
|
1504
1713
|
const config = {
|
|
1505
1714
|
identifierAtts: {
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrail/flexplm",
|
|
3
|
-
"version": "1.7.4-alpha.
|
|
3
|
+
"version": "1.7.4-alpha.458d29b",
|
|
4
4
|
"description": "Library used for integration with flexplm.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"flexplm-mapping": "lib/cli/index.js"
|
|
9
|
-
"flexplm-config": "lib/cli/config-index.js"
|
|
8
|
+
"flexplm-mapping": "lib/cli/index.js"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
12
11
|
"lib/**/*",
|
package/scripts/copy-template.js
CHANGED
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const SRC = path.join('src', 'cli', 'template', 'mapping-template.ts.template');
|
|
6
|
+
const DST = path.join('lib', 'cli', 'template', 'mapping-template.ts.template');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
fs.mkdirSync(path.dirname(DST), { recursive: true });
|
|
12
|
-
fs.copyFileSync(SRC, DST);
|
|
13
|
-
console.log(`Copied ${SRC} -> ${DST}`);
|
|
14
|
-
}
|
|
8
|
+
fs.mkdirSync(path.dirname(DST), { recursive: true });
|
|
9
|
+
fs.copyFileSync(SRC, DST);
|
|
10
|
+
console.log(`Copied ${SRC} -> ${DST}`);
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.ConfigCreateCommand = void 0;
|
|
27
|
-
const fs = __importStar(require("fs"));
|
|
28
|
-
const path = __importStar(require("path"));
|
|
29
|
-
const readline = __importStar(require("readline"));
|
|
30
|
-
const TEMPLATE_FILENAME = 'config-template.json.template';
|
|
31
|
-
const ORG_PLACEHOLDER = '<ORG_NAME>';
|
|
32
|
-
const APP_IDENTIFIER_PLACEHOLDER = '<APP_IDENTIFIER>';
|
|
33
|
-
const DEFAULT_APP_IDENTIFIER = '@vibeiq/flexplm-connector';
|
|
34
|
-
class ConfigCreateCommand {
|
|
35
|
-
prompt(rl, question) {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
rl.question(question, (answer) => resolve(answer));
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
findTemplate() {
|
|
41
|
-
const candidates = [
|
|
42
|
-
path.join(__dirname, '..', 'template', TEMPLATE_FILENAME),
|
|
43
|
-
path.join(__dirname, '..', '..', '..', 'src', 'cli', 'template', TEMPLATE_FILENAME),
|
|
44
|
-
];
|
|
45
|
-
for (const candidate of candidates) {
|
|
46
|
-
if (fs.existsSync(candidate)) {
|
|
47
|
-
return candidate;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
throw new Error(`Could not locate ${TEMPLATE_FILENAME}. Tried:\n ${candidates.join('\n ')}`);
|
|
51
|
-
}
|
|
52
|
-
async run() {
|
|
53
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
54
|
-
const onSigint = () => {
|
|
55
|
-
rl.close();
|
|
56
|
-
process.stdout.write('\n');
|
|
57
|
-
process.exit(130);
|
|
58
|
-
};
|
|
59
|
-
process.once('SIGINT', onSigint);
|
|
60
|
-
let orgName;
|
|
61
|
-
let appIdentifier;
|
|
62
|
-
try {
|
|
63
|
-
orgName = (await this.prompt(rl, 'orgName: ')).trim();
|
|
64
|
-
appIdentifier = (await this.prompt(rl, `appIdentifier (default: ${DEFAULT_APP_IDENTIFIER}): `)).trim() || DEFAULT_APP_IDENTIFIER;
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
process.removeListener('SIGINT', onSigint);
|
|
68
|
-
rl.close();
|
|
69
|
-
}
|
|
70
|
-
if (!orgName) {
|
|
71
|
-
throw new Error('orgName is required');
|
|
72
|
-
}
|
|
73
|
-
const templatePath = this.findTemplate();
|
|
74
|
-
const templateBody = fs.readFileSync(templatePath, 'utf8');
|
|
75
|
-
const rendered = templateBody.split(ORG_PLACEHOLDER).join(orgName).split(APP_IDENTIFIER_PLACEHOLDER).join(appIdentifier);
|
|
76
|
-
const outPath = path.resolve(process.cwd(), `${orgName}-flexplmConfig.json`);
|
|
77
|
-
if (fs.existsSync(outPath)) {
|
|
78
|
-
throw new Error(`Refusing to overwrite existing file: ${outPath}`);
|
|
79
|
-
}
|
|
80
|
-
fs.writeFileSync(outPath, rendered, 'utf8');
|
|
81
|
-
console.log(`Created ${outPath}`);
|
|
82
|
-
console.log('"orgName" and "appIdentifier" are pre-filled, but this file is not complete yet: FlexPLM connectivity also requires apiHost, userName, and password. See the "_availableAttributes" block in the file for these and other attributes this org can set (identifierAtts, LCSMaterial, etc.) — add the ones you need as real top-level keys, then delete "_availableAttributes"; the connector ignores it and applies defaults at runtime for anything you omit.');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
exports.ConfigCreateCommand = ConfigCreateCommand;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
const fs = __importStar(require("fs"));
|
|
27
|
-
const os = __importStar(require("os"));
|
|
28
|
-
const path = __importStar(require("path"));
|
|
29
|
-
let answers = [];
|
|
30
|
-
jest.mock('readline', () => ({
|
|
31
|
-
createInterface: () => ({
|
|
32
|
-
question: (_q, cb) => cb(answers.shift() || ''),
|
|
33
|
-
close: () => { },
|
|
34
|
-
}),
|
|
35
|
-
}));
|
|
36
|
-
const config_create_1 = require("./config-create");
|
|
37
|
-
describe('ConfigCreateCommand', () => {
|
|
38
|
-
let tempDir;
|
|
39
|
-
let originalCwd;
|
|
40
|
-
let logSpy;
|
|
41
|
-
beforeEach(() => {
|
|
42
|
-
originalCwd = process.cwd();
|
|
43
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flexplm-config-create-'));
|
|
44
|
-
process.chdir(tempDir);
|
|
45
|
-
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
46
|
-
answers = [];
|
|
47
|
-
});
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
logSpy.mockRestore();
|
|
50
|
-
process.chdir(originalCwd);
|
|
51
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
-
});
|
|
53
|
-
it('writes <orgName>-flexplmConfig.json with orgName and appIdentifier filled in', async () => {
|
|
54
|
-
answers = ['acme', '@vibeiq/flexplm-connector'];
|
|
55
|
-
await new config_create_1.ConfigCreateCommand().run();
|
|
56
|
-
const outPath = path.join(tempDir, 'acme-flexplmConfig.json');
|
|
57
|
-
expect(fs.existsSync(outPath)).toBe(true);
|
|
58
|
-
const config = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
|
59
|
-
expect(config.orgName).toEqual('acme');
|
|
60
|
-
expect(config.appIdentifier).toEqual('@vibeiq/flexplm-connector');
|
|
61
|
-
});
|
|
62
|
-
it('defaults appIdentifier to @vibeiq/flexplm-connector when left blank', async () => {
|
|
63
|
-
answers = ['acme', ''];
|
|
64
|
-
await new config_create_1.ConfigCreateCommand().run();
|
|
65
|
-
const outPath = path.join(tempDir, 'acme-flexplmConfig.json');
|
|
66
|
-
const config = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
|
67
|
-
expect(config.appIdentifier).toEqual('@vibeiq/flexplm-connector');
|
|
68
|
-
});
|
|
69
|
-
it('throws when orgName is empty', async () => {
|
|
70
|
-
answers = [' '];
|
|
71
|
-
await expect(new config_create_1.ConfigCreateCommand().run()).rejects.toThrow(/orgName is required/);
|
|
72
|
-
});
|
|
73
|
-
it('refuses to overwrite an existing file', async () => {
|
|
74
|
-
const existing = path.join(tempDir, 'acme-flexplmConfig.json');
|
|
75
|
-
fs.writeFileSync(existing, 'do not clobber', 'utf8');
|
|
76
|
-
answers = ['acme', '@vibeiq/flexplm-connector'];
|
|
77
|
-
await expect(new config_create_1.ConfigCreateCommand().run()).rejects.toThrow(/Refusing to overwrite/);
|
|
78
|
-
expect(fs.readFileSync(existing, 'utf8')).toEqual('do not clobber');
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
interface ConfigUploadOptions {
|
|
2
|
-
filePath: string;
|
|
3
|
-
message?: string;
|
|
4
|
-
branch?: string;
|
|
5
|
-
skipGit: boolean;
|
|
6
|
-
updateConfig: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare class ConfigUploadCommand {
|
|
9
|
-
static parseArgs(args: string[]): ConfigUploadOptions;
|
|
10
|
-
static buildCommitMessage(userMessage: string, fileId: string): string;
|
|
11
|
-
private prompt;
|
|
12
|
-
private promptHidden;
|
|
13
|
-
private runGit;
|
|
14
|
-
private tryRunGit;
|
|
15
|
-
private commitToGit;
|
|
16
|
-
run(args: string[]): Promise<void>;
|
|
17
|
-
private setConfigFileOnAppOrg;
|
|
18
|
-
}
|
|
19
|
-
export {};
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.ConfigUploadCommand = void 0;
|
|
27
|
-
const child_process_1 = require("child_process");
|
|
28
|
-
const fs = __importStar(require("fs"));
|
|
29
|
-
const path = __importStar(require("path"));
|
|
30
|
-
const readline = __importStar(require("readline"));
|
|
31
|
-
const sdk_1 = require("@contrail/sdk");
|
|
32
|
-
class ConfigUploadCommand {
|
|
33
|
-
static parseArgs(args) {
|
|
34
|
-
let filePath;
|
|
35
|
-
let message;
|
|
36
|
-
let branch;
|
|
37
|
-
let skipGit = false;
|
|
38
|
-
let updateConfig = false;
|
|
39
|
-
for (let i = 0; i < args.length; i++) {
|
|
40
|
-
const a = args[i];
|
|
41
|
-
if (a === '-m') {
|
|
42
|
-
message = args[++i];
|
|
43
|
-
if (message === undefined) {
|
|
44
|
-
throw new Error('-m requires a commit message argument');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
else if (a === '-b') {
|
|
48
|
-
branch = args[++i];
|
|
49
|
-
if (branch === undefined) {
|
|
50
|
-
throw new Error('-b requires a branch name argument');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
else if (a === '--skip-git' || a === '--skipGit') {
|
|
54
|
-
skipGit = true;
|
|
55
|
-
}
|
|
56
|
-
else if (a === '--update-config') {
|
|
57
|
-
updateConfig = true;
|
|
58
|
-
}
|
|
59
|
-
else if (a.startsWith('-')) {
|
|
60
|
-
throw new Error(`Unknown option: ${a}`);
|
|
61
|
-
}
|
|
62
|
-
else if (!filePath) {
|
|
63
|
-
filePath = a;
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
throw new Error(`Unexpected argument: ${a}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (!filePath) {
|
|
70
|
-
throw new Error('upload: missing <path.json> argument');
|
|
71
|
-
}
|
|
72
|
-
return { filePath, message, branch, skipGit, updateConfig };
|
|
73
|
-
}
|
|
74
|
-
static buildCommitMessage(userMessage, fileId) {
|
|
75
|
-
const lines = userMessage.split(/\r?\n/);
|
|
76
|
-
lines[0] = `${lines[0]} [fileId: ${fileId}]`;
|
|
77
|
-
return lines.join('\n');
|
|
78
|
-
}
|
|
79
|
-
prompt(question) {
|
|
80
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
81
|
-
return new Promise((resolve) => {
|
|
82
|
-
rl.question(question, (answer) => {
|
|
83
|
-
rl.close();
|
|
84
|
-
resolve(answer.trim());
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
promptHidden(question) {
|
|
89
|
-
return new Promise((resolve) => {
|
|
90
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
91
|
-
const rlAny = rl;
|
|
92
|
-
rlAny._writeToOutput = (str) => {
|
|
93
|
-
if (str.includes(question)) {
|
|
94
|
-
rlAny.output.write(str);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
rl.question(question, (answer) => {
|
|
98
|
-
rl.close();
|
|
99
|
-
process.stdout.write('\n');
|
|
100
|
-
resolve(answer);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
runGit(args, cwd) {
|
|
105
|
-
return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
106
|
-
}
|
|
107
|
-
tryRunGit(args, cwd) {
|
|
108
|
-
try {
|
|
109
|
-
const stdout = this.runGit(args, cwd);
|
|
110
|
-
return { ok: true, stdout, stderr: '' };
|
|
111
|
-
}
|
|
112
|
-
catch (err) {
|
|
113
|
-
return {
|
|
114
|
-
ok: false,
|
|
115
|
-
stdout: err && err.stdout ? err.stdout.toString() : '',
|
|
116
|
-
stderr: err && err.stderr ? err.stderr.toString() : (err && err.message) || '',
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async commitToGit(absPath, fileId, options) {
|
|
121
|
-
const repoDir = path.dirname(absPath);
|
|
122
|
-
const relPath = path.basename(absPath);
|
|
123
|
-
const versionCheck = this.tryRunGit(['--version'], repoDir);
|
|
124
|
-
if (!versionCheck.ok) {
|
|
125
|
-
console.log('git command not available; skipping git commit.');
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const insideRepo = this.tryRunGit(['rev-parse', '--is-inside-work-tree'], repoDir);
|
|
129
|
-
if (!insideRepo.ok || insideRepo.stdout.trim() !== 'true') {
|
|
130
|
-
console.log(`Not inside a git working tree (${repoDir}); skipping git commit.`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const tracked = this.tryRunGit(['ls-files', '--error-unmatch', relPath], repoDir);
|
|
134
|
-
if (!tracked.ok) {
|
|
135
|
-
const answer = (await this.prompt(`File is not tracked by git: ${relPath}\nAdd it to git? (Y/n): `)).toLowerCase();
|
|
136
|
-
if (answer === 'n' || answer === 'no') {
|
|
137
|
-
console.log('Nothing was done in git.');
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (options.branch) {
|
|
142
|
-
const branchResult = this.tryRunGit(['checkout', '-b', options.branch], repoDir);
|
|
143
|
-
if (!branchResult.ok) {
|
|
144
|
-
throw new Error(`Failed to create branch "${options.branch}": ${branchResult.stderr.trim()}`);
|
|
145
|
-
}
|
|
146
|
-
console.log(`Created and switched to branch "${options.branch}"`);
|
|
147
|
-
}
|
|
148
|
-
let message = options.message;
|
|
149
|
-
if (!message) {
|
|
150
|
-
message = await this.prompt('Commit message: ');
|
|
151
|
-
if (!message) {
|
|
152
|
-
throw new Error('A commit message is required');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const finalMessage = ConfigUploadCommand.buildCommitMessage(message, fileId);
|
|
156
|
-
const addResult = this.tryRunGit(['add', '--', relPath], repoDir);
|
|
157
|
-
if (!addResult.ok) {
|
|
158
|
-
throw new Error(`git add failed: ${addResult.stderr.trim()}`);
|
|
159
|
-
}
|
|
160
|
-
const commitResult = this.tryRunGit(['commit', '-m', finalMessage, '--', relPath], repoDir);
|
|
161
|
-
if (!commitResult.ok) {
|
|
162
|
-
throw new Error(`git commit failed: ${commitResult.stderr.trim() || commitResult.stdout.trim()}`);
|
|
163
|
-
}
|
|
164
|
-
console.log(commitResult.stdout.trim());
|
|
165
|
-
}
|
|
166
|
-
async run(args) {
|
|
167
|
-
const options = ConfigUploadCommand.parseArgs(args);
|
|
168
|
-
const absPath = path.resolve(process.cwd(), options.filePath);
|
|
169
|
-
if (!fs.existsSync(absPath)) {
|
|
170
|
-
throw new Error(`File not found: ${absPath}`);
|
|
171
|
-
}
|
|
172
|
-
if (!absPath.endsWith('.json')) {
|
|
173
|
-
throw new Error(`Expected a .json file, got: ${absPath}`);
|
|
174
|
-
}
|
|
175
|
-
const raw = fs.readFileSync(absPath, 'utf8');
|
|
176
|
-
let config;
|
|
177
|
-
try {
|
|
178
|
-
config = JSON.parse(raw);
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
throw new Error(`File is not valid JSON: ${absPath}\n${err && err.message ? err.message : err}`);
|
|
182
|
-
}
|
|
183
|
-
const orgName = config && config.orgName;
|
|
184
|
-
const appIdentifier = config && config.appIdentifier;
|
|
185
|
-
if (!orgName) {
|
|
186
|
-
throw new Error(`Config file is missing "orgName": ${absPath}`);
|
|
187
|
-
}
|
|
188
|
-
if (!appIdentifier) {
|
|
189
|
-
throw new Error(`Config file is missing "appIdentifier": ${absPath}`);
|
|
190
|
-
}
|
|
191
|
-
let email = process.env.CONTRAIL_CLI_EMAIL;
|
|
192
|
-
let password = process.env.CONTRAIL_CLI_PASSWORD;
|
|
193
|
-
if (!email) {
|
|
194
|
-
email = await this.prompt('Email: ');
|
|
195
|
-
}
|
|
196
|
-
if (!password) {
|
|
197
|
-
password = await this.promptHidden('Password: ');
|
|
198
|
-
}
|
|
199
|
-
if (!email || !password) {
|
|
200
|
-
throw new Error('Email and password are required');
|
|
201
|
-
}
|
|
202
|
-
await (0, sdk_1.login)({ orgSlug: orgName, email, password });
|
|
203
|
-
console.log(`Logged in to org "${orgName}" as ${email}`);
|
|
204
|
-
const apps = await new sdk_1.Entities().get({
|
|
205
|
-
entityName: 'app',
|
|
206
|
-
criteria: { identifier: appIdentifier },
|
|
207
|
-
});
|
|
208
|
-
if (!apps || apps.length !== 1) {
|
|
209
|
-
throw new Error(`Expected exactly one app with identifier "${appIdentifier}" in org "${orgName}", found ${apps ? apps.length : 0}`);
|
|
210
|
-
}
|
|
211
|
-
const app = apps[0];
|
|
212
|
-
const buffer = fs.readFileSync(absPath);
|
|
213
|
-
const fileName = path.basename(absPath);
|
|
214
|
-
const fileOwner = `app:${app.id}`;
|
|
215
|
-
const uploadedFile = await new sdk_1.Files().createAndUploadFileFromBuffer(buffer, 'application/json', fileName, fileOwner);
|
|
216
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
217
|
-
const responsePath = `${absPath}.uploaded-${timestamp}.json`;
|
|
218
|
-
fs.writeFileSync(responsePath, JSON.stringify(uploadedFile, null, 2), 'utf8');
|
|
219
|
-
console.log(`Wrote response to ${responsePath}`);
|
|
220
|
-
console.log(`FILE ID: ${uploadedFile.id}`);
|
|
221
|
-
if (!options.skipGit) {
|
|
222
|
-
await this.commitToGit(absPath, uploadedFile.id, options);
|
|
223
|
-
}
|
|
224
|
-
if (options.updateConfig) {
|
|
225
|
-
await this.setConfigFileOnAppOrg(app.id, appIdentifier, orgName, uploadedFile.id);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
async setConfigFileOnAppOrg(appId, appIdentifier, orgName, fileId) {
|
|
229
|
-
const appOrgs = await new sdk_1.Entities().get({
|
|
230
|
-
entityName: 'app-org',
|
|
231
|
-
criteria: { appId },
|
|
232
|
-
});
|
|
233
|
-
if (!appOrgs || appOrgs.length === 0) {
|
|
234
|
-
throw new Error(`Failed to set the file onto the app config for "${appIdentifier}" because it is not installed in org "${orgName}". Install it via the admin console before using --update-config. You can paste the uploaded file's ID into the app config without needing to re-run this command.`);
|
|
235
|
-
}
|
|
236
|
-
if (appOrgs.length > 1) {
|
|
237
|
-
throw new Error(`Failed to set the file onto the app config for "${appIdentifier}" in org "${orgName}" because ${appOrgs.length} installations were identified. Expected one. Please contact customer support for assistance.`);
|
|
238
|
-
}
|
|
239
|
-
const appOrg = appOrgs[0];
|
|
240
|
-
const nextAppConfig = { ...(appOrg.appConfig || {}), configFile: fileId };
|
|
241
|
-
await new sdk_1.Entities().update({
|
|
242
|
-
entityName: 'app-org',
|
|
243
|
-
id: appOrg.id,
|
|
244
|
-
object: { appConfig: nextAppConfig },
|
|
245
|
-
});
|
|
246
|
-
console.log(`Successfully set "appConfig.configFile" for installed "${appIdentifier}" to new FILE ID: "${fileId}" on org "${orgName}"`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
exports.ConfigUploadCommand = ConfigUploadCommand;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
jest.mock('@contrail/sdk', () => ({
|
|
4
|
-
Entities: jest.fn(),
|
|
5
|
-
Files: jest.fn(),
|
|
6
|
-
login: jest.fn(),
|
|
7
|
-
}));
|
|
8
|
-
const config_upload_1 = require("./config-upload");
|
|
9
|
-
describe('ConfigUploadCommand.parseArgs', () => {
|
|
10
|
-
it('parses a bare file path', () => {
|
|
11
|
-
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json']);
|
|
12
|
-
expect(opts).toEqual({
|
|
13
|
-
filePath: 'config.json',
|
|
14
|
-
message: undefined,
|
|
15
|
-
branch: undefined,
|
|
16
|
-
skipGit: false,
|
|
17
|
-
updateConfig: false,
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
it('parses -m commit message option', () => {
|
|
21
|
-
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '-m', 'my message']);
|
|
22
|
-
expect(opts.message).toEqual('my message');
|
|
23
|
-
expect(opts.skipGit).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
it('parses -b branch option', () => {
|
|
26
|
-
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '-b', 'feature/x']);
|
|
27
|
-
expect(opts.branch).toEqual('feature/x');
|
|
28
|
-
});
|
|
29
|
-
it('parses --skip-git flag', () => {
|
|
30
|
-
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--skip-git']);
|
|
31
|
-
expect(opts.skipGit).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
it('accepts the legacy --skipGit alias', () => {
|
|
34
|
-
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--skipGit']);
|
|
35
|
-
expect(opts.skipGit).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
it('parses --update-config flag', () => {
|
|
38
|
-
const opts = config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--update-config']);
|
|
39
|
-
expect(opts.updateConfig).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
it('throws when -m is missing its value', () => {
|
|
42
|
-
expect(() => config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '-m'])).toThrow(/-m requires a commit message/);
|
|
43
|
-
});
|
|
44
|
-
it('throws on unknown option', () => {
|
|
45
|
-
expect(() => config_upload_1.ConfigUploadCommand.parseArgs(['config.json', '--bogus'])).toThrow(/Unknown option: --bogus/);
|
|
46
|
-
});
|
|
47
|
-
it('throws when no file path is provided', () => {
|
|
48
|
-
expect(() => config_upload_1.ConfigUploadCommand.parseArgs([])).toThrow(/missing <path\.json>/);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe('ConfigUploadCommand.buildCommitMessage', () => {
|
|
52
|
-
it('appends fileId to the first line of a single-line message', () => {
|
|
53
|
-
expect(config_upload_1.ConfigUploadCommand.buildCommitMessage('initial commit', 'abc123')).toEqual('initial commit [fileId: abc123]');
|
|
54
|
-
});
|
|
55
|
-
it('handles CRLF line endings', () => {
|
|
56
|
-
const result = config_upload_1.ConfigUploadCommand.buildCommitMessage('header\r\nbody', 'fid');
|
|
57
|
-
expect(result).toEqual('header [fileId: fid]\nbody');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
describe('ConfigUploadCommand.run', () => {
|
|
61
|
-
const fs = require('fs');
|
|
62
|
-
const os = require('os');
|
|
63
|
-
const path = require('path');
|
|
64
|
-
let tempDir;
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flexplm-config-upload-'));
|
|
67
|
-
});
|
|
68
|
-
afterEach(() => {
|
|
69
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
70
|
-
});
|
|
71
|
-
it('throws when the config file is missing orgName', async () => {
|
|
72
|
-
const filePath = path.join(tempDir, 'bad.json');
|
|
73
|
-
fs.writeFileSync(filePath, JSON.stringify({ appIdentifier: '@vibeiq/flexplm-connector' }), 'utf8');
|
|
74
|
-
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/missing "orgName"/);
|
|
75
|
-
});
|
|
76
|
-
it('throws when the config file is missing appIdentifier', async () => {
|
|
77
|
-
const filePath = path.join(tempDir, 'bad.json');
|
|
78
|
-
fs.writeFileSync(filePath, JSON.stringify({ orgName: 'acme' }), 'utf8');
|
|
79
|
-
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/missing "appIdentifier"/);
|
|
80
|
-
});
|
|
81
|
-
it('throws when the file is not valid JSON', async () => {
|
|
82
|
-
const filePath = path.join(tempDir, 'bad.json');
|
|
83
|
-
fs.writeFileSync(filePath, '{not json', 'utf8');
|
|
84
|
-
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/not valid JSON/);
|
|
85
|
-
});
|
|
86
|
-
it('throws when the file does not exist', async () => {
|
|
87
|
-
await expect(new config_upload_1.ConfigUploadCommand().run([path.join(tempDir, 'nope.json'), '--skip-git'])).rejects.toThrow(/File not found/);
|
|
88
|
-
});
|
|
89
|
-
it('throws when the file is not a .json file', async () => {
|
|
90
|
-
const filePath = path.join(tempDir, 'config.txt');
|
|
91
|
-
fs.writeFileSync(filePath, '{}', 'utf8');
|
|
92
|
-
await expect(new config_upload_1.ConfigUploadCommand().run([filePath, '--skip-git'])).rejects.toThrow(/Expected a \.json file/);
|
|
93
|
-
});
|
|
94
|
-
});
|
package/lib/cli/config-index.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.main = exports.ConfigCli = void 0;
|
|
5
|
-
const config_create_1 = require("./commands/config-create");
|
|
6
|
-
const config_upload_1 = require("./commands/config-upload");
|
|
7
|
-
const USAGE = `Usage: flexplm-config <command> [args]
|
|
8
|
-
|
|
9
|
-
Commands:
|
|
10
|
-
create Scaffold a new connector config .json file in the current directory
|
|
11
|
-
upload <path.json> [opts] Upload a connector config .json file to VibeIQ
|
|
12
|
-
|
|
13
|
-
Upload options:
|
|
14
|
-
-m <message> Git commit message (prompted if omitted)
|
|
15
|
-
-b <branch> Create a new git branch before committing
|
|
16
|
-
--skip-git Skip the post-upload git commit (default: commit)
|
|
17
|
-
--update-config Patch the app-org appConfig.configFile with the uploaded file ID without needing to paste into the admin console
|
|
18
|
-
|
|
19
|
-
Environment (upload):
|
|
20
|
-
CONTRAIL_CLI_EMAIL VibeIQ user email
|
|
21
|
-
CONTRAIL_CLI_PASSWORD VibeIQ user password
|
|
22
|
-
`;
|
|
23
|
-
class ConfigCli {
|
|
24
|
-
async main() {
|
|
25
|
-
const [, , command, ...rest] = process.argv;
|
|
26
|
-
switch (command) {
|
|
27
|
-
case 'create':
|
|
28
|
-
await new config_create_1.ConfigCreateCommand().run();
|
|
29
|
-
return;
|
|
30
|
-
case 'upload':
|
|
31
|
-
if (!rest[0]) {
|
|
32
|
-
console.error('upload: missing <path.json> argument');
|
|
33
|
-
console.error(USAGE);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
await new config_upload_1.ConfigUploadCommand().run(rest);
|
|
37
|
-
return;
|
|
38
|
-
case undefined:
|
|
39
|
-
case '-h':
|
|
40
|
-
case '--help':
|
|
41
|
-
case 'help':
|
|
42
|
-
console.log(USAGE);
|
|
43
|
-
return;
|
|
44
|
-
default:
|
|
45
|
-
console.error(`Unknown command: ${command}`);
|
|
46
|
-
console.error(USAGE);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
exports.ConfigCli = ConfigCli;
|
|
52
|
-
function main() {
|
|
53
|
-
return new ConfigCli().main();
|
|
54
|
-
}
|
|
55
|
-
exports.main = main;
|
|
56
|
-
if (require.main === module) {
|
|
57
|
-
main().catch((err) => {
|
|
58
|
-
console.error(err && err.message ? err.message : err);
|
|
59
|
-
process.exit(1);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const createRunMock = jest.fn().mockResolvedValue(undefined);
|
|
4
|
-
const uploadRunMock = jest.fn().mockResolvedValue(undefined);
|
|
5
|
-
jest.mock('./commands/config-create', () => ({
|
|
6
|
-
ConfigCreateCommand: jest.fn().mockImplementation(() => ({ run: createRunMock })),
|
|
7
|
-
}));
|
|
8
|
-
jest.mock('./commands/config-upload', () => ({
|
|
9
|
-
ConfigUploadCommand: jest.fn().mockImplementation(() => ({ run: uploadRunMock })),
|
|
10
|
-
}));
|
|
11
|
-
const config_index_1 = require("./config-index");
|
|
12
|
-
describe('config cli main dispatcher', () => {
|
|
13
|
-
let originalArgv;
|
|
14
|
-
let logSpy;
|
|
15
|
-
let errorSpy;
|
|
16
|
-
let exitSpy;
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
originalArgv = process.argv;
|
|
19
|
-
createRunMock.mockClear();
|
|
20
|
-
uploadRunMock.mockClear();
|
|
21
|
-
logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
22
|
-
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
23
|
-
exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
24
|
-
throw new Error(`__EXIT__:${code}`);
|
|
25
|
-
}));
|
|
26
|
-
});
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
process.argv = originalArgv;
|
|
29
|
-
logSpy.mockRestore();
|
|
30
|
-
errorSpy.mockRestore();
|
|
31
|
-
exitSpy.mockRestore();
|
|
32
|
-
});
|
|
33
|
-
function setArgv(...args) {
|
|
34
|
-
process.argv = ['node', 'cli', ...args];
|
|
35
|
-
}
|
|
36
|
-
it('dispatches the create command', async () => {
|
|
37
|
-
setArgv('create');
|
|
38
|
-
await (0, config_index_1.main)();
|
|
39
|
-
expect(createRunMock).toHaveBeenCalledTimes(1);
|
|
40
|
-
expect(uploadRunMock).not.toHaveBeenCalled();
|
|
41
|
-
});
|
|
42
|
-
it('dispatches the upload command and forwards remaining args', async () => {
|
|
43
|
-
setArgv('upload', 'config.json', '-m', 'msg', '--skip-git');
|
|
44
|
-
await (0, config_index_1.main)();
|
|
45
|
-
expect(uploadRunMock).toHaveBeenCalledWith(['config.json', '-m', 'msg', '--skip-git']);
|
|
46
|
-
});
|
|
47
|
-
it('exits when upload is missing its argument', async () => {
|
|
48
|
-
setArgv('upload');
|
|
49
|
-
await expect((0, config_index_1.main)()).rejects.toThrow('__EXIT__:1');
|
|
50
|
-
expect(uploadRunMock).not.toHaveBeenCalled();
|
|
51
|
-
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/missing <path\.json>/));
|
|
52
|
-
});
|
|
53
|
-
it.each(['help', '-h', '--help'])('prints usage for %s', async (helpFlag) => {
|
|
54
|
-
setArgv(helpFlag);
|
|
55
|
-
await (0, config_index_1.main)();
|
|
56
|
-
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/Usage: flexplm-config/));
|
|
57
|
-
});
|
|
58
|
-
it('prints usage when no command is provided', async () => {
|
|
59
|
-
setArgv();
|
|
60
|
-
await (0, config_index_1.main)();
|
|
61
|
-
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/Usage: flexplm-config/));
|
|
62
|
-
});
|
|
63
|
-
it('exits on an unknown command', async () => {
|
|
64
|
-
setArgv('bogus');
|
|
65
|
-
await expect((0, config_index_1.main)()).rejects.toThrow('__EXIT__:1');
|
|
66
|
-
expect(errorSpy).toHaveBeenCalledWith(expect.stringMatching(/Unknown command: bogus/));
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"orgName": "<ORG_NAME>",
|
|
3
|
-
"appIdentifier": "<APP_IDENTIFIER>",
|
|
4
|
-
|
|
5
|
-
"_availableAttributes": {
|
|
6
|
-
"_note": "Reference only — the connector never reads this key. Add any of the attributes below as real top-level keys if this org needs them, then delete this whole \"_availableAttributes\" block.",
|
|
7
|
-
"apiHost": "FlexPLM API host base URL. Required — throws NEED_CONFIG_VALUES if missing",
|
|
8
|
-
"userName": "FlexPLM user name used for Basic Auth. Required; rewritten to a () => string getter",
|
|
9
|
-
"password": "FlexPLM password used for Basic Auth. Required; rewritten to a () => string getter",
|
|
10
|
-
"urlContext": "Path prefix for FlexPLM URLs. Default: '/Windchill'",
|
|
11
|
-
"itemPreDevelopmentLifecycleStages": "Item lifecycle stages during which items are not synced to FlexPLM. Default: ['concept']",
|
|
12
|
-
"logLevel": "Possible values: error/warn/info/debug. Default: info",
|
|
13
|
-
"identifierAtts": "Deprecated. Map of FlexPLM object class to identifier attribute name(s), e.g. { \"LCSProduct\": [\"itemNumber\"] }",
|
|
14
|
-
"plmEnviornment": "Sent as the PLM_ENV header on every FlexPLM request. Only used by 1 customer. NOTE: this key is intentionally spelled \"plmEnviornment\" (matching the connector's actual config key) — do not \"fix\" the spelling to \"plmEnvironment\", it will silently stop working",
|
|
15
|
-
"propertyMapping": "Deprecated. Reserved for custom property-mapping overrides",
|
|
16
|
-
"complexConfig": "Object of additional attributes (see below) merged onto the top level of the config, then deleted; __proto__/constructor/prototype keys are stripped for safety. e.g. { \"complexConfig\": { \"csrfEndpoint\": \"...\" } }",
|
|
17
|
-
"csrfEndpoint (in complexConfig)": "CSRF token endpoint path. Default: '/servlet/rest/security/csrf'",
|
|
18
|
-
"dataConverter.useDisplayForEnumerationMatching (in complexConfig)": "Default: false",
|
|
19
|
-
"dataConverter.verboseDebug (in complexConfig)": "Default: false",
|
|
20
|
-
"flexplmConnect.staticHeaders (in complexConfig)": "Extra static headers added to every FlexPLM request",
|
|
21
|
-
"LCSMaterial.processAsItem (in complexConfig)": "true routes LCSMaterial to item:material instead of custom-entity. Default: false",
|
|
22
|
-
"max_thumbnail_size (in complexConfig)": "Max thumbnail size in bytes. Default: 5 MB",
|
|
23
|
-
"payloadDefaultAsArray (in complexConfig)": "Whether outbound payload values default to arrays. Default: true",
|
|
24
|
-
"search.<entityType>.useIdentityServiceForInboundData (in complexConfig)": "entityType is one of: item, color, custom-entity, project-item. Default: false",
|
|
25
|
-
"sendMode.ASYNC_PUBLISH_SEASON (in complexConfig)": "Default: 'vibeiqfile'",
|
|
26
|
-
"syncOptionSets (in complexConfig)": "Array of { flexInternalName, vibeSlug }",
|
|
27
|
-
"useDistinctRestEndPointForImages (in complexConfig)": "Default: false; when true routes image fetch through /rfa/vibeiq/image instead of the raw URL",
|
|
28
|
-
"vibeEventEndpoint (in complexConfig)": "Endpoint VibeIQ posts inbound events to. Default: '/rfa/vibeiq/vibeEvents'"
|
|
29
|
-
}
|
|
30
|
-
}
|