@contrail/flexplm 1.6.0-alpha.6f15d4e → 1.6.0-alpha.8e73fa3
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.
|
@@ -44,10 +44,41 @@ export interface DirectionalSection {
|
|
|
44
44
|
* (e.g. a REKEY task with `rekeyTransformersKey: 'rekey'` reads
|
|
45
45
|
* {@link DirectionalSection.rekey}).
|
|
46
46
|
*
|
|
47
|
+
* Ordering matters — tasks run top-to-bottom and operate on whatever
|
|
48
|
+
* the previous task left behind. Also there can be multiple tasks of
|
|
49
|
+
* the same processor type, as long as they have different config keys.
|
|
50
|
+
* This allows you to split up different logical steps (e.g. remove most
|
|
51
|
+
* unwanted fields before a value transform. Where the value transform
|
|
52
|
+
* needs a field to be present, but it must be removed before sending.
|
|
53
|
+
* Conventional order:
|
|
54
|
+
* 1. **REMOVE** — drop keys you don't want carried forward, while the
|
|
55
|
+
* row is still in its input shape (so REMOVE keys match the input
|
|
56
|
+
* system's slugs).
|
|
57
|
+
* 2. **VALUE_TRANSFORM** / **MORPH** — coerce / normalize values
|
|
58
|
+
* while the keys are still recognizable.
|
|
59
|
+
* 3. **REKEY** — last, so the remaining keys are renamed to the
|
|
60
|
+
* output system. After REKEY runs with `rekeyDelete: true`, the
|
|
61
|
+
* input-side slugs are gone and any later REMOVE / VALUE_TRANSFORM
|
|
62
|
+
* would need to be written against the output-side slugs instead.
|
|
63
|
+
*
|
|
64
|
+
* Swapping REKEY before REMOVE is a common cause of "my removeKey isn't
|
|
65
|
+
* doing anything" — by the time REMOVE runs, the keys have already
|
|
66
|
+
* been renamed.
|
|
67
|
+
*
|
|
68
|
+
* **Only processors listed in `transformOrder` actually run.** A
|
|
69
|
+
* `rekey` / `removeKey` / `valueTransform` table sitting on the
|
|
70
|
+
* directional section is inert unless a matching task references it
|
|
71
|
+
* here — defining `rekey: {...}` without a REKEY entry in
|
|
72
|
+
* `transformOrder` is silently a no-op. If a step seems to be doing
|
|
73
|
+
* nothing, check that its processor is in this list before assuming
|
|
74
|
+
* the lookup table is wrong.
|
|
75
|
+
*
|
|
47
76
|
* @example
|
|
77
|
+
* // Conventional: REMOVE → VALUE_TRANSFORM → REKEY.
|
|
48
78
|
* transformOrder: [
|
|
49
|
-
* { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
|
|
50
79
|
* { processor: 'REMOVE', removeKeysKey: 'removeKey' },
|
|
80
|
+
* { processor: 'VALUE_TRANSFORM', functionTransformersKey: 'valueTransform' },
|
|
81
|
+
* { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
|
|
51
82
|
* ]
|
|
52
83
|
*/
|
|
53
84
|
transformOrder?: TransformTask[];
|
|
@@ -119,8 +150,42 @@ export interface DirectionalSection {
|
|
|
119
150
|
* Names of keys to delete from each row. Referenced by `removeKeysKey`
|
|
120
151
|
* on the REMOVE task.
|
|
121
152
|
*
|
|
153
|
+
* On the `vibe2flex` side, this is the **outbound filter** —
|
|
154
|
+
* {@link MappingSection.vibeOwningKeys} only governs inbound
|
|
155
|
+
* protection, so anything you want kept out of the FlexPLM payload
|
|
156
|
+
* has to be listed here. Typical entries:
|
|
157
|
+
* - Vibe-internal fields with no FlexPLM equivalent
|
|
158
|
+
* (`federatedId`, computed totals, planning-only properties).
|
|
159
|
+
* - FlexPLM-owned inbound properties under their Vibe-side slug,
|
|
160
|
+
* so they aren't echoed back to FlexPLM after a prior inbound sync.
|
|
161
|
+
*
|
|
162
|
+
* Keys are matched against the row in its **input** shape, so on
|
|
163
|
+
* `vibe2flex` list Vibe-side slugs (REMOVE runs before REKEY in the
|
|
164
|
+
* conventional ordering) and on `flex2vibe` list FlexPLM field names.
|
|
165
|
+
*
|
|
166
|
+
* `removeKey` vs {@link MappingSection.vibeOwningKeys} — different
|
|
167
|
+
* jobs, not redundant:
|
|
168
|
+
* - `vibeOwningKeys` = **refuse to accept on inbound**.
|
|
169
|
+
* - `vibe2flex.removeKey` = **refuse to send on outbound**.
|
|
170
|
+
*
|
|
171
|
+
* A property can belong in one, both, or neither — decide per
|
|
172
|
+
* property based on which system owns it:
|
|
173
|
+
* - `targetIntroDate` (Vibe-owned, no Flex counterpart)
|
|
174
|
+
* → in **both** (protect inbound, don't bother sending).
|
|
175
|
+
* - `materialDescription` (Flex-owned, Vibe stores it after a
|
|
176
|
+
* prior inbound sync) → in **`removeKey` only**, so the value
|
|
177
|
+
* isn't echoed back to Flex; Flex still owns updates inbound.
|
|
178
|
+
* - `event` (Vibe-owned, published to Flex) → in
|
|
179
|
+
* **`vibeOwningKeys` only**; we want to keep sending it.
|
|
180
|
+
*
|
|
122
181
|
* @example
|
|
123
|
-
* removeKey
|
|
182
|
+
* // vibe2flex.removeKey — strip Vibe-internal + Flex-owned-inbound
|
|
183
|
+
* // properties before publish.
|
|
184
|
+
* removeKey: [
|
|
185
|
+
* 'federatedId',
|
|
186
|
+
* 'targetIntroDate', // Vibe-owned, no Flex counterpart
|
|
187
|
+
* 'materialDescription', // Vibe-side slug of a Flex-owned inbound field
|
|
188
|
+
* ]
|
|
124
189
|
*/
|
|
125
190
|
removeKey?: string[];
|
|
126
191
|
/**
|
|
@@ -185,12 +250,17 @@ export interface DirectionalSection {
|
|
|
185
250
|
*
|
|
186
251
|
* @example
|
|
187
252
|
* LCSProduct: {
|
|
188
|
-
*
|
|
253
|
+
* // Identifier — used for lookup / create-vs-update routing.
|
|
189
254
|
* getIdentifierProperties: () => ['styleNumber'],
|
|
255
|
+
* // Vibe-owned attributes.
|
|
256
|
+
* vibeOwningKeys: ['lifecycleStage', 'targetIntroDate'],
|
|
190
257
|
* vibe2flex: {
|
|
191
258
|
* transformOrder: [
|
|
259
|
+
* { processor: 'REMOVE', removeKeysKey: 'removeKey' },
|
|
192
260
|
* { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
|
|
193
261
|
* ],
|
|
262
|
+
* // Strip Vibe-only / Flex-owned-inbound props before publish.
|
|
263
|
+
* removeKey: ['federatedId'],
|
|
194
264
|
* // destination: source -> flexKey: 'vibeKey'
|
|
195
265
|
* rekey: { productName: 'name' },
|
|
196
266
|
* },
|
|
@@ -205,12 +275,46 @@ export interface DirectionalSection {
|
|
|
205
275
|
*/
|
|
206
276
|
export interface MappingSection {
|
|
207
277
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
278
|
+
* Slugs of the VibeIQ properties that VibeIQ owns. During inbound
|
|
279
|
+
* (flex2vibe) processing, any value FlexPLM tries to send for one
|
|
280
|
+
* of these slugs is ignored — the existing VibeIQ value is preserved.
|
|
281
|
+
*
|
|
282
|
+
* Scope: **inbound only**. This list does *not* filter outbound
|
|
283
|
+
* payloads — Vibe-only properties still flow to FlexPLM unless you
|
|
284
|
+
* strip them in {@link DirectionalSection.removeKey} on the
|
|
285
|
+
* `vibe2flex` side.
|
|
286
|
+
*
|
|
287
|
+
* What `vibeOwningKeys` does **not** do — reach for these fields
|
|
288
|
+
* instead:
|
|
289
|
+
* - Strip fields from the outbound payload → use
|
|
290
|
+
* {@link DirectionalSection.removeKey} on `vibe2flex`.
|
|
291
|
+
* - Block inbound entity creation → use
|
|
292
|
+
* {@link MappingSection.isInboundCreatable}.
|
|
293
|
+
* - Drive lookup / matching / dedupe → use
|
|
294
|
+
* {@link MappingSection.getIdentifierProperties}.
|
|
295
|
+
* - Skip value transformations or rekeying → those run regardless;
|
|
296
|
+
* omit the property from `rekey` / `valueTransform` if you want
|
|
297
|
+
* it left alone.
|
|
298
|
+
*
|
|
299
|
+
* `vibeOwningKeys` vs {@link DirectionalSection.removeKey} —
|
|
300
|
+
* different jobs, not redundant:
|
|
301
|
+
* - `vibeOwningKeys` = **refuse to accept on inbound**.
|
|
302
|
+
* - `vibe2flex.removeKey` = **refuse to send on outbound**.
|
|
303
|
+
*
|
|
304
|
+
* A property can belong in one, both, or neither — decide per
|
|
305
|
+
* property based on which system owns it:
|
|
306
|
+
* - `targetIntroDate` (Vibe-owned, no Flex counterpart)
|
|
307
|
+
* → in **both** (protect inbound, don't bother sending).
|
|
308
|
+
* - `materialDescription` (Flex-owned, Vibe stores it after a
|
|
309
|
+
* prior inbound sync) → in **`removeKey` only**, so the value
|
|
310
|
+
* isn't echoed back to Flex; Flex still owns updates inbound.
|
|
311
|
+
* - `event` (Vibe-owned, published to Flex) → in
|
|
312
|
+
* **`vibeOwningKeys` only**; we want to keep sending it.
|
|
211
313
|
*
|
|
212
314
|
* @example
|
|
213
|
-
*
|
|
315
|
+
* // Right: Vibe-owned attributes;
|
|
316
|
+
* // If Vibe owns the identifier, include it here as well.
|
|
317
|
+
* vibeOwningKeys: ['itemNumber', 'lifecycleStage', 'targetIntroDate']
|
|
214
318
|
*/
|
|
215
319
|
vibeOwningKeys?: string[];
|
|
216
320
|
/**
|
|
@@ -228,8 +332,43 @@ export interface MappingSection {
|
|
|
228
332
|
* one. Consulted by `TypeConversionUtils.isInboundCreatableFromObject`;
|
|
229
333
|
* defaults to `false` when the function is absent.
|
|
230
334
|
*
|
|
335
|
+
* The function receives the inbound row, so the decision can be
|
|
336
|
+
* per-record (e.g. only allow inbound creation when a status or
|
|
337
|
+
* type-discriminator flag is set). Common patterns:
|
|
338
|
+
* - `() => true` — FlexPLM owns creation for this entity type.
|
|
339
|
+
* - `() => false` — VibeIQ owns creation; inbound rows can only
|
|
340
|
+
* update existing entities.
|
|
341
|
+
* - Conditional — inspect the row and allow / disallow per record.
|
|
342
|
+
*
|
|
343
|
+
* On the **season-link** sections (`LCSProductSeasonLink` /
|
|
344
|
+
* `LCSSKUSeasonLink`), `() => true` enables the add-to-project
|
|
345
|
+
* flow: FlexPLM-driven inbound events can add an item to a Vibe
|
|
346
|
+
* project (not just update an existing project-item).
|
|
347
|
+
* Pair with {@link MappingSection.isOutboundCreatable} returning
|
|
348
|
+
* `false` so the outbound side becomes update-only. This pattern
|
|
349
|
+
* also requires matching FlexPLM-side configuration; see the
|
|
350
|
+
* project documentation for the full setup.
|
|
351
|
+
*
|
|
231
352
|
* @example
|
|
353
|
+
* // Flex owns creation.
|
|
232
354
|
* isInboundCreatable: () => true
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* // Conditional: allow inbound creation only when the row is active
|
|
358
|
+
* // and the type-path matches a class we accept from FlexPLM.
|
|
359
|
+
* isInboundCreatable: (object) => {
|
|
360
|
+
* if (object?.['status']?.value?.toLowerCase?.() !== 'active') return false;
|
|
361
|
+
* return object?.['flexPLMTypePath'] === 'Color\\Solid';
|
|
362
|
+
* }
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* // add-to-project flow — Flex drives project-item creation.
|
|
366
|
+
* // Paired with isOutboundCreatable: () => false to make
|
|
367
|
+
* // outbound update-only.
|
|
368
|
+
* LCSProductSeasonLink: {
|
|
369
|
+
* isInboundCreatable: () => true,
|
|
370
|
+
* isOutboundCreatable: () => false,
|
|
371
|
+
* }
|
|
233
372
|
*/
|
|
234
373
|
isInboundCreatable?: (entity: Record<string, unknown>, context?: unknown) => boolean;
|
|
235
374
|
/**
|
|
@@ -239,8 +378,24 @@ export interface MappingSection {
|
|
|
239
378
|
* `TypeConversionUtils.isOutboundCreatableFromEntity`; defaults to
|
|
240
379
|
* `true` when the function is absent.
|
|
241
380
|
*
|
|
381
|
+
* On the **season-link** sections (`LCSProductSeasonLink` /
|
|
382
|
+
* `LCSSKUSeasonLink`), `() => false` converts outbound publish events
|
|
383
|
+
* to `UPDATE_ON_SEASON`: they only update an existing season-link,
|
|
384
|
+
* never create one. If the Product / Colorway isn't already on the
|
|
385
|
+
* season, **that specific event fails** while other events continue
|
|
386
|
+
* to process — the failure is per-record, not fatal to the batch.
|
|
387
|
+
*
|
|
388
|
+
* Pair with {@link MappingSection.isInboundCreatable} returning
|
|
389
|
+
* `true` to delegate add-to-project creation to FlexPLM. This pattern
|
|
390
|
+
* also requires matching FlexPLM-side configuration; see the project
|
|
391
|
+
* documentation for the full setup.
|
|
392
|
+
*
|
|
242
393
|
* @example
|
|
243
|
-
*
|
|
394
|
+
* // add-to-project flow — Vibe never adds items to seasons; Flex does.
|
|
395
|
+
* LCSProductSeasonLink: {
|
|
396
|
+
* isInboundCreatable: () => true,
|
|
397
|
+
* isOutboundCreatable: () => false, // becomes UPDATE_ON_SEASON
|
|
398
|
+
* }
|
|
244
399
|
*/
|
|
245
400
|
isOutboundCreatable?: (entity: Record<string, unknown>, context?: unknown) => boolean;
|
|
246
401
|
/**
|
|
@@ -272,8 +427,55 @@ export interface MappingSection {
|
|
|
272
427
|
* `vibeIQIdentifierProperties` is not present on the row, falling
|
|
273
428
|
* back to `TypeDefaults` when this is absent.
|
|
274
429
|
*
|
|
430
|
+
* Identifier properties are the *business key* used to answer
|
|
431
|
+
* "does this inbound payload refer to an existing entity, or do we
|
|
432
|
+
* need to create one?" The connector uses them for entity lookup,
|
|
433
|
+
* create-vs-update routing, and the deterministic targeting of
|
|
434
|
+
* retry / resend events.
|
|
435
|
+
*
|
|
436
|
+
* Inbound lookup outcomes drive routing:
|
|
437
|
+
* - **0 matches** → row is treated as a new entity; creation
|
|
438
|
+
* proceeds only if {@link MappingSection.isInboundCreatable}
|
|
439
|
+
* allows it, otherwise the row is dropped.
|
|
440
|
+
* - **1 match** → existing entity is updated.
|
|
441
|
+
* - **>1 match** → processing fails for that row to avoid
|
|
442
|
+
* ambiguity. Multiple matches means the identifier isn't
|
|
443
|
+
* actually unique; fix the data or pick a tighter key rather
|
|
444
|
+
* than letting the merge happen silently.
|
|
445
|
+
*
|
|
446
|
+
* Multiple slugs form a **composite key** — all of them must match
|
|
447
|
+
* the same existing entity for it to count as the same record. If
|
|
448
|
+
* any value differs, the row is treated as a different entity.
|
|
449
|
+
*
|
|
450
|
+
* Best practices:
|
|
451
|
+
* - Pick the **minimum set** of slugs that uniquely identifies the
|
|
452
|
+
* entity. Multiple slugs are treated as a composite key — all of
|
|
453
|
+
* them must match for the lookup to succeed.
|
|
454
|
+
* - Prefer **stable** business identifiers (item / style / season
|
|
455
|
+
* numbers) over frequently-changing fields. Renames of an
|
|
456
|
+
* identifier are a data-migration event, not a routine update.
|
|
457
|
+
* - Ensure the identifier exists in **both** systems and on
|
|
458
|
+
* historical records. If it's missing on legacy data the lookup
|
|
459
|
+
* will silently create duplicates.
|
|
460
|
+
*
|
|
461
|
+
* When the connector app config sets
|
|
462
|
+
* `search.<entityType>.useIdentityServiceForInboundData: true` for
|
|
463
|
+
* the entity type (e.g. `item`, `color`, `custom-entity`), inbound
|
|
464
|
+
* identifier matching switches from a direct-property query to the
|
|
465
|
+
* platform identity service. This is forward-looking: enable it only
|
|
466
|
+
* after the identifier property has been configured as unique in
|
|
467
|
+
* Vibe and the historical-data backfill is complete. {@link
|
|
468
|
+
* MappingSection.uniquenessPool} declares which identity-service
|
|
469
|
+
* uniqueness pool the entity participates in.
|
|
470
|
+
*
|
|
275
471
|
* @example
|
|
472
|
+
* // Single business key
|
|
276
473
|
* getIdentifierProperties: () => ['itemNumber']
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* // Composite key — both slugs must match for a row to count as the
|
|
477
|
+
* // same entity.
|
|
478
|
+
* getIdentifierProperties: () => ['seasonName', 'year']
|
|
277
479
|
*/
|
|
278
480
|
getIdentifierProperties?: (entity: Record<string, unknown>) => string[];
|
|
279
481
|
/**
|
|
@@ -282,8 +484,35 @@ export interface MappingSection {
|
|
|
282
484
|
* Consulted by `TypeConversionUtils.getInformationalProperties` with
|
|
283
485
|
* the same lookup precedence as {@link MappingSection.getIdentifierProperties}.
|
|
284
486
|
*
|
|
487
|
+
* Purpose is **observability**, not behavior. Informational properties:
|
|
488
|
+
* - ride along in retry / resend event payloads so the retry target
|
|
489
|
+
* is human-readable, not just a numeric ID;
|
|
490
|
+
* - surface in logs, error messages, and monitoring dashboards so
|
|
491
|
+
* failures can be triaged without opening a debugger;
|
|
492
|
+
* - give engineers and ops a way to recognize "which entity is this"
|
|
493
|
+
* at a glance.
|
|
494
|
+
*
|
|
495
|
+
* They are **not** used for entity matching, create-vs-update routing,
|
|
496
|
+
* uniqueness enforcement, or duplicate prevention. If a property
|
|
497
|
+
* needs to influence any of those, put it in
|
|
498
|
+
* {@link MappingSection.getIdentifierProperties} instead.
|
|
499
|
+
*
|
|
500
|
+
* Prefer stable, readable, scalar fields (names, numbers). Avoid
|
|
501
|
+
* large objects, references, or frequently-changing values — they'll
|
|
502
|
+
* bloat retry payloads and clutter logs without adding clarity.
|
|
503
|
+
*
|
|
285
504
|
* @example
|
|
286
|
-
*
|
|
505
|
+
* // SKU: matched by colorwaySeqID, but log lines show the itemNumber.
|
|
506
|
+
* // "Failed to upsert SKU — colorwaySeqID=12345 itemNumber=NB-574-BLU"
|
|
507
|
+
* LCSSKU: {
|
|
508
|
+
* getIdentifierProperties: () => ['colorwaySeqID'],
|
|
509
|
+
* getInformationalProperties: () => ['itemNumber'],
|
|
510
|
+
* }
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* // Multiple informational properties — all carried together; none
|
|
514
|
+
* // affect matching.
|
|
515
|
+
* getInformationalProperties: () => ['itemNumber', 'skuName']
|
|
287
516
|
*/
|
|
288
517
|
getInformationalProperties?: (entity: Record<string, unknown>) => string[];
|
|
289
518
|
/**
|
|
@@ -292,6 +521,23 @@ export interface MappingSection {
|
|
|
292
521
|
* FlexPLM shape, so inside `rekey` the destination (left) is the FlexPLM
|
|
293
522
|
* key and the source (right) is the VibeIQ key — see
|
|
294
523
|
* {@link DirectionalSection.rekey}.
|
|
524
|
+
*
|
|
525
|
+
* Responsibilities:
|
|
526
|
+
* - Rename Vibe attribute slugs to Flex field names (`rekey`).
|
|
527
|
+
* - Remove Vibe-only attributes from the payload
|
|
528
|
+
* ({@link DirectionalSection.removeKey}).
|
|
529
|
+
* - Convert / coerce values to formats FlexPLM accepts
|
|
530
|
+
* ({@link DirectionalSection.valueTransform},
|
|
531
|
+
* {@link DirectionalSection.morphTransform}).
|
|
532
|
+
* - Produce a Flex-valid payload ready for publish.
|
|
533
|
+
*
|
|
534
|
+
* Note the asymmetry with `flex2vibe`: the inbound ownership and
|
|
535
|
+
* creation gates ({@link MappingSection.vibeOwningKeys},
|
|
536
|
+
* {@link MappingSection.isInboundCreatable}) do **not** apply here.
|
|
537
|
+
* `vibe2flex` is the side that *sends* Vibe data out — protecting
|
|
538
|
+
* Vibe data from being overwritten only makes sense on the inbound
|
|
539
|
+
* side. To keep Vibe-only fields from leaking outbound, list them in
|
|
540
|
+
* `removeKey`.
|
|
295
541
|
*/
|
|
296
542
|
vibe2flex?: DirectionalSection;
|
|
297
543
|
/**
|
|
@@ -300,6 +546,21 @@ export interface MappingSection {
|
|
|
300
546
|
* VibeIQ shape, so inside `rekey` the destination (left) is the VibeIQ
|
|
301
547
|
* key and the source (right) is the FlexPLM key — see
|
|
302
548
|
* {@link DirectionalSection.rekey}.
|
|
549
|
+
*
|
|
550
|
+
* Responsibilities:
|
|
551
|
+
* - Rename Flex field names to Vibe attribute slugs (`rekey`).
|
|
552
|
+
* - Normalize / coerce inbound values into Vibe-compatible formats
|
|
553
|
+
* ({@link DirectionalSection.valueTransform},
|
|
554
|
+
* {@link DirectionalSection.morphTransform}).
|
|
555
|
+
* - Produce a row ready to hand to `setEntityValues()` /
|
|
556
|
+
* inbound persistence.
|
|
557
|
+
*
|
|
558
|
+
* This is the direction where the protective gates fire:
|
|
559
|
+
* - {@link MappingSection.vibeOwningKeys} prevents inbound rows
|
|
560
|
+
* from overwriting Vibe-owned attributes.
|
|
561
|
+
* - {@link MappingSection.isInboundCreatable} decides whether an
|
|
562
|
+
* inbound row whose identifier matches nothing should create a
|
|
563
|
+
* new Vibe entity or be dropped.
|
|
303
564
|
*/
|
|
304
565
|
flex2vibe?: DirectionalSection;
|
|
305
566
|
}
|
|
@@ -375,9 +636,38 @@ export interface TypeConversionSection {
|
|
|
375
636
|
* }
|
|
376
637
|
*/
|
|
377
638
|
export interface TypeConversion {
|
|
378
|
-
/**
|
|
639
|
+
/**
|
|
640
|
+
* Routes outbound VibeIQ entities to their FlexPLM mapping sections.
|
|
641
|
+
*
|
|
642
|
+
* When no entry matches the row's VibeIQ entity type, the connector
|
|
643
|
+
* falls back to looking up a section by the row's `TypeDefaults`
|
|
644
|
+
* class. The explicit `vibe2flex` entries here override that fallback
|
|
645
|
+
* and are only needed when a single VibeIQ type fans out to multiple
|
|
646
|
+
* mapping sections (e.g. `'custom-entity'` dispatched by `typePath`).
|
|
647
|
+
*/
|
|
379
648
|
vibe2flex: TypeConversionSection;
|
|
380
|
-
/**
|
|
649
|
+
/**
|
|
650
|
+
* Routes inbound FlexPLM objects to their VibeIQ mapping sections.
|
|
651
|
+
*
|
|
652
|
+
* When no entry matches the row's FlexPLM object class, the connector
|
|
653
|
+
* falls back to using the row's `flexPLMObjectClass` directly as the
|
|
654
|
+
* section key. Explicit `flex2vibe` entries are only needed when one
|
|
655
|
+
* FlexPLM class fans out to multiple mapping sections (e.g.
|
|
656
|
+
* `LCSLifecycleManaged` dispatched by `flexPLMTypePath`).
|
|
657
|
+
*
|
|
658
|
+
* `LCSMaterial` routing — special case: by default `LCSMaterial`
|
|
659
|
+
* maps to `custom-entity` on the VibeIQ side. To route it to
|
|
660
|
+
* `item:material` (treating materials as a subtype of `item`)
|
|
661
|
+
* instead, **both** of the following must be in place:
|
|
662
|
+
* 1. Connector app config sets
|
|
663
|
+
* `{ "LCSMaterial": { "processAsItem": true } }`. The mapping
|
|
664
|
+
* file alone can't change which entity type LCSMaterial maps to.
|
|
665
|
+
* 2. `flex2vibe.LCSMaterial.getMapKey` returns the name of a custom
|
|
666
|
+
* mapping section that declares `getClass: () => 'item'` +
|
|
667
|
+
* `getSoftType: () => 'item:material'` on its `flex2vibe` side.
|
|
668
|
+
* The matching `vibe2flex.item.getMapKey` should route
|
|
669
|
+
* `item:material` typePath back to the same section.
|
|
670
|
+
*/
|
|
381
671
|
flex2vibe: TypeConversionSection;
|
|
382
672
|
}
|
|
383
673
|
/** Identifies the application and tenant that owns a {@link MappingFile}. */
|
|
@@ -403,6 +693,39 @@ interface MappingFileBase {
|
|
|
403
693
|
* {@link TypeConversionEntry.getMapKey} (e.g. `LCSProduct`, `LCSSKU`,
|
|
404
694
|
* `'Exchange Rate'`).
|
|
405
695
|
*
|
|
696
|
+
* # Supported entity mappings
|
|
697
|
+
*
|
|
698
|
+
* The tables below describe which VibeIQ entity types and FlexPLM object
|
|
699
|
+
* classes can be synced today and what each direction does behind the
|
|
700
|
+
* scenes. "Not yet supported" entries are listed explicitly so authors
|
|
701
|
+
* know not to attempt them.
|
|
702
|
+
*
|
|
703
|
+
* ## VibeIQ → FlexPLM
|
|
704
|
+
*
|
|
705
|
+
* | VibeIQ entity type | FlexPLM object class | Behavior |
|
|
706
|
+
* |----------------------|----------------------------------|------------------------------------------------------------------------------------------------------------|
|
|
707
|
+
* | `item` | `LCSProduct` / `LCSSKU` | Syncs once `lifecycleStage` is out of `concept`. Sends Primary Viewable as the thumbnail (no other images). Creates `LCSProduct` / `LCSSKU` if missing; auto-retries `LCSSKU` creation when the parent `LCSProduct` isn't ready yet. |
|
|
708
|
+
* | `color` | `LCSColor` | Syncs on create / update. Sends Primary Viewable as the thumbnail. Creates `LCSColor` if missing. |
|
|
709
|
+
* | `custom-entity` | `LCSRevisableEntity` | Syncs on create / update. Creates `LCSRevisableEntity` if missing. Workflows should gate on the custom-entity subtype so only the intended subtypes publish. |
|
|
710
|
+
* | `project-item` | `LCSProductSeasonLink` / `LCSSKUSeasonLink` | Syncs on plan publish. Requires the assortment to have `publishToFlexPLM=true` and `flexPLMSeasonName` set. Adds the Product / SKU to the `LCSSeason` if not already linked. Only project-items changed since the last publish are sent (configurable to reach further back). Processing on the FlexPLM side is async via a Windchill Queue. |
|
|
711
|
+
* | `item:material` | `LCSMaterial` | Supported via custom mapping section + `typeConversion`. Route `item:material` typePath to a mapping section that declares `getClass: () => 'LCSMaterial'` and `getSoftType: () => 'Material'`. See {@link TypeConversion} for the `LCSMaterial.processAsItem` config flag that pairs with this. |
|
|
712
|
+
* | `custom-entity` | `LCSLast` / `LCSLifecycleManaged` | **Not yet supported.** |
|
|
713
|
+
*
|
|
714
|
+
* ## FlexPLM → VibeIQ
|
|
715
|
+
*
|
|
716
|
+
* | FlexPLM object class | VibeIQ entity type | Behavior |
|
|
717
|
+
* |-------------------------|----------------------------|------------------------------------------------------------------------------------------------------------|
|
|
718
|
+
* | `LCSProduct` | `item` (role: `family`) | Configurable via mapping file to create new items; updates existing items if found. Does **not** sync the thumbnail to VibeIQ. |
|
|
719
|
+
* | `LCSSKU` | `item` (role: `option`) | Configurable to create new items; updates existing items. Does **not** sync the thumbnail. |
|
|
720
|
+
* | `LCSProductSeasonLink` | `project-item` (`family`) | Updates existing items if found. Can be configured to add an item to a project, including setting carried-over data — see the add-to-project flow under {@link MappingSection.isInboundCreatable}. |
|
|
721
|
+
* | `LCSSKUSeasonLink` | `project-item` (`option`) | Updates existing items. Add-to-project supported under the same flow. |
|
|
722
|
+
* | `LCSColor` | `color` | Configurable to create new colors; updates existing colors. Does **not** sync the thumbnail. |
|
|
723
|
+
* | `LCSLast` | `custom-entity` | Configurable to create new custom-entities; updates existing. |
|
|
724
|
+
* | `LCSLifecycleManaged` | `custom-entity` | Configurable to create new custom-entities; updates existing. |
|
|
725
|
+
* | `LCSMaterial` | `custom-entity` (default) or `item:material` | Default: custom-entity. To route to `item:material` instead, set `{ "LCSMaterial": { "processAsItem": true } }` in the connector app config and write a mapping section with `getClass: () => 'item'` + `getSoftType: () => 'item:material'`. Wire it via `flex2vibe.LCSMaterial.getMapKey` returning that section's name. |
|
|
726
|
+
* | `LCSRevisableEntity` | `custom-entity` | Configurable to create new custom-entities; updates existing. |
|
|
727
|
+
* | (any) | `item` (direct, no link) | **Not yet supported.** |
|
|
728
|
+
*
|
|
406
729
|
* @example
|
|
407
730
|
* export const mapping: MappingFile = {
|
|
408
731
|
* orgInfo: {
|
|
@@ -86,8 +86,10 @@ export declare class BaseProcessPublishAssortment {
|
|
|
86
86
|
getResultsCount(events: any): any;
|
|
87
87
|
private sendEvents;
|
|
88
88
|
private sendToFlexPLM;
|
|
89
|
+
private buildPublishError;
|
|
89
90
|
private saveToLocalFile;
|
|
90
91
|
private handleVibeIQFile;
|
|
92
|
+
private sendPublishPayloadEvent;
|
|
91
93
|
private getCurrentDateString;
|
|
92
94
|
getItemFamilyChanges(pcd: PublishChangeData, changeDetail: any, assortmentItemFullChangeMap: Map<string, any>, assortmentItemDeleteMap: Map<string, any>): Map<string, ItemFamilyChanges>;
|
|
93
95
|
getEventsForPublishChangeData(publishChangeData: PublishChangeData): Promise<SeasonalPayload[]>;
|
|
@@ -595,14 +595,37 @@ class BaseProcessPublishAssortment {
|
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
597
|
async sendToFlexPLM(events, eventType) {
|
|
598
|
-
const
|
|
598
|
+
const outboundPublishEvent = {
|
|
599
599
|
taskId: this.config.taskId,
|
|
600
600
|
eventType,
|
|
601
601
|
objectClass: 'LCSSeason',
|
|
602
602
|
events
|
|
603
603
|
};
|
|
604
604
|
const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
|
|
605
|
-
|
|
605
|
+
const [sendResult, eventResult] = await Promise.allSettled([
|
|
606
|
+
flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
|
|
607
|
+
this.sendPublishPayloadEvent(outboundPublishEvent)
|
|
608
|
+
]);
|
|
609
|
+
if (sendResult.status === 'rejected' || eventResult.status === 'rejected') {
|
|
610
|
+
throw this.buildPublishError(sendResult, eventResult, outboundPublishEvent);
|
|
611
|
+
}
|
|
612
|
+
const result = sendResult.value;
|
|
613
|
+
const isResultObject = typeof result === 'object' && result !== null;
|
|
614
|
+
return isResultObject
|
|
615
|
+
? { ...result, outboundPublishEvent }
|
|
616
|
+
: { result, outboundPublishEvent };
|
|
617
|
+
}
|
|
618
|
+
buildPublishError(sendResult, eventResult, outboundPublishEvent) {
|
|
619
|
+
const baseReason = sendResult.status === 'rejected' ? sendResult.reason : eventResult.reason;
|
|
620
|
+
const error = baseReason instanceof Error ? baseReason : new Error(String(baseReason));
|
|
621
|
+
error.outboundPublishEvent = outboundPublishEvent;
|
|
622
|
+
error.sendToFlexPLMResult = sendResult.status === 'fulfilled'
|
|
623
|
+
? sendResult.value
|
|
624
|
+
: { error: sendResult.reason?.message ?? String(sendResult.reason) };
|
|
625
|
+
error.sendPublishPayloadEventResult = eventResult.status === 'fulfilled'
|
|
626
|
+
? eventResult.value
|
|
627
|
+
: { error: eventResult.reason?.message ?? String(eventResult.reason) };
|
|
628
|
+
return error;
|
|
606
629
|
}
|
|
607
630
|
async saveToLocalFile(events, eventType) {
|
|
608
631
|
let results = {};
|
|
@@ -614,15 +637,16 @@ class BaseProcessPublishAssortment {
|
|
|
614
637
|
const eventDirName = 'events-output';
|
|
615
638
|
const fileName = `${path.sep}${eventDirName}${path.sep}events-${dateString}.json`;
|
|
616
639
|
fileHandle = await fsPromise.open('.' + fileName, 'a');
|
|
617
|
-
const
|
|
640
|
+
const outboundPublishEvent = {
|
|
618
641
|
taskId: this.config.taskId,
|
|
619
642
|
eventType,
|
|
620
643
|
objectClass: 'LCSSeason',
|
|
621
644
|
events
|
|
622
645
|
};
|
|
623
|
-
await fileHandle.writeFile(JSON.stringify(
|
|
646
|
+
await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
|
|
624
647
|
results = {
|
|
625
648
|
message: 'Successfully Saved File',
|
|
649
|
+
outboundPublishEvent,
|
|
626
650
|
fileLocation: dirName + fileName
|
|
627
651
|
};
|
|
628
652
|
}
|
|
@@ -638,7 +662,7 @@ class BaseProcessPublishAssortment {
|
|
|
638
662
|
const eventBuffer = Buffer.from(JSON.stringify(events), 'utf-8');
|
|
639
663
|
const fileName = 'ASYNC_PUBLISH_SEASON-events.json';
|
|
640
664
|
const uploadFile = await new sdk_1.Files().createAndUploadFileFromBuffer(eventBuffer, 'application/json', fileName, null, this.TTL);
|
|
641
|
-
const
|
|
665
|
+
const outboundPublishEvent = {
|
|
642
666
|
taskId: this.config.taskId,
|
|
643
667
|
eventType,
|
|
644
668
|
objectClass: 'LCSSeason',
|
|
@@ -648,15 +672,46 @@ class BaseProcessPublishAssortment {
|
|
|
648
672
|
};
|
|
649
673
|
if (sendMode === 'vibeiqfile') {
|
|
650
674
|
const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
|
|
651
|
-
|
|
675
|
+
const [sendResult, eventResult] = await Promise.allSettled([
|
|
676
|
+
flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
|
|
677
|
+
this.sendPublishPayloadEvent(outboundPublishEvent)
|
|
678
|
+
]);
|
|
679
|
+
if (sendResult.status === 'rejected' || eventResult.status === 'rejected') {
|
|
680
|
+
throw this.buildPublishError(sendResult, eventResult, outboundPublishEvent);
|
|
681
|
+
}
|
|
682
|
+
const result = sendResult.value;
|
|
683
|
+
const isResultObject = typeof result === 'object' && result !== null;
|
|
684
|
+
return isResultObject
|
|
685
|
+
? { ...result, outboundPublishEvent }
|
|
686
|
+
: { result, outboundPublishEvent };
|
|
652
687
|
}
|
|
653
688
|
else {
|
|
689
|
+
await this.sendPublishPayloadEvent(outboundPublishEvent);
|
|
654
690
|
return {
|
|
655
691
|
message: 'Successfully Uploaded File.',
|
|
656
|
-
|
|
692
|
+
outboundPublishEvent
|
|
657
693
|
};
|
|
658
694
|
}
|
|
659
695
|
}
|
|
696
|
+
async sendPublishPayloadEvent(outboundPublishEvent) {
|
|
697
|
+
console.info('sendPublishPayloadEvent()');
|
|
698
|
+
let initialEvent = {};
|
|
699
|
+
if (this.config?.event) {
|
|
700
|
+
initialEvent = (typeof this.config?.event === 'string')
|
|
701
|
+
? JSON.parse(this.config?.event)
|
|
702
|
+
: this.config?.event;
|
|
703
|
+
}
|
|
704
|
+
const eventBody = {
|
|
705
|
+
originSystemType: 'VibeIQ',
|
|
706
|
+
objectClass: 'AssortmentPublishedToFlexPLM',
|
|
707
|
+
outboundPublishEvent,
|
|
708
|
+
initialEvent
|
|
709
|
+
};
|
|
710
|
+
await new sdk_1.Entities().create({
|
|
711
|
+
entityName: 'external-event',
|
|
712
|
+
object: eventBody
|
|
713
|
+
});
|
|
714
|
+
}
|
|
660
715
|
getCurrentDateString() {
|
|
661
716
|
const d = new Date();
|
|
662
717
|
return '' + d.getUTCFullYear()
|
|
@@ -9,6 +9,7 @@ const sdk_1 = require("@contrail/sdk");
|
|
|
9
9
|
const type_conversion_utils_1 = require("../util/type-conversion-utils");
|
|
10
10
|
const map_utils_1 = require("../util/map-utils");
|
|
11
11
|
const item_family_changes_1 = require("../interfaces/item-family-changes");
|
|
12
|
+
const flexplm_connect_1 = require("../util/flexplm-connect");
|
|
12
13
|
let federatedId = '';
|
|
13
14
|
jest.mock('../util/data-converter', () => {
|
|
14
15
|
return {
|
|
@@ -31,6 +32,13 @@ jest.mock('../util/federation', () => {
|
|
|
31
32
|
});
|
|
32
33
|
let entityObject = {};
|
|
33
34
|
let getOptionsObject = {};
|
|
35
|
+
let createCallArg = undefined;
|
|
36
|
+
let fileUploadCalls = [];
|
|
37
|
+
let fileUploadResult = {
|
|
38
|
+
id: 'file-123',
|
|
39
|
+
downloadUrl: 'https://download.url',
|
|
40
|
+
adminDownloadUrl: 'https://admin.download.url'
|
|
41
|
+
};
|
|
34
42
|
jest.mock('@contrail/sdk', () => {
|
|
35
43
|
return {
|
|
36
44
|
Entities: class {
|
|
@@ -38,6 +46,18 @@ jest.mock('@contrail/sdk', () => {
|
|
|
38
46
|
getOptionsObject = _getOtionsObject;
|
|
39
47
|
return entityObject;
|
|
40
48
|
}
|
|
49
|
+
create(arg) {
|
|
50
|
+
createCallArg = arg;
|
|
51
|
+
return Promise.resolve({});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
Files: class {
|
|
55
|
+
createAndUploadFileFromBuffer(buffer, mimeType, fileName, _x, ttl) {
|
|
56
|
+
fileUploadCalls.push({ buffer, mimeType, fileName, ttl });
|
|
57
|
+
return Promise.resolve(fileUploadResult);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
Request: class {
|
|
41
61
|
}
|
|
42
62
|
};
|
|
43
63
|
});
|
|
@@ -1686,3 +1706,195 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
|
|
|
1686
1706
|
expect(type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity).toHaveBeenNthCalledWith(2, undefined, mapFileUtil, colorProjectItem, { item: itemData, assortment });
|
|
1687
1707
|
});
|
|
1688
1708
|
});
|
|
1709
|
+
describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
|
|
1710
|
+
const config = {
|
|
1711
|
+
taskId: 'task-abc',
|
|
1712
|
+
event: '{"sourceEventId":"src-1"}'
|
|
1713
|
+
};
|
|
1714
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
1715
|
+
const dc = new data_converter_1.DataConverter(config, mapFileUtil);
|
|
1716
|
+
const events = [{ objectClass: 'LCSProductSeasonLink' }];
|
|
1717
|
+
const eventType = 'ASYNC_PUBLISH_SEASON';
|
|
1718
|
+
beforeEach(() => {
|
|
1719
|
+
createCallArg = undefined;
|
|
1720
|
+
fileUploadCalls = [];
|
|
1721
|
+
fileUploadResult = {
|
|
1722
|
+
id: 'file-123',
|
|
1723
|
+
downloadUrl: 'https://download.url',
|
|
1724
|
+
adminDownloadUrl: 'https://admin.download.url'
|
|
1725
|
+
};
|
|
1726
|
+
});
|
|
1727
|
+
afterEach(() => {
|
|
1728
|
+
jest.restoreAllMocks();
|
|
1729
|
+
});
|
|
1730
|
+
it('should merge outboundPublishEvent into result and call sendPublishPayloadEvent in parallel', async () => {
|
|
1731
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1732
|
+
const flexResponse = { status: 200, data: { ok: true } };
|
|
1733
|
+
let flexResolved = false;
|
|
1734
|
+
let eventResolved = false;
|
|
1735
|
+
const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
|
|
1736
|
+
.mockImplementation(async () => {
|
|
1737
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1738
|
+
flexResolved = true;
|
|
1739
|
+
return flexResponse;
|
|
1740
|
+
});
|
|
1741
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
|
|
1742
|
+
.mockImplementation(async () => {
|
|
1743
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1744
|
+
eventResolved = true;
|
|
1745
|
+
});
|
|
1746
|
+
const result = await bppa.sendToFlexPLM(events, eventType);
|
|
1747
|
+
expect(spyFlex).toHaveBeenCalledTimes(1);
|
|
1748
|
+
expect(spyEvent).toHaveBeenCalledTimes(1);
|
|
1749
|
+
expect(flexResolved).toBe(true);
|
|
1750
|
+
expect(eventResolved).toBe(true);
|
|
1751
|
+
const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
|
|
1752
|
+
expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
|
|
1753
|
+
expect(passedOutboundPublishEvent.eventType).toBe(eventType);
|
|
1754
|
+
expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
|
|
1755
|
+
expect(passedOutboundPublishEvent.events).toBe(events);
|
|
1756
|
+
expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
|
|
1757
|
+
expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
|
|
1758
|
+
});
|
|
1759
|
+
it('sendToFlexPLM throws when flexPLMConnect.sendToFlexPLM fails, attaching outboundPublishEvent and both results', async () => {
|
|
1760
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1761
|
+
const flexError = new Error('flex failed');
|
|
1762
|
+
const eventResponse = { eventOk: true };
|
|
1763
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
|
|
1764
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockResolvedValue(eventResponse);
|
|
1765
|
+
await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toMatchObject({
|
|
1766
|
+
message: 'flex failed',
|
|
1767
|
+
outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType, objectClass: 'LCSSeason', events }),
|
|
1768
|
+
sendToFlexPLMResult: { error: 'flex failed' },
|
|
1769
|
+
sendPublishPayloadEventResult: eventResponse
|
|
1770
|
+
});
|
|
1771
|
+
});
|
|
1772
|
+
it('sendToFlexPLM throws when sendPublishPayloadEvent fails, with flex result attached', async () => {
|
|
1773
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1774
|
+
const flexResponse = { status: 200 };
|
|
1775
|
+
const eventError = new Error('event failed');
|
|
1776
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse);
|
|
1777
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
|
|
1778
|
+
await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toMatchObject({
|
|
1779
|
+
message: 'event failed',
|
|
1780
|
+
outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType }),
|
|
1781
|
+
sendToFlexPLMResult: flexResponse,
|
|
1782
|
+
sendPublishPayloadEventResult: { error: 'event failed' }
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
it('sendToFlexPLM throws the flexPLMConnect error when both fail', async () => {
|
|
1786
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1787
|
+
const flexError = new Error('flex failed');
|
|
1788
|
+
const eventError = new Error('event failed');
|
|
1789
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
|
|
1790
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
|
|
1791
|
+
await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toBe(flexError);
|
|
1792
|
+
expect(flexError.outboundPublishEvent).toBeDefined();
|
|
1793
|
+
expect(flexError.sendToFlexPLMResult).toEqual({ error: 'flex failed' });
|
|
1794
|
+
expect(flexError.sendPublishPayloadEventResult).toEqual({ error: 'event failed' });
|
|
1795
|
+
});
|
|
1796
|
+
it('handleVibeIQFile (vibeiqfile) throws when flexPLMConnect.sendToFlexPLM fails, with both results attached', async () => {
|
|
1797
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1798
|
+
const flexError = new Error('flex failed');
|
|
1799
|
+
const eventResponse = { eventOk: true };
|
|
1800
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
|
|
1801
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockResolvedValue(eventResponse);
|
|
1802
|
+
await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
|
|
1803
|
+
message: 'flex failed',
|
|
1804
|
+
outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
|
|
1805
|
+
sendToFlexPLMResult: { error: 'flex failed' },
|
|
1806
|
+
sendPublishPayloadEventResult: eventResponse
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
it('handleVibeIQFile (vibeiqfile) throws when sendPublishPayloadEvent fails, with flex result attached', async () => {
|
|
1810
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1811
|
+
const flexResponse = { status: 200 };
|
|
1812
|
+
const eventError = new Error('event failed');
|
|
1813
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse);
|
|
1814
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
|
|
1815
|
+
await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
|
|
1816
|
+
message: 'event failed',
|
|
1817
|
+
outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
|
|
1818
|
+
sendToFlexPLMResult: flexResponse,
|
|
1819
|
+
sendPublishPayloadEventResult: { error: 'event failed' }
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
it('handleVibeIQFile (vibeiqfile) throws the flexPLMConnect error when both fail', async () => {
|
|
1823
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1824
|
+
const flexError = new Error('flex failed');
|
|
1825
|
+
const eventError = new Error('event failed');
|
|
1826
|
+
jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
|
|
1827
|
+
jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
|
|
1828
|
+
await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toBe(flexError);
|
|
1829
|
+
expect(flexError.sendToFlexPLMResult).toEqual({ error: 'flex failed' });
|
|
1830
|
+
expect(flexError.sendPublishPayloadEventResult).toEqual({ error: 'event failed' });
|
|
1831
|
+
});
|
|
1832
|
+
it('should merge outboundPublishEvent into FlexPLM result and call sendPublishPayloadEvent when mode is vibeiqfile', async () => {
|
|
1833
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1834
|
+
const flexResponse = { status: 200, data: { ok: true } };
|
|
1835
|
+
const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
|
|
1836
|
+
.mockResolvedValue(flexResponse);
|
|
1837
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
|
|
1838
|
+
.mockResolvedValue(undefined);
|
|
1839
|
+
const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile');
|
|
1840
|
+
expect(fileUploadCalls).toHaveLength(1);
|
|
1841
|
+
expect(fileUploadCalls[0].fileName).toBe('ASYNC_PUBLISH_SEASON-events.json');
|
|
1842
|
+
expect(fileUploadCalls[0].mimeType).toBe('application/json');
|
|
1843
|
+
const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
|
|
1844
|
+
expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
|
|
1845
|
+
expect(passedOutboundPublishEvent.eventType).toBe(eventType);
|
|
1846
|
+
expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
|
|
1847
|
+
expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
|
|
1848
|
+
expect(passedOutboundPublishEvent.eventsDownloadLink).toBe('https://download.url');
|
|
1849
|
+
expect(passedOutboundPublishEvent.eventsAdminDownloadLink).toBe('https://admin.download.url');
|
|
1850
|
+
expect(spyFlex).toHaveBeenCalledTimes(1);
|
|
1851
|
+
expect(spyEvent).toHaveBeenCalledTimes(1);
|
|
1852
|
+
expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
|
|
1853
|
+
expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
|
|
1854
|
+
});
|
|
1855
|
+
it('should skip FlexPLM but still call sendPublishPayloadEvent when mode is vibeiqfile-dontsendtoflexplm', async () => {
|
|
1856
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1857
|
+
const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
|
|
1858
|
+
.mockResolvedValue({});
|
|
1859
|
+
const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
|
|
1860
|
+
.mockResolvedValue(undefined);
|
|
1861
|
+
const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile-dontsendtoflexplm');
|
|
1862
|
+
expect(spyFlex).not.toHaveBeenCalled();
|
|
1863
|
+
expect(spyEvent).toHaveBeenCalledTimes(1);
|
|
1864
|
+
const passedOutboundPublishEvent = spyEvent.mock.calls[0][0];
|
|
1865
|
+
expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
|
|
1866
|
+
expect(result).toEqual({
|
|
1867
|
+
message: 'Successfully Uploaded File.',
|
|
1868
|
+
outboundPublishEvent: passedOutboundPublishEvent
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
it('should create an external-event with initialEvent parsed from config', async () => {
|
|
1872
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
|
|
1873
|
+
const outboundPublishEvent = { foo: 'bar' };
|
|
1874
|
+
await bppa.sendPublishPayloadEvent(outboundPublishEvent);
|
|
1875
|
+
expect(createCallArg).toEqual({
|
|
1876
|
+
entityName: 'external-event',
|
|
1877
|
+
object: {
|
|
1878
|
+
originSystemType: 'VibeIQ',
|
|
1879
|
+
objectClass: 'AssortmentPublishedToFlexPLM',
|
|
1880
|
+
outboundPublishEvent,
|
|
1881
|
+
initialEvent: { sourceEventId: 'src-1' }
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
});
|
|
1885
|
+
it('should default initialEvent to {} when config has no event', async () => {
|
|
1886
|
+
const bareConfig = { taskId: 'task-x' };
|
|
1887
|
+
const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(bareConfig, dc, mapFileUtil);
|
|
1888
|
+
const outboundPublishEvent = { hello: 'world' };
|
|
1889
|
+
await bppa.sendPublishPayloadEvent(outboundPublishEvent);
|
|
1890
|
+
expect(createCallArg).toEqual({
|
|
1891
|
+
entityName: 'external-event',
|
|
1892
|
+
object: {
|
|
1893
|
+
originSystemType: 'VibeIQ',
|
|
1894
|
+
objectClass: 'AssortmentPublishedToFlexPLM',
|
|
1895
|
+
outboundPublishEvent,
|
|
1896
|
+
initialEvent: {}
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrail/flexplm",
|
|
3
|
-
"version": "1.6.0-alpha.
|
|
3
|
+
"version": "1.6.0-alpha.8e73fa3",
|
|
4
4
|
"description": "Library used for integration with flexplm.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"scripts/copy-template.js"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc
|
|
15
|
+
"build": "tsc; node scripts/copy-template.js",
|
|
16
|
+
"build:win": "tsc && node scripts/copy-template.js",
|
|
16
17
|
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
|
|
17
18
|
"lint": "tslint -p tsconfig.json",
|
|
18
19
|
"test": "jest",
|