@contrail/flexplm 1.5.1-alpha.c9b11be → 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.
Files changed (111) hide show
  1. package/lib/cli/commands/compile.d.ts +4 -0
  2. package/lib/cli/commands/compile.js +73 -0
  3. package/lib/cli/commands/compile.spec.d.ts +1 -0
  4. package/lib/cli/commands/compile.spec.js +80 -0
  5. package/lib/cli/commands/create.d.ts +5 -0
  6. package/lib/cli/commands/create.js +77 -0
  7. package/lib/cli/commands/create.spec.d.ts +1 -0
  8. package/lib/cli/commands/create.spec.js +78 -0
  9. package/lib/cli/commands/upload.d.ts +17 -0
  10. package/lib/cli/commands/upload.js +228 -0
  11. package/lib/cli/commands/upload.spec.d.ts +1 -0
  12. package/lib/cli/commands/upload.spec.js +88 -0
  13. package/lib/cli/index.d.ts +5 -0
  14. package/lib/cli/index.js +70 -0
  15. package/lib/cli/index.spec.d.ts +1 -0
  16. package/lib/cli/index.spec.js +85 -0
  17. package/lib/cli/template/mapping-template.ts.template +62 -0
  18. package/lib/entity-processor/base-entity-processor.d.ts +65 -0
  19. package/lib/entity-processor/base-entity-processor.js +71 -0
  20. package/lib/entity-processor/base-entity-processor.spec.js +1 -0
  21. package/lib/index.d.ts +1 -0
  22. package/lib/index.js +1 -0
  23. package/lib/interfaces/mapping-file.d.ts +783 -0
  24. package/lib/interfaces/mapping-file.js +2 -0
  25. package/lib/publish/base-process-publish-assortment.d.ts +26 -0
  26. package/lib/publish/base-process-publish-assortment.js +87 -13
  27. package/lib/publish/base-process-publish-assortment.spec.js +100 -9
  28. package/lib/publish/mockData.js +5 -0
  29. package/lib/transform/identifier-conversion-spec-mockData.js +34 -6
  30. package/lib/transform/identifier-conversion.d.ts +36 -0
  31. package/lib/transform/identifier-conversion.js +36 -0
  32. package/lib/transform/identifier-conversion.spec.js +4 -0
  33. package/lib/util/config-defaults.js +3 -0
  34. package/lib/util/config-defaults.spec.js +9 -0
  35. package/lib/util/data-converter-spec-mockData.js +17 -3
  36. package/lib/util/data-converter.d.ts +97 -0
  37. package/lib/util/data-converter.js +127 -1
  38. package/lib/util/data-converter.spec.js +2 -0
  39. package/lib/util/error-response-object.d.ts +5 -0
  40. package/lib/util/error-response-object.js +7 -0
  41. package/lib/util/event-short-message-status.js +1 -0
  42. package/lib/util/federation.js +8 -0
  43. package/lib/util/flexplm-connect.d.ts +7 -0
  44. package/lib/util/flexplm-connect.js +14 -0
  45. package/lib/util/logger-config.js +1 -0
  46. package/lib/util/map-util-spec-mockData.js +17 -3
  47. package/lib/util/map-utils.d.ts +27 -0
  48. package/lib/util/map-utils.js +27 -0
  49. package/lib/util/thumbnail-util.d.ts +21 -0
  50. package/lib/util/thumbnail-util.js +28 -1
  51. package/lib/util/thumbnail-util.spec.js +6 -0
  52. package/lib/util/type-conversion-utils-spec-mockData.js +3 -3
  53. package/lib/util/type-conversion-utils.d.ts +151 -0
  54. package/lib/util/type-conversion-utils.js +154 -0
  55. package/lib/util/type-defaults.d.ts +66 -0
  56. package/lib/util/type-defaults.js +66 -0
  57. package/lib/util/type-defaults.spec.js +5 -5
  58. package/lib/util/type-utils.d.ts +21 -0
  59. package/lib/util/type-utils.js +23 -0
  60. package/lib/util/type-utils.spec.js +2 -0
  61. package/package.json +22 -6
  62. package/scripts/copy-template.js +10 -0
  63. package/.github/pull_request_template.md +0 -31
  64. package/.github/workflows/flexplm-lib.yml +0 -27
  65. package/.github/workflows/publish-to-npm.yml +0 -121
  66. package/CHANGELOG.md +0 -49
  67. package/publish.bat +0 -5
  68. package/publish.sh +0 -5
  69. package/src/entity-processor/base-entity-processor.spec.ts +0 -689
  70. package/src/entity-processor/base-entity-processor.ts +0 -583
  71. package/src/flexplm-request.ts +0 -28
  72. package/src/flexplm-utils.spec.ts +0 -27
  73. package/src/flexplm-utils.ts +0 -29
  74. package/src/index.ts +0 -22
  75. package/src/interfaces/interfaces.ts +0 -122
  76. package/src/interfaces/item-family-changes.ts +0 -67
  77. package/src/interfaces/publish-change-data.ts +0 -43
  78. package/src/publish/base-process-publish-assortment-callback.ts +0 -50
  79. package/src/publish/base-process-publish-assortment.spec.ts +0 -2154
  80. package/src/publish/base-process-publish-assortment.ts +0 -1173
  81. package/src/publish/mockData.ts +0 -4561
  82. package/src/transform/identifier-conversion-spec-mockData.ts +0 -496
  83. package/src/transform/identifier-conversion.spec.ts +0 -386
  84. package/src/transform/identifier-conversion.ts +0 -282
  85. package/src/util/config-defaults.spec.ts +0 -445
  86. package/src/util/config-defaults.ts +0 -106
  87. package/src/util/data-converter-spec-mockData.ts +0 -231
  88. package/src/util/data-converter.spec.ts +0 -1622
  89. package/src/util/data-converter.ts +0 -819
  90. package/src/util/error-response-object.spec.ts +0 -116
  91. package/src/util/error-response-object.ts +0 -50
  92. package/src/util/event-short-message-status.ts +0 -22
  93. package/src/util/federation.ts +0 -172
  94. package/src/util/flexplm-connect.spec.ts +0 -132
  95. package/src/util/flexplm-connect.ts +0 -208
  96. package/src/util/logger-config.ts +0 -20
  97. package/src/util/map-util-spec-mockData.ts +0 -231
  98. package/src/util/map-utils.spec.ts +0 -103
  99. package/src/util/map-utils.ts +0 -41
  100. package/src/util/mockData.ts +0 -101
  101. package/src/util/thumbnail-util.spec.ts +0 -508
  102. package/src/util/thumbnail-util.ts +0 -272
  103. package/src/util/type-conversion-utils-spec-mockData.ts +0 -272
  104. package/src/util/type-conversion-utils.spec.ts +0 -1031
  105. package/src/util/type-conversion-utils.ts +0 -490
  106. package/src/util/type-defaults.spec.ts +0 -797
  107. package/src/util/type-defaults.ts +0 -320
  108. package/src/util/type-utils.spec.ts +0 -227
  109. package/src/util/type-utils.ts +0 -144
  110. package/tsconfig.json +0 -24
  111. package/tslint.json +0 -57
@@ -0,0 +1,783 @@
1
+ import { FunctionTransformers, RekeyTransformers, TransformDefinition, TransformTask } from "@contrail/transform-data";
2
+ /**
3
+ * One direction of a {@link MappingSection} (either `vibe2flex` or `flex2vibe`).
4
+ *
5
+ * Holds the per-direction transform pipeline plus the named lookup tables
6
+ * (`rekey`, `removeKey`, `valueTransform`) that the tasks in
7
+ * {@link DirectionalSection.transformOrder} reference by key. A REKEY task
8
+ * configured with `rekeyTransformersKey: 'rekey'` resolves to
9
+ * {@link DirectionalSection.rekey} on the same directional section.
10
+ *
11
+ * The named members below cover the conventional shape, but the section
12
+ * is open: each task in a pipeline resolves its config by reading the
13
+ * sibling property whose name is supplied by the matching `*Key` pointer
14
+ * on the task. That means a directional section can hold:
15
+ * - additional `TransformTask[]` pipelines under names other than
16
+ * `transformOrder` (selected by passing a non-default `orderKey` to
17
+ * `MapFileUtil.getTransformTasks`)
18
+ * - additional REKEY / REMOVE / MORPH / VALUE_TRANSFORM / CONDITIONAL
19
+ * lookup tables under arbitrary names referenced by the task's
20
+ * `rekeyTransformersKey`, `removeKeysKey`, `functionTransformersKey`,
21
+ * or `conditionalTransformDefinitionsKey`.
22
+ *
23
+ * @example
24
+ * vibe2flex: {
25
+ * transformOrder: [
26
+ * { processor: 'VALUE_TRANSFORM', functionTransformersKey: 'valueTransform' },
27
+ * { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
28
+ * { processor: 'REMOVE', removeKeysKey: 'removeKey' },
29
+ * ],
30
+ * rekey: { productName: 'name', custBrand: 'brand' },
31
+ * valueTransform: {
32
+ * itemNumber: (row) => '' + row['itemNumber'],
33
+ * },
34
+ * removeKey: ['patternReference', 'vibeOnlyProp'],
35
+ * }
36
+ */
37
+ export interface DirectionalSection {
38
+ /**
39
+ * Ordered pipeline of transforms applied to each row in this direction.
40
+ *
41
+ * Each task is dispatched by its `processor` discriminator
42
+ * (REKEY, REMOVE, VALUE_TRANSFORM, MORPH, CONDITIONAL) and reads its
43
+ * configuration from the corresponding sibling property on this section
44
+ * (e.g. a REKEY task with `rekeyTransformersKey: 'rekey'` reads
45
+ * {@link DirectionalSection.rekey}).
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
+ *
76
+ * @example
77
+ * // Conventional: REMOVE → VALUE_TRANSFORM → REKEY.
78
+ * transformOrder: [
79
+ * { processor: 'REMOVE', removeKeysKey: 'removeKey' },
80
+ * { processor: 'VALUE_TRANSFORM', functionTransformersKey: 'valueTransform' },
81
+ * { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
82
+ * ]
83
+ */
84
+ transformOrder?: TransformTask[];
85
+ /**
86
+ * Resolves the FlexPLM object class (vibe2flex) or VibeIQ entity class
87
+ * (flex2vibe) for a row when it cannot be derived from the row itself.
88
+ *
89
+ * Consulted by `TypeConversionUtils.getObjectClass` /
90
+ * `getEntityClassFromObject` after the explicit `flexPLMObjectClass` /
91
+ * `vibeIQEntityClass` property and before the `TypeDefaults` fallback.
92
+ *
93
+ * @example
94
+ * // vibe2flex
95
+ * getClass: () => 'LCSRevisableEntity'
96
+ * // flex2vibe
97
+ * getClass: () => 'custom-entity'
98
+ */
99
+ getClass?: (entity: Record<string, unknown>) => string;
100
+ /**
101
+ * Resolves the FlexPLM type path (vibe2flex) or VibeIQ type path
102
+ * (flex2vibe) for a row when it cannot be derived from the row itself.
103
+ *
104
+ * Consulted by `TypeConversionUtils.getObjectTypePath` /
105
+ * `getEntityTypePathFromObject` after the explicit `flexPLMTypePath` /
106
+ * `vibeIQTypePath` property and before the `TypeDefaults` fallback.
107
+ *
108
+ * @example
109
+ * // vibe2flex
110
+ * getSoftType: () => 'Revisable Entity\\Product Concept'
111
+ * // flex2vibe
112
+ * getSoftType: () => 'custom-entity:styleConcept'
113
+ */
114
+ getSoftType?: (entity: Record<string, unknown>) => string;
115
+ /**
116
+ * Lookup table consumed by REKEY tasks in {@link DirectionalSection.transformOrder}.
117
+ *
118
+ * **Read each entry as `destination: 'source'`** — the property name (left)
119
+ * is the key written onto the row, the string value (right) is the
120
+ * existing key whose value is read. Each entry copies (or moves, when the
121
+ * task sets `rekeyDelete: true`) the value at the source key onto the
122
+ * destination key. Referenced by `rekeyTransformersKey` on the REKEY task.
123
+ *
124
+ * Because REKEY runs on the row in its **input** shape, the source side
125
+ * matches the direction's input system and the destination side matches
126
+ * its output system:
127
+ * - Inside `vibe2flex.rekey`: `flexKey: 'vibeKey'` (vibe in → flex out).
128
+ * - Inside `flex2vibe.rekey`: `vibeKey: 'flexKey'` (flex in → vibe out).
129
+ *
130
+ * @example
131
+ * // vibe2flex — left side is the FlexPLM key, right side is the VibeIQ key
132
+ * rekey: {
133
+ * productName: 'name', // flex 'productName' ← vibe 'name'
134
+ * custBrand: 'brand', // flex 'custBrand' ← vibe 'brand'
135
+ * custStyleNumber: 'styleNumber', // flex 'custStyleNumber' ← vibe 'styleNumber'
136
+ * }
137
+ *
138
+ * @example
139
+ * // flex2vibe — left side is the VibeIQ key, right side is the FlexPLM key
140
+ * rekey: {
141
+ * name: 'productName', // vibe 'name' ← flex 'productName'
142
+ * brand: 'custBrand', // vibe 'brand' ← flex 'custBrand'
143
+ * styleNumber: 'custStyleNumber', // vibe 'styleNumber' ← flex 'custStyleNumber'
144
+ * }
145
+ */
146
+ rekey?: RekeyTransformers;
147
+ /**
148
+ * Lookup table consumed by REMOVE tasks in {@link DirectionalSection.transformOrder}.
149
+ *
150
+ * Names of keys to delete from each row. Referenced by `removeKeysKey`
151
+ * on the REMOVE task.
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
+ *
181
+ * @example
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
+ * ]
189
+ */
190
+ removeKey?: string[];
191
+ /**
192
+ * Lookup table consumed by VALUE_TRANSFORM tasks in
193
+ * {@link DirectionalSection.transformOrder}.
194
+ *
195
+ * Each entry's key is the property the function's return value is written
196
+ * to on the row; the function receives the full row plus optional
197
+ * dependencies. Referenced by `functionTransformersKey` on the
198
+ * VALUE_TRANSFORM task.
199
+ *
200
+ * @example
201
+ * valueTransform: {
202
+ * itemNumber: (row) => {
203
+ * const numValue = parseInt(row['itemNumber'], 10);
204
+ * return isNaN(numValue) ? row['itemNumber'] : numValue;
205
+ * },
206
+ * }
207
+ */
208
+ valueTransform?: Record<string, (row: Record<string, unknown>, dependencies?: unknown) => unknown>;
209
+ /**
210
+ * Lookup table consumed by MORPH tasks in {@link DirectionalSection.transformOrder}.
211
+ *
212
+ * Each entry's function receives the full row plus optional dependencies
213
+ * and may rewrite/augment it in place; unlike {@link DirectionalSection.valueTransform},
214
+ * the function key is just an identifier and is not the destination
215
+ * property. Referenced by `functionTransformersKey` on the MORPH task.
216
+ *
217
+ * @example
218
+ * morphTransform: {
219
+ * morph1: (row) => {
220
+ * const val = row['productStatus'];
221
+ * if (val && Object.keys(val).length === 0) {
222
+ * delete row['productStatus'];
223
+ * }
224
+ * },
225
+ * }
226
+ */
227
+ morphTransform?: Record<string, (row: Record<string, unknown>, dependencies?: unknown) => unknown>;
228
+ /**
229
+ * Catch-all for additional pipelines and per-processor lookup tables
230
+ * referenced by sibling `*Key` pointers on tasks. Each entry should be
231
+ * one of:
232
+ * - {@link TransformTask}[] — alternate pipeline (read it by passing
233
+ * its property name as `orderKey` to `MapFileUtil.getTransformTasks`)
234
+ * - {@link RekeyTransformers} — `newKey` → `existingKey` map for REKEY
235
+ * - `string[]` — keys to delete for REMOVE
236
+ * - {@link FunctionTransformers} — Record<string, Function> for MORPH or VALUE_TRANSFORM
237
+ * - {@link TransformDefinition}[] — conditional rules for CONDITIONAL
238
+ */
239
+ [key: string]: TransformTask[] | TransformDefinition[] | RekeyTransformers | FunctionTransformers | Record<string, unknown> | string[] | ((entity: Record<string, unknown>) => string) | ((entity: Record<string, unknown>) => string[]) | ((row: Record<string, unknown>, dependencies?: unknown) => unknown) | undefined;
240
+ }
241
+ /**
242
+ * Per-entity-type configuration block, keyed in the {@link MappingFile}
243
+ * by the section key returned from
244
+ * {@link TypeConversionEntry.getMapKey} (e.g. `LCSProduct`,
245
+ * `'Exchange Rate'`).
246
+ *
247
+ * Holds direction-agnostic settings (ownership, identifier/informational
248
+ * properties, creation and image-sync gates) plus the two
249
+ * {@link DirectionalSection}s that drive the actual data transforms.
250
+ *
251
+ * @example
252
+ * LCSProduct: {
253
+ * // Identifier — used for lookup / create-vs-update routing.
254
+ * getIdentifierProperties: () => ['styleNumber'],
255
+ * // Vibe-owned attributes.
256
+ * vibeOwningKeys: ['lifecycleStage', 'targetIntroDate'],
257
+ * vibe2flex: {
258
+ * transformOrder: [
259
+ * { processor: 'REMOVE', removeKeysKey: 'removeKey' },
260
+ * { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
261
+ * ],
262
+ * // Strip Vibe-only / Flex-owned-inbound props before publish.
263
+ * removeKey: ['federatedId'],
264
+ * // destination: source -> flexKey: 'vibeKey'
265
+ * rekey: { productName: 'name' },
266
+ * },
267
+ * flex2vibe: {
268
+ * transformOrder: [
269
+ * { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
270
+ * ],
271
+ * // destination: source -> vibeKey: 'flexKey'
272
+ * rekey: { name: 'productName' },
273
+ * },
274
+ * }
275
+ */
276
+ export interface MappingSection {
277
+ /**
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.
313
+ *
314
+ * @example
315
+ * // Right: Vibe-owned attributes;
316
+ * // If Vibe owns the identifier, include it here as well.
317
+ * vibeOwningKeys: ['itemNumber', 'lifecycleStage', 'targetIntroDate']
318
+ */
319
+ vibeOwningKeys?: string[];
320
+ /**
321
+ * Tag identifying which identity service uniqueness pool this entity participates in.
322
+ * There can only be one entity with a given value for the property(defined by `getIdentifierProperties`) within the same uniqueness pool.
323
+ *
324
+ * @example
325
+ * uniquenessPool: 'item'
326
+ */
327
+ uniquenessPool?: string;
328
+ /**
329
+ * Gate controlling whether an inbound FlexPLM object is allowed to
330
+ * create a new VibeIQ entity. When the function returns `false`, the
331
+ * inbound row may still update an existing entity but will not create
332
+ * one. Consulted by `TypeConversionUtils.isInboundCreatableFromObject`;
333
+ * defaults to `false` when the function is absent.
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
+ *
352
+ * @example
353
+ * // Flex owns creation.
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
+ * }
372
+ */
373
+ isInboundCreatable?: (entity: Record<string, unknown>, context?: unknown) => boolean;
374
+ /**
375
+ * Gate controlling whether an outbound VibeIQ entity is allowed to
376
+ * create a new FlexPLM object. When the function returns `false`, the
377
+ * outbound publish becomes an update-only operation. Consulted by
378
+ * `TypeConversionUtils.isOutboundCreatableFromEntity`; defaults to
379
+ * `true` when the function is absent.
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
+ *
393
+ * @example
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
+ * }
399
+ */
400
+ isOutboundCreatable?: (entity: Record<string, unknown>, context?: unknown) => boolean;
401
+ /**
402
+ * Gate controlling whether image/thumbnail content should be synced
403
+ * from FlexPLM into VibeIQ for this entity type. Consulted by
404
+ * `TypeConversionUtils.syncInboundImages`; defaults to `false` when
405
+ * the function is absent. Typically aligned with whichever system
406
+ * owns creation of the entity.
407
+ *
408
+ * @example
409
+ * syncInboundImages: () => true
410
+ */
411
+ syncInboundImages?: (entity: Record<string, unknown>, context?: unknown) => boolean;
412
+ /**
413
+ * Gate controlling whether image/thumbnail content should be synced
414
+ * from VibeIQ out to FlexPLM for this entity type. Consulted by
415
+ * `TypeConversionUtils.syncOutboundImages`; defaults to `true` when
416
+ * the function is absent. Typically aligned with whichever system
417
+ * owns creation of the entity.
418
+ *
419
+ * @example
420
+ * syncOutboundImages: () => false
421
+ */
422
+ syncOutboundImages?: (entity: Record<string, unknown>, context?: unknown) => boolean;
423
+ /**
424
+ * Returns the property names that uniquely identify this entity. Used
425
+ * by `TypeConversionUtils.getIdentifierProperties` to drive lookup and
426
+ * matching when an explicit `flexPLMIdentifierProperties` /
427
+ * `vibeIQIdentifierProperties` is not present on the row, falling
428
+ * back to `TypeDefaults` when this is absent.
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
+ *
471
+ * @example
472
+ * // Single business key
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']
479
+ */
480
+ getIdentifierProperties?: (entity: Record<string, unknown>) => string[];
481
+ /**
482
+ * Returns supplemental property names that should be carried alongside
483
+ * identifiers (e.g. display names) but do not participate in identity.
484
+ * Consulted by `TypeConversionUtils.getInformationalProperties` with
485
+ * the same lookup precedence as {@link MappingSection.getIdentifierProperties}.
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
+ *
504
+ * @example
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']
516
+ */
517
+ getInformationalProperties?: (entity: Record<string, unknown>) => string[];
518
+ /**
519
+ * Transform configuration applied when sending data from VibeIQ to FlexPLM.
520
+ * Pipeline runs on rows in their VibeIQ shape and produces rows in their
521
+ * FlexPLM shape, so inside `rekey` the destination (left) is the FlexPLM
522
+ * key and the source (right) is the VibeIQ key — see
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`.
541
+ */
542
+ vibe2flex?: DirectionalSection;
543
+ /**
544
+ * Transform configuration applied when receiving data from FlexPLM into VibeIQ.
545
+ * Pipeline runs on rows in their FlexPLM shape and produces rows in their
546
+ * VibeIQ shape, so inside `rekey` the destination (left) is the VibeIQ
547
+ * key and the source (right) is the FlexPLM key — see
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.
564
+ */
565
+ flex2vibe?: DirectionalSection;
566
+ }
567
+ /**
568
+ * Entry in the {@link TypeConversionSection} that maps a single source
569
+ * type onto the {@link MappingSection} key that should handle it.
570
+ *
571
+ * Useful for fanning a single VibeIQ `entityType` (e.g. `'custom-entity'`)
572
+ * out to multiple sections based on the row's `typePath`.
573
+ */
574
+ export interface TypeConversionEntry {
575
+ /**
576
+ * Returns the {@link MappingFile} key whose {@link MappingSection}
577
+ * should be used to transform `entity`. Returning an empty string
578
+ * signals that no section applies and the row should be skipped.
579
+ *
580
+ * @example
581
+ * getMapKey: (entity) => {
582
+ * switch (entity['typePath']) {
583
+ * case 'custom-entity:exchangeRate': return 'Exchange Rate';
584
+ * case 'custom-entity:styleConcept': return 'Style Concept Master';
585
+ * default: return '';
586
+ * }
587
+ * }
588
+ */
589
+ getMapKey: (entity: Record<string, unknown>) => string;
590
+ }
591
+ /**
592
+ * One direction of {@link TypeConversion}, keyed by the source-system
593
+ * type:
594
+ * - In `vibe2flex`, keys are VibeIQ entity types (e.g. `'custom-entity'`,
595
+ * `'size-range-template'`) — see `TypeConversionUtils.getEntityType`.
596
+ * - In `flex2vibe`, keys are FlexPLM object classes (e.g. `LCSProduct`) —
597
+ * see `TypeConversionUtils.getObjectType`.
598
+ */
599
+ export interface TypeConversionSection {
600
+ [type: string]: TypeConversionEntry;
601
+ }
602
+ /**
603
+ * Top-level routing table that resolves a row to its
604
+ * {@link MappingSection} key for each direction.
605
+ *
606
+ * `TypeConversionUtils.getMapKey` / `getMapKeyFromObject` look up the
607
+ * row's type in the appropriate side, invoke
608
+ * {@link TypeConversionEntry.getMapKey}, and use the returned string
609
+ * as the key into the rest of the {@link MappingFile}.
610
+ *
611
+ * @example
612
+ * typeConversion: {
613
+ * vibe2flex: {
614
+ * 'custom-entity': {
615
+ * getMapKey: (entity) => {
616
+ * switch (entity['typePath']) {
617
+ * case 'custom-entity:exchangeRate': return 'Exchange Rate';
618
+ * default: return '';
619
+ * }
620
+ * },
621
+ * },
622
+ * 'size-range-template': {
623
+ * getMapKey: () => 'size-range-template',
624
+ * },
625
+ * },
626
+ * flex2vibe: {
627
+ * LCSRevisableEntity: {
628
+ * getMapKey: (object) => {
629
+ * switch (object['flexPLMTypePath']) {
630
+ * case 'Revisable Entity\\Exchange Rate': return 'Exchange Rate';
631
+ * default: return '';
632
+ * }
633
+ * },
634
+ * },
635
+ * },
636
+ * }
637
+ */
638
+ export interface TypeConversion {
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
+ */
648
+ vibe2flex: TypeConversionSection;
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
+ */
671
+ flex2vibe: TypeConversionSection;
672
+ }
673
+ /** Identifies the application and tenant that owns a {@link MappingFile}. */
674
+ export interface OrgInfo {
675
+ /** This is the identifier for the application, and is used to set the owner of the mapping file */
676
+ appIdentifier: '@vibeiq/flexplm-connector';
677
+ /** The name of the organization using the mapping file. */
678
+ orgName: string;
679
+ }
680
+ interface MappingFileBase {
681
+ /** The information about the organization using the mapping file */
682
+ orgInfo: OrgInfo;
683
+ /** The type conversion information for the mapping file. */
684
+ typeConversion: TypeConversion;
685
+ }
686
+ /**
687
+ * Full mapping file driving bidirectional sync between VibeIQ and FlexPLM
688
+ * for a single tenant.
689
+ *
690
+ * Combines the fixed {@link MappingFileBase} fields (`orgInfo`,
691
+ * `typeConversion`) with an open set of {@link MappingSection} entries
692
+ * keyed by the strings returned from
693
+ * {@link TypeConversionEntry.getMapKey} (e.g. `LCSProduct`, `LCSSKU`,
694
+ * `'Exchange Rate'`).
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
+ *
729
+ * @example
730
+ * export const mapping: MappingFile = {
731
+ * orgInfo: {
732
+ * appIdentifier: '@vibeiq/flexplm-connector',
733
+ * orgName: 'acme',
734
+ * },
735
+ * typeConversion: {
736
+ * vibe2flex: {
737
+ * 'custom-entity': {
738
+ * getMapKey: (entity) => {
739
+ * switch (entity['typePath']) {
740
+ * case 'custom-entity:exchangeRate': return 'Exchange Rate';
741
+ * default: return '';
742
+ * }
743
+ * },
744
+ * },
745
+ * 'size-range-template': {
746
+ * getMapKey: () => 'size-range-template',
747
+ * },
748
+ * },
749
+ * flex2vibe: {
750
+ * LCSRevisableEntity: {
751
+ * getMapKey: (object) => {
752
+ * switch (object['flexPLMTypePath']) {
753
+ * case 'Revisable Entity\\Exchange Rate': return 'Exchange Rate';
754
+ * default: return '';
755
+ * }
756
+ * },
757
+ * },
758
+ * },
759
+ * },
760
+ * 'Exchange Rate': {
761
+ * vibe2flex: {
762
+ * transformOrder: [
763
+ * { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
764
+ * ],
765
+ * rekey: { exchangeRateDescription: 'name' },
766
+ * getClass: () => 'LCSRevisableEntity',
767
+ * getSoftType: () => 'Revisable Entity\\Exchange Rate',
768
+ * },
769
+ * flex2vibe: {
770
+ * transformOrder: [
771
+ * { processor: 'REKEY', rekeyDelete: true, rekeyKeepEmptyValues: true, rekeyTransformersKey: 'rekey' },
772
+ * ],
773
+ * rekey: { name: 'exchangeRateDescription' },
774
+ * getClass: () => 'custom-entity',
775
+ * getSoftType: () => 'custom-entity:exchangeRate',
776
+ * },
777
+ * },
778
+ * };
779
+ */
780
+ export type MappingFile = MappingFileBase & {
781
+ [mapKey: string]: MappingSection | MappingFileBase[keyof MappingFileBase];
782
+ };
783
+ export {};