map-transform 0.3.11 → 0.4.0-alpha.3

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 (112) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +573 -98
  3. package/dist/functions/compare.d.ts +6 -5
  4. package/dist/functions/compare.js +21 -16
  5. package/dist/functions/compare.js.map +1 -1
  6. package/dist/functions/explode.d.ts +6 -0
  7. package/dist/functions/explode.js +45 -0
  8. package/dist/functions/explode.js.map +1 -0
  9. package/dist/functions/get.d.ts +2 -2
  10. package/dist/functions/get.js +4 -2
  11. package/dist/functions/get.js.map +1 -1
  12. package/dist/functions/index.d.ts +13 -3
  13. package/dist/functions/index.js +14 -4
  14. package/dist/functions/index.js.map +1 -1
  15. package/dist/functions/join.d.ts +2 -2
  16. package/dist/functions/join.js +1 -1
  17. package/dist/functions/join.js.map +1 -1
  18. package/dist/functions/joinSplit.d.ts +8 -0
  19. package/dist/functions/joinSplit.js +33 -0
  20. package/dist/functions/joinSplit.js.map +1 -0
  21. package/dist/functions/logical.d.ts +7 -0
  22. package/dist/functions/logical.js +28 -0
  23. package/dist/functions/logical.js.map +1 -0
  24. package/dist/functions/map.d.ts +8 -0
  25. package/dist/functions/map.js +43 -0
  26. package/dist/functions/map.js.map +1 -0
  27. package/dist/functions/not.d.ts +2 -2
  28. package/dist/functions/not.js.map +1 -1
  29. package/dist/functions/template.d.ts +7 -0
  30. package/dist/functions/template.js +27 -0
  31. package/dist/functions/template.js.map +1 -0
  32. package/dist/functions/validate.d.ts +2 -2
  33. package/dist/functions/validate.js +1 -1
  34. package/dist/functions/validate.js.map +1 -1
  35. package/dist/functions/value.d.ts +4 -0
  36. package/dist/functions/value.js +20 -0
  37. package/dist/functions/value.js.map +1 -0
  38. package/dist/index.d.ts +8 -7
  39. package/dist/index.js +45 -27
  40. package/dist/index.js.map +1 -1
  41. package/dist/operations/alt.js +4 -9
  42. package/dist/operations/alt.js.map +1 -1
  43. package/dist/operations/apply.d.ts +2 -0
  44. package/dist/operations/apply.js +15 -0
  45. package/dist/operations/apply.js.map +1 -0
  46. package/dist/operations/concat.js +4 -4
  47. package/dist/operations/concat.js.map +1 -1
  48. package/dist/operations/directionals.d.ts +1 -1
  49. package/dist/operations/directionals.js +6 -5
  50. package/dist/operations/directionals.js.map +1 -1
  51. package/dist/operations/filter.d.ts +2 -2
  52. package/dist/operations/filter.js +6 -3
  53. package/dist/operations/filter.js.map +1 -1
  54. package/dist/operations/fixed.d.ts +2 -2
  55. package/dist/operations/fixed.js +5 -1
  56. package/dist/operations/fixed.js.map +1 -1
  57. package/dist/operations/getSet.d.ts +3 -3
  58. package/dist/operations/getSet.js +18 -24
  59. package/dist/operations/getSet.js.map +1 -1
  60. package/dist/operations/ifelse.d.ts +2 -0
  61. package/dist/operations/ifelse.js +18 -0
  62. package/dist/operations/ifelse.js.map +1 -0
  63. package/dist/operations/iterate.d.ts +2 -0
  64. package/dist/operations/iterate.js +23 -0
  65. package/dist/operations/iterate.js.map +1 -0
  66. package/dist/operations/lookup.js +3 -3
  67. package/dist/operations/lookup.js.map +1 -1
  68. package/dist/operations/merge.d.ts +2 -0
  69. package/dist/operations/merge.js +20 -0
  70. package/dist/operations/merge.js.map +1 -0
  71. package/dist/operations/modify.d.ts +2 -0
  72. package/dist/operations/modify.js +18 -0
  73. package/dist/operations/modify.js.map +1 -0
  74. package/dist/operations/mutate.js +48 -8
  75. package/dist/operations/mutate.js.map +1 -1
  76. package/dist/operations/pipe.js +9 -5
  77. package/dist/operations/pipe.js.map +1 -1
  78. package/dist/operations/plug.js +1 -1
  79. package/dist/operations/plug.js.map +1 -1
  80. package/dist/operations/root.js +2 -2
  81. package/dist/operations/root.js.map +1 -1
  82. package/dist/operations/transform.js +2 -2
  83. package/dist/operations/transform.js.map +1 -1
  84. package/dist/operations/value.d.ts +2 -2
  85. package/dist/operations/value.js +5 -1
  86. package/dist/operations/value.js.map +1 -1
  87. package/dist/types.d.ts +46 -21
  88. package/dist/utils/definitionHelpers.d.ts +8 -9
  89. package/dist/utils/definitionHelpers.js +60 -42
  90. package/dist/utils/definitionHelpers.js.map +1 -1
  91. package/dist/utils/functional.d.ts +2 -0
  92. package/dist/utils/functional.js +8 -0
  93. package/dist/utils/functional.js.map +1 -0
  94. package/dist/utils/is.d.ts +1 -0
  95. package/dist/utils/is.js +6 -0
  96. package/dist/utils/is.js.map +1 -0
  97. package/dist/utils/pathGetter.d.ts +2 -2
  98. package/dist/utils/pathGetter.js +23 -9
  99. package/dist/utils/pathGetter.js.map +1 -1
  100. package/dist/utils/pathSetter.d.ts +3 -3
  101. package/dist/utils/pathSetter.js +19 -21
  102. package/dist/utils/pathSetter.js.map +1 -1
  103. package/dist/utils/stateHelpers.d.ts +7 -36
  104. package/dist/utils/stateHelpers.js +21 -10
  105. package/dist/utils/stateHelpers.js.map +1 -1
  106. package/package.json +18 -56
  107. package/dist/functions/alt.d.ts +0 -6
  108. package/dist/functions/alt.js +0 -10
  109. package/dist/functions/alt.js.map +0 -1
  110. package/dist/utils/objectToMapFunction.d.ts +0 -2
  111. package/dist/utils/objectToMapFunction.js +0 -34
  112. package/dist/utils/objectToMapFunction.js.map +0 -1
package/README.md CHANGED
@@ -85,8 +85,8 @@ const def2 = [
85
85
  {
86
86
  title: 'name',
87
87
  author: 'meta.author',
88
- date: 'meta.date'
89
- }
88
+ date: 'meta.date',
89
+ },
90
90
  ]
91
91
 
92
92
  const target2 = mapTransform(def2)(source)
@@ -105,15 +105,15 @@ const { mapTransform, transform } = require('map-transform')
105
105
  // ....
106
106
 
107
107
  // Write a transform function, that accepts a value and returns a value
108
- const msToDate = ms => new Date(ms).toISOString()
108
+ const msToDate = (ms) => new Date(ms).toISOString()
109
109
 
110
110
  const def3 = [
111
111
  'data[0].content',
112
112
  {
113
113
  title: 'name',
114
114
  author: 'meta.author',
115
- date: ['meta.date', transform(msToDate)]
116
- }
115
+ date: ['meta.date', transform(msToDate)],
116
+ },
117
117
  ]
118
118
 
119
119
  const target3 = mapTransform(def3)(source)
@@ -143,6 +143,14 @@ Install from npm:
143
143
  npm install map-transform --save
144
144
  ```
145
145
 
146
+ ## Breaking changes in v0.4
147
+
148
+ - Map objects won't be mapped over an array by default. You have to specify
149
+ `$iterate: true`
150
+ - The `alt` operation now accepts any type of pipeline, but not a helper
151
+ function
152
+ - The root path prefix is changed from `$` to `^`
153
+
146
154
  ## Usage
147
155
 
148
156
  ### The transform object
@@ -159,15 +167,15 @@ taste.
159
167
 
160
168
  ```javascript
161
169
  const def1 = {
162
- 'data.entry.title': 'heading'
170
+ 'data.entry.title': 'heading',
163
171
  }
164
172
 
165
173
  const def2 = {
166
174
  data: {
167
175
  entry: {
168
- title: 'heading'
169
- }
170
- }
176
+ title: 'heading',
177
+ },
178
+ },
171
179
  }
172
180
 
173
181
  // def1 and def2 are identical, and will result in an object like this:
@@ -180,28 +188,55 @@ const def2 = {
180
188
  // }
181
189
  ```
182
190
 
183
- If MapTransform happens upon an array in the source data, it will map it and
184
- set an array where each item is mapped according to the mapping object. But to
185
- ensure that you get an array, even when the source data contains only an object,
186
- you may suffix a key with brackets `[]`.
191
+ When you transform an array of data with a mapping object, each item in the
192
+ data array will be be transformed with the mapping object.
187
193
 
188
194
  ```javascript
189
195
  const def3 = {
190
- 'data.entries[]': {
196
+ title: 'heading',
197
+ }
198
+
199
+ // -->
200
+ // [
201
+ // { title: 'The first heading' },
202
+ // { title: 'The second heading' }
203
+ // ]
204
+ ```
205
+
206
+ If you want to apply the mapping object to the entire array – not each item in
207
+ the array – set `$iterate: false` on the mapping object or wrap the object in a
208
+ transform pipeline.
209
+
210
+ **Note:** When a mapping object is part of a transform pipeline, the default is
211
+ to not iterate. See [**transform pipeline**](#transform-pipeline) for more.
212
+
213
+ A key will set whatever is returned by the pipeline (see
214
+ [next section](#values-on-the-transform-object)), whether it is a string, a
215
+ boolean, an array, etc. If you want to ensure that you always get an array, you
216
+ can suffix the key with `[]`. Any value that is not an array will be wrapped in
217
+ one.
218
+
219
+ ```javascript
220
+ const def27 = {
221
+ $iterate: false
222
+ 'articles[]': {
191
223
  title: 'heading'
192
224
  }
193
225
  }
194
226
 
195
- // def3 will always give you entries as an array:
227
+ // -->
196
228
  // {
197
- // data: {
198
- // entries: [
199
- // {title: 'The actual heading'}
200
- // ]
201
- // }
229
+ // articles: [
230
+ // { title: 'Wrapped in an array, even if the data was just an object' },
231
+ // ]
202
232
  // }
203
233
  ```
204
234
 
235
+ A bonus of using the `[]` suffix, is that when key has another transform object
236
+ as its value, this transform object will be iterated by default (no need to set
237
+ the `$iterate` property). This does not happen to pipelines, paths, or
238
+ operations.
239
+
205
240
  #### Values on the transform object
206
241
 
207
242
  The values on the transform objects define how to retrieve and transform data
@@ -217,7 +252,7 @@ is at this path on the source object.
217
252
 
218
253
  ```javascript
219
254
  const def4 = {
220
- title: 'data.item.heading'
255
+ title: 'data.item.heading',
221
256
  }
222
257
 
223
258
  const source1 = {
@@ -225,9 +260,9 @@ const source1 = {
225
260
  item: {
226
261
  id: 'item1',
227
262
  heading: 'The actual heading',
228
- intro: 'The actual intro'
229
- }
230
- }
263
+ intro: 'The actual intro',
264
+ },
265
+ },
231
266
  }
232
267
 
233
268
  // `mapTransform(def4)(source1)` will transform to:
@@ -251,7 +286,7 @@ an array by indicating the array index in the brackets.
251
286
 
252
287
  ```javascript
253
288
  const def5 = {
254
- title: 'data.items[0].heading'
289
+ title: 'data.items[0].heading',
255
290
  }
256
291
 
257
292
  // def5 will pull the heading from the first item in the `items` array, and will
@@ -267,6 +302,30 @@ the transform pipeline (which the dot notation path really is, and – come to
267
302
  think of it – the transform object itself too). This is explained in detail
268
303
  below.
269
304
 
305
+ #### A note on undefined and null
306
+
307
+ MapTransform will treat `undefined` as a value that is not set, and so a
308
+ transform object will not be applied to it. So if an `undefined` value meets
309
+ a transform object, it will produce `undefined`, regardsless of the shape of the
310
+ transform object.
311
+
312
+ This is not the case for `null`, though. MapTransform treats `null` as a value,
313
+ an intended nothing, and will apply a transform object to it, even though it
314
+ will most likely produce nothing but default values. To change this behavior,
315
+ set `mutateNull: false` on the `options` object passed to MapTransform. This
316
+ will essentially make MapTransform treat `null` the same way as `undefined` when
317
+ it comes to the transform object.
318
+
319
+ #### Directional transform objects
320
+
321
+ A transform object is by default applied on forward transformations and in
322
+ reverse. You may alter this by setting the `$direction` prop on a transform
323
+ object, with `fwd`, `rev`, or `both` (the default) as possible values.
324
+
325
+ When running a forward transformation, transform objects marked with
326
+ `$direction: 'rev'` will be skipped. The same goes for `$direction: 'fwd'` in
327
+ reverse.
328
+
270
329
  ### Transform pipeline
271
330
 
272
331
  The idea of the transform pipeline, is that you describe a set of
@@ -277,7 +336,7 @@ original format again (although with a potential loss of data, if not all
277
336
  properties are transformed). This is what you do in a
278
337
  [reverse mapping](#reverse-mapping).
279
338
 
280
- One way to put it is that the pipeline describes the difference between the two
339
+ One way to put it, is that the pipeline describes the difference between the two
281
340
  possible states of the data, and allows you to go back and forth between them.
282
341
  Or you can just view it as operations applied in the order they are defined – or
283
342
  back again.
@@ -299,24 +358,33 @@ import { transform, filter } from 'map-transform'
299
358
  const def6 = [
300
359
  'data.items[]',
301
360
  {
361
+ $iterate: true,
302
362
  id: 'articleNo',
303
363
  title: ['headline', transform(maxLength(20))],
304
- sections: 'meta.sections[].id'
364
+ sections: 'meta.sections[].id',
305
365
  },
306
- filter(onlyItemsWithSection)
366
+ filter(onlyItemsWithSection),
307
367
  ]
308
368
  ```
309
369
 
310
370
  (Note that in this example, both `maxLength` and `onlyItemsWithSection` are
311
371
  custom functions for this case, but their implementations are not provided.)
312
372
 
313
- #### `transform(fn, fnRev)` operation
373
+ **A note on arrays:** In a transform pipeline, the default behavior is to treat
374
+ an array as any other data. The array will be passed on to a `transform()`
375
+ operation, the entire array will be set on a path, etc. This also means that a
376
+ mapping object will be applied to the entire array if nothing else is specified.
377
+ In the example above, we have set `$iterate: true` on the mapping object, to
378
+ signal that we want the mapping to be applied to the items of any array. See
379
+ also [the `iterate` operation](#iteratepipeline-operation) for more.
380
+
381
+ #### `transform(transformFn, transformFnRev)` operation
314
382
 
315
383
  The simple beauty of the `transform()` operation, is that it will apply whatever
316
384
  function you provide it with to the data at that point in the pipeline. It's
317
385
  completely up to you to write the function that does the transformation.
318
386
 
319
- You may supply a second function (`fnRev`), that will be used when
387
+ You may supply a second function (`transformFnRev`), that will be used when
320
388
  [reverse mapping](#reverse-mapping). If you only supplies one function, it will
321
389
  be used in both directions. You may supply `null` for either of these, to make
322
390
  it uni-directional, but it might be clearer to use `fwd()` or `rev()` operations
@@ -336,16 +404,16 @@ on an object. You would probably just not get the end result you expected.
336
404
  ```javascript
337
405
  import { mapTransform, transform } from 'map-transform'
338
406
 
339
- const ensureInteger = data => Number.parseInt(data, 10) || 0
407
+ const ensureInteger = (data) => Number.parseInt(data, 10) || 0
340
408
  const def7 = {
341
- count: ['statistics.views', transform(ensureInteger)]
409
+ count: ['statistics.views', transform(ensureInteger)],
342
410
  }
343
411
 
344
412
  const data = {
345
413
  statistics: {
346
- view: '18'
414
+ view: '18',
347
415
  // ...
348
- }
416
+ },
349
417
  }
350
418
 
351
419
  mapTransform(def7)(data)
@@ -381,41 +449,50 @@ Example transformation pipeline with a `yearsSince` function:
381
449
 
382
450
  ```javascript
383
451
  const def8 = {
384
- age: ['birthyear', yearsSince(new Date())]
452
+ age: ['birthyear', yearsSince(new Date())],
385
453
  }
386
454
  ```
387
455
 
456
+ **Note:** When the `transform` operation is applied to an array, it will not
457
+ iterate the array. Mapping over each item needs to be handled in the transform
458
+ itself, or wrap the `transform` operation in an `iterate` operation.
459
+
388
460
  You may also define a transform operation as an object:
389
461
 
390
462
  ```javascript
391
463
  import { mapTransform } from 'map-transform'
392
464
 
393
- const ensureInteger = operands => data => Number.parseInt(data, 10) || 0
394
- const customFunctions = { ensureInteger }
465
+ const ensureInteger = (operands) => (data) => Number.parseInt(data, 10) || 0
466
+ const functions = { ensureInteger }
395
467
  const def7asObject = {
396
- count: ['statistics.views', { $transform: 'ensureInteger' }]
468
+ count: ['statistics.views', { $transform: 'ensureInteger' }],
397
469
  }
398
470
 
399
471
  const data = {
400
472
  statistics: {
401
- view: '18'
473
+ view: '18',
402
474
  // ...
403
- }
475
+ },
404
476
  }
405
477
 
406
- mapTransform(def7asObject, { customFunctions })(data)
478
+ mapTransform(def7asObject, { functions })(data)
407
479
  // --> {
408
480
  // count: 18
409
481
  // }
410
482
  ```
411
483
 
412
- Note that the function itself is passed on the `customFunctions` object. When
484
+ Note that the function itself is passed on the `functions` object. When
413
485
  you provide the custom function this way, it should be given as a function
414
486
  accepting an object with operands / arguments, that returns the actual function
415
487
  used in the transform. Any properties given on the operation object, apart from
416
488
  `$transform`, will be passed in the `operands` object.
417
489
 
418
- #### `filter(fn)` operation
490
+ When you define the `transform` operation as an object, you may specify
491
+ `$iterate: true` on the object to apply the transform to every item on an array,
492
+ if an array is encountered. You may also set `$direction: 'fwd'` or
493
+ `$direction: 'rev'` to have it transform in one direction only.
494
+
495
+ #### `filter(conditionFn)` operation
419
496
 
420
497
  Just like the transform operation, the filter operation will apply whatever
421
498
  function you give it to the data at that point in the transform pipeline, but
@@ -458,7 +535,7 @@ Defining a filter operation as an object:
458
535
  import { mapTransform } from 'map-transform'
459
536
 
460
537
  const onlyActives = (data) => data.active
461
- const customFunctions = { onlyActives }
538
+ const functions = { onlyActives }
462
539
  const def9asObject = [
463
540
  'members'
464
541
  {
@@ -469,7 +546,120 @@ const def9asObject = [
469
546
  ]
470
547
  ```
471
548
 
472
- See the `transform()` operation on how defining as an object works.
549
+ You may also set `$direction: 'fwd'` or `$direction: 'rev'` on the object, to
550
+ have it filter in one direction only.
551
+
552
+ See the `transform()` operation for more on how defining as an object works.
553
+
554
+ #### `ifelse(conditionFn, truePipeline, falsePipeline)` operation
555
+
556
+ The `ifelse()` operation will run the `truePipeline` if the `conditionFn` results
557
+ in `true`, otherwise it will run the `falsePipeline`. See
558
+ [the `filter()` operation](#filterconditionFn-operation) for more on the
559
+ requirements for the `conditionFn`.
560
+
561
+ Both `truePipeline` and `falsePipeline` are optional, in case you only need to
562
+ apply a pipeline in one of the cases.
563
+
564
+ Example:
565
+
566
+ ```javascript
567
+ import { mapTransform, ifelse } from 'map-transform'
568
+
569
+ const onlyActives = (data) => data.active
570
+ const def31 = [
571
+ 'members'
572
+ {
573
+ name: 'name',
574
+ active: 'hasPayed'
575
+ },
576
+ ifelse(onlyActives, set('active[]'), set('inactive[]'))
577
+ ]
578
+ ```
579
+
580
+ #### `iterate(pipeline)` operation
581
+
582
+ If you want something to be mapped over the items of an array, the `iterate`
583
+ operation is your friend. When you wrap another operation, a pipeline, or a
584
+ mapping object in an `iterate` operation, it will be applied to each item.
585
+
586
+ When iterating, the state's `context` will iterate as well, to support `alt`
587
+ operations etc. that need to access the corresponding item in the context.
588
+
589
+ In this example, each value in the array returned by `statistics[].views` will
590
+ be transformed with the `ensureInteger` function, even if the function does not
591
+ support arrays:
592
+
593
+ ```javascript
594
+ import { mapTransform, iterate } from 'map-transform'
595
+
596
+ const ensureInteger = data => Number.parseInt(data, 10) || 0
597
+ const def26 = [
598
+ counts: ['statistics[].views', iterate(transform(ensureInteger))]
599
+ ]
600
+ ```
601
+
602
+ The `iterate` operation may also be used to wrap mapping objects in a transform
603
+ pipeline, however, setting `$iterate: true` on the object may be more
604
+ convenient.
605
+
606
+ #### `apply(pipelineId)` operation
607
+
608
+ The apply operation allows pipelines to be reused. Several pipelines may be
609
+ provided on the `pipelines` object on the `options` provided to
610
+ `mapTransform()`, and the keys of the `pipelines` object serves as ids.
611
+ When an id is passed to the apply operation as `pipelinedId`, the pipeline will
612
+ be applied in the place of the apply operation and executed as if it was part of
613
+ the pipeline definition in the first place.
614
+
615
+ ```javascript
616
+ import { mapTransform, apply, transform } from 'map-transform'
617
+
618
+ const ensureInteger = (data) => Number.parseInt(data, 10) || 0
619
+ const pipelines = {
620
+ castEntry: {
621
+ title: ['title', transform(String)],
622
+ count: ['count', transform(ensureInteger)],
623
+ },
624
+ }
625
+ const def25 = [
626
+ {
627
+ title: 'heading',
628
+ count: 'statistics.views',
629
+ },
630
+ apply('castEntry'),
631
+ ]
632
+
633
+ const data = {
634
+ heading: 'Entry 1',
635
+ statistics: {
636
+ view: '18',
637
+ },
638
+ }
639
+
640
+ mapTransform(def7)(data)
641
+ // --> {
642
+ // title: 'Entry 1',
643
+ // count: 18
644
+ // }
645
+ ```
646
+
647
+ You may also define the apply operation as an operation object:
648
+
649
+ ```javascript
650
+ const def25 = [
651
+ {
652
+ title: 'heading',
653
+ count: 'statistics.views',
654
+ },
655
+ { $apply: 'castEntry' },
656
+ ]
657
+ ```
658
+
659
+ When you define the `apply` operation as an object, you may specify
660
+ `$iterate: true` on the object to apply the pipeline to every item on an array,
661
+ if an array is encountered. You may also set `$direction: 'fwd'` or
662
+ `$direction: 'rev'` to have it apply in one direction only.
473
663
 
474
664
  #### `value(data)` operation
475
665
 
@@ -490,12 +680,23 @@ import { value, alt } from 'map-transform'
490
680
  const def10 = {
491
681
  id: 'data.customerNo',
492
682
  type: value('customer'),
493
- name: ['data.name', alt(value('Anonymous'))]
683
+ name: ['data.name', alt(value('Anonymous'))],
494
684
  }
495
685
  ```
496
686
 
497
687
  The operation will not set anything when mapping with `.onlyMappedValues()`.
498
688
 
689
+ If you want to define the `value` operation as an operation object, use
690
+ `$transform` or `$alt`:
691
+
692
+ ```javascript
693
+ const def10asObject = {
694
+ id: 'data.customerNo',
695
+ type: { $transform: 'value', value: 'customer' },
696
+ name: ['data.name', { $alt: 'value', value: 'Anonymous' }],
697
+ }
698
+ ```
699
+
499
700
  #### `fixed(data)` operation
500
701
 
501
702
  The data given to the fixed operation, will be inserted in the pipeline in place
@@ -507,15 +708,20 @@ will be included when mapping with `.onlyMappedValues()` as well.
507
708
 
508
709
  #### `alt(pipeline)` operation
509
710
 
510
- The alt operation will apply the function or pipeline it is given when the data
511
- already in the pipeline is `undefined`. This is how you provide default values
512
- in MapTransform. The pipeline may be as simple as a `value()` operation, a dot
711
+ The alt operation will apply the pipeline it is given when the data already in
712
+ the pipeline is `undefined`. This is how you provide default values in
713
+ MapTransform. The pipeline may be as simple as a `value()` operation, a dot
513
714
  notation path into the source data, or a full pipeline of several operations.
514
715
 
716
+ When given an array, the `alt` operation will treat it as a value and do
717
+ nothing, as it is not an `undefined` value. To provide the `alt` operation to
718
+ every item in the array, please use the `iterate` operation.
719
+
515
720
  ```javascript
516
- import { alt, transform, value } from 'map-transform'
517
- const currentDate = data => new Date()
518
- const formatDate = data => {
721
+ import { alt, transform, functions } from 'map-transform'
722
+ const { value } = functions
723
+ const currentDate = (data) => new Date()
724
+ const formatDate = (data) => {
519
725
  /* implementation not included */
520
726
  }
521
727
 
@@ -526,30 +732,38 @@ const def11 = {
526
732
  'data.updateDate',
527
733
  alt('data.createDate'),
528
734
  alt(transform(currentDate)),
529
- transform(formatDate)
530
- ]
735
+ transform(formatDate),
736
+ ],
531
737
  }
532
738
  ```
533
739
 
534
740
  In the example above, we first try to set the `updatedAt` prop to the data found
535
741
  at `data.updateDate` in the source data. If that does not exist (i.e. we get
536
742
  `undefined`), the alt operation kicks in and try the path `data.createDate`. If
537
- we still have `undefined`, the second alt will call a transform operation with
538
- the `currentDate` function, that simply returns the current date as a JS object.
539
- Finally, another transform operation pipes whatever data we get from all of this
540
- through the `formatDate` function.
743
+ we still have `undefined`, the second alt will call the customer function
744
+ `currentDate`, that simply returns the current date as a JS object. Finally,
745
+ another transform operation pipes whatever data we get from all of this through
746
+ the `formatDate` function.
541
747
 
542
748
  You may also define an alt operation as an object:
543
749
 
544
750
  ```javascript
545
751
  const def11asObject = {
546
752
  id: 'data.id',
547
- name: ['data.name', { $transform: 'alt', value: 'Anonymous' }]
753
+ name: ['data.name', { $alt: 'value', value: 'Anonymous' }],
754
+ updatedAt: [
755
+ 'data.updateDate',
756
+ { $alt: 'get', path: 'data.createDate' },
757
+ { $alt: 'currentDate' },
758
+ { $transform: 'formatDate' },
759
+ ],
548
760
  }
549
761
  ```
550
762
 
551
- For now, only the value operand is supported. In the example above, the value
552
- `'Anonymous'` will be used when `data.name` is undefined.
763
+ When you define the `alt` operation as an object, you may specify
764
+ `$iterate: true` on the object to provide a default value to every `undefined`
765
+ item on an array, if an array is encountered. You may also set
766
+ `$direction: 'fwd'` or `$direction: 'rev'` to limit it to one direction only.
553
767
 
554
768
  #### `concat(pipeline, pipeline, ...)` operation
555
769
 
@@ -561,6 +775,49 @@ This operation will always return an array, even when it is given only one
561
775
  pipeline that does not return an array. Pipelines that does not result in a
562
776
  value (i.e. return `undefined`) will be filtered away.
563
777
 
778
+ #### `merge(pipeline, pipeline, ...)` operation
779
+
780
+ `merge()` will run all given pipelines and deep merge their results. Conflicts
781
+ are resolved by prioritizing results from the rightmost of the conflicting
782
+ pipelines.
783
+
784
+ #### `modify(pipeline)` operation
785
+
786
+ Use the `modify()` operation when you want the pipeline to modify an object,
787
+ instead of replacing it.
788
+
789
+ Example:
790
+
791
+ ```javascript
792
+ import { modify } from 'map-transform'
793
+
794
+ const def34 = modify({
795
+ data: 'data.deeply.placed.items',
796
+ })
797
+ ```
798
+
799
+ `def34` will in effect set the values placed at a deep path on the `data`
800
+ prop. Giving this an object like:
801
+
802
+ ```javascript
803
+ const response = {
804
+ status: 'ok',
805
+ data: { deeply: { placed: { items: [{ id: 'ent1' }] } } },
806
+ }
807
+ ```
808
+
809
+ ...will result in:
810
+
811
+ ```javascript
812
+ const response = {
813
+ status: 'ok',
814
+ data: [{ id: 'ent1' }],
815
+ }
816
+ ```
817
+
818
+ Had we ran this without the `modify()` operation, the returned object would only
819
+ have the `data` prop.
820
+
564
821
  #### `fwd(pipeline)` and `rev(pipeline)` operation
565
822
 
566
823
  All operations in MapTransform will apply in both directions, although some of
@@ -573,11 +830,11 @@ pipeline when we're mapping in reverse.
573
830
 
574
831
  ```javascript
575
832
  import { fwd, rev, transform } from 'map-transform'
576
- const increment = data => data + 1
577
- const decrement = data => data - 1
833
+ const increment = (data) => data + 1
834
+ const decrement = (data) => data - 1
578
835
 
579
836
  const def12 = {
580
- order: ['index', fwd(transform(increment)), rev(transform(decrement))]
837
+ order: ['index', fwd(transform(increment)), rev(transform(decrement))],
581
838
  }
582
839
  ```
583
840
 
@@ -630,7 +887,7 @@ This example results in the exact same pipeline as the example above:
630
887
 
631
888
  ```javascript
632
889
  const def14 = {
633
- 'content[]': 'data.items[].content'
890
+ 'content[]': 'data.items[].content',
634
891
  }
635
892
  ```
636
893
 
@@ -659,13 +916,13 @@ const def15 = [
659
916
  {
660
917
  id: 'id',
661
918
  title: 'headline',
662
- section: root('meta.section')
663
- }
919
+ section: root('meta.section'),
920
+ },
664
921
  ]
665
922
 
666
923
  const data = {
667
924
  articles: [{ id: '1', headline: 'An article' } /* ... */],
668
- meta: { section: 'news' }
925
+ meta: { section: 'news' },
669
926
  }
670
927
 
671
928
  mapTransform(def15)(data)
@@ -680,14 +937,14 @@ As you see, every item in the `articles[]` array, will be mapped with the
680
937
  items without the root operation.
681
938
 
682
939
  There's also a shortcut notation for root, by prefixing a dot notation path with
683
- `$`. This only works when the path is used for getting a value, and it will be
940
+ `^`. This only works when the path is used for getting a value, and it will be
684
941
  plugged when used as set (i.e., it will return no value). This may be used in
685
942
  `get()` and `set()` operations, and in transformation objects.
686
943
 
687
944
  In the following example, `def16` and `def17` is exactly the same:
688
945
 
689
946
  ```javascript
690
- const def16 = get('$meta.section')
947
+ const def16 = get('^meta.section')
691
948
  const def17 = divide(root('meta.section'), plug())
692
949
  ```
693
950
 
@@ -727,8 +984,8 @@ const data = {
727
984
  users: [
728
985
  { id: 'user1', name: 'User 1' },
729
986
  { id: 'user2', name: 'User 2' },
730
- { id: 'user3', name: 'User 3' }
731
- ]
987
+ { id: 'user3', name: 'User 3' },
988
+ ],
732
989
  }
733
990
  const mapper = mapTransform(def18)
734
991
  const mappedData = mapper(data)
@@ -741,31 +998,193 @@ mapper.rev(mappedData)
741
998
  // --> { content: { meta: { authors: ['user1', 'user3'] } } }
742
999
  ```
743
1000
 
744
- #### `compare(path, value)` helper
1001
+ #### `compare({ path, operator, match, matchPath })` function
1002
+
1003
+ This is a helper function intended for use with the `filter()` operation. You
1004
+ pass a dot notation `path` and a `match` value (string, number, boolean) to
1005
+ `compare()`, and it returns a function that you can pass to `filter()` for
1006
+ filtering away data that does not not have the value set at the provided path.
1007
+
1008
+ As an alternative to `match`, you may specify a `matchPath`, which is a dot
1009
+ notation path, in which case the match value will be fetched from the provided
1010
+ data.
745
1011
 
746
- This is a helper intended for use with the `filter()` operation. You pass a dot
747
- notation path and a value (string, number, boolean) to `compare()`, and it
748
- returns a function that you can pass to `filter()` for filtering away data that
749
- does not not have the value set at the provided path. If the path points to an
750
- array, the value is expected to be one of the values in the array.
1012
+ The default is to compare the values resulting from `path` and `match` or
1013
+ `matchPath` with equality, but other operations may be set on the `operator`
1014
+ property. Alternatives: `'='`, `'!='`, `'>'`, `'>='`, `'<'`, or `'<='`.
1015
+
1016
+ If the path points to an array, the value is expected to be one of the values in
1017
+ the array.
751
1018
 
752
1019
  Here's an example where only data where role is set to 'admin' will be kept:
753
1020
 
754
1021
  ```javascript
755
- import { filter, compare } from 'map-transform'
1022
+ import { filter, functions } from 'map-transform'
1023
+ const { compare } = functions
756
1024
 
757
1025
  const def19 = [
758
1026
  {
759
1027
  name: 'name',
760
- role: 'editor'
1028
+ role: 'editor',
1029
+ },
1030
+ filter(compare({ path: 'role', operator: '=', match: 'admin' })),
1031
+ ]
1032
+ ```
1033
+
1034
+ You may also define this with a transform object:
1035
+
1036
+ ```javascript
1037
+ const def19o = [
1038
+ {
1039
+ name: 'name',
1040
+ role: 'editor',
761
1041
  },
762
- filter(compare('role', 'admin'))
1042
+ { $filter: 'compare', path: 'role', operator: '=', match: 'admin' },
763
1043
  ]
764
1044
  ```
765
1045
 
766
- #### `validate(path, schema)` helper
1046
+ #### `explode()` function
1047
+
1048
+ Given an object, the `explode` helper function will return an array with one
1049
+ object for each property in the source object, with a `key` property for the
1050
+ property key, and a `value` property for the value.
1051
+
1052
+ When given an array, the `explode` helper will return on object for every item
1053
+ in the array, with a `key` property set to the index number in the source array
1054
+ and a `value` property to the item value.
1055
+
1056
+ When transforming in reverse, `explode` will try to compile an object or an
1057
+ array from an array of key/value objects. If all `key` props are numbers, an
1058
+ array is produced, otherwise an object. Anything that don't match the expected
1059
+ structure will be skipped.
1060
+
1061
+ Example:
1062
+
1063
+ ```javascript
1064
+ import { mapTransform, transform, functions } from 'map-transform'
1065
+ const { explode } = functions
1066
+
1067
+ const data = {
1068
+ currencies: { NOK: 1, USD: 0.125, EUR: 0.1 },
1069
+ }
1070
+
1071
+ const def32 = ['currencies', transform(explode())]
1072
+
1073
+ mapTransform(def32)(data)
1074
+ // --> [{ key: 'NOK', value: 1 }, { key: 'USD', value: 0.125 },
1075
+ // { key: 'EUR', value: 0.1 }]
1076
+ ```
1077
+
1078
+ Or as a transform object:
1079
+
1080
+ ```javascript
1081
+ const def32o = ['currencies', { $transform: 'explode' }]
1082
+ ```
1083
+
1084
+ #### `map(dictionary)` function
1085
+
1086
+ This helper function accepts a dictionary described as an array of tuples, where
1087
+ each tuple holds a from value and a to value. When a data value is given to the
1088
+ `map` helper, it is replaced with a value from the dictionary. For a forward
1089
+ transformation, the first value in the tuple will be matched with the given
1090
+ data value, and the second value will be returned. In reverse transformation,
1091
+ the second value is matched and the first is returned.
1092
+
1093
+ The wildcard value `*` will match any value, and is applied if there is no other
1094
+ match in the dictionary. When the returned value is `*`, the original data value
1095
+ is returned instead.
1096
+
1097
+ The `map` function only supports primitive values, so any object will be mapped to
1098
+ `undefined` or the value given by the wildcard in the dictionary. Arrays will be
1099
+ iterated to map each value in the array.
1100
+
1101
+ Example:
1102
+
1103
+ ```
1104
+ import { transform, functions } from 'map-transform'
1105
+ const { map } = functions
1106
+
1107
+ const dictionary = [
1108
+ [200, 'ok'],
1109
+ [404, 'notfound'],
1110
+ ['*', 'error']
1111
+ ]
1112
+
1113
+ const def28 = {
1114
+ status: ['result', transform(map({ dictionary }))]
1115
+ }
1116
+ ```
767
1117
 
768
- This is a helper for validating the value at the path against a
1118
+ When using `map` in an operation object, you may provice a dictionary array
1119
+ or a named dictionary on the `dictionary` property. An example of with a named
1120
+ dictionary:
1121
+
1122
+ ```
1123
+ import { mapTransform } from 'map-transform'
1124
+
1125
+ const dictionary = [
1126
+ [200, 'ok'],
1127
+ [404, 'notfound'],
1128
+ ['*', 'error']
1129
+ ]
1130
+
1131
+ const def29 = {
1132
+ status: [
1133
+ 'result',
1134
+ { $transform: 'map', dictionary: 'statusCodes' }
1135
+ ]
1136
+ }
1137
+
1138
+ const mapper = mapTransform(def29, { dictionaries: { statusCodes: dictionary } })
1139
+ ```
1140
+
1141
+ #### `template(template)` function
1142
+
1143
+ The `template` helper function takes a [handlebars] template and applies the
1144
+ given data to it. The placeholders in the template is dot notaion paths to
1145
+ fields in the given data. A simple dot (`.`) refers to the data itself, and may
1146
+ be useful when the pipeline data is a string.
1147
+
1148
+ Values will be forced to strings before being inserted in the template.
1149
+
1150
+ Example:
1151
+
1152
+ ```javascript
1153
+ import { mapTransform, transform, functions } from 'map-transform'
1154
+ const { template } = functions
1155
+
1156
+ const data = {
1157
+ content: { description: 'Bergen by night', artist: 'John F.' },
1158
+ }
1159
+
1160
+ const def30 = {
1161
+ caption: ['content', transform(template('{{description}}. By {{artist}}'))],
1162
+ }
1163
+
1164
+ const ret = mapTransform(def30)(data)
1165
+ // --> { caption: 'Bergen by night. By John F.' }
1166
+ ```
1167
+
1168
+ The `template` function is also available through a transform object:
1169
+
1170
+ ```javascript
1171
+ const def30o = {
1172
+ caption: [
1173
+ 'content',
1174
+ { $transform: 'template', template: '{{description}}. By {{artist}}' },
1175
+ ],
1176
+ }
1177
+ ```
1178
+
1179
+ If you the template is available as part of the data, you may use `templatePath`
1180
+ instead of `template`, and set it to the path of the template in the data. When
1181
+ using the transform object format, this is as simple as it sounds. With the
1182
+ function version, you supply an operand object to `template()` like this:
1183
+ `transform(template({ templatePath: 'options.captionTemplate' }))`.
1184
+
1185
+ #### `validate(path, schema)` function
1186
+
1187
+ This is a helper function for validating the value at the path against a
769
1188
  [JSON Schema](http://json-schema.org). We won't go into details of JSON Schema
770
1189
  here, and the `validate()` helper simply retrieves the value at the path and
771
1190
  validates it according to the provided schema.
@@ -774,18 +1193,19 @@ Note that if you provide a schema that is always valid, it will be valid even
774
1193
  when the data has no value at the given path.
775
1194
 
776
1195
  ```javascript
777
- import { filter, validate } from 'map-transform'
1196
+ import { filter, functions } from 'map-transform'
1197
+ const { validate } = functions
778
1198
 
779
1199
  const def20 = [
780
1200
  'items',
781
1201
  filter(validate('draft', { const: false })),
782
1202
  {
783
- title: 'heading'
784
- }
1203
+ title: 'heading',
1204
+ },
785
1205
  ]
786
1206
  ```
787
1207
 
788
- #### `not(value)` helper
1208
+ #### `not(value)` function
789
1209
 
790
1210
  `not()` will return `false` when value if truthy and `true` when value is falsy.
791
1211
  This is useful for making the `filter()` operation do the opposite of what the
@@ -794,14 +1214,15 @@ filter function implies.
794
1214
  Here we filter away all data where role is set to 'admin':
795
1215
 
796
1216
  ```javascript
797
- import { filter, compare } from 'map-transform'
1217
+ import { filter, functions } from 'map-transform'
1218
+ const { compare } = functions
798
1219
 
799
1220
  const def21 = [
800
1221
  {
801
1222
  name: 'name',
802
- role: 'role'
1223
+ role: 'role',
803
1224
  },
804
- filter(not(compare('role', 'admin')))
1225
+ filter(not(compare('role', 'admin'))),
805
1226
  ]
806
1227
  ```
807
1228
 
@@ -828,14 +1249,14 @@ const def22 = [
828
1249
  'data.customers[]',
829
1250
  {
830
1251
  id: 'customerNo',
831
- name: ['fullname', alt(value('Anonymous'))]
832
- }
1252
+ name: ['fullname', alt(value('Anonymous'))],
1253
+ },
833
1254
  ]
834
1255
 
835
1256
  const dataInTargetState = [
836
1257
  { id: 'cust1', name: 'Fred Johnsen' },
837
- { id: 'cust2', name: 'Lucy Knight' },
838
- { id: 'cust3' }
1258
+ // { id: 'cust2', name: 'Lucy Knight' },
1259
+ { id: 'cust3' },
839
1260
  ]
840
1261
 
841
1262
  const dataInSourceState = mapTransform(def22).rev(dataInTargetState)
@@ -861,15 +1282,15 @@ For example:
861
1282
  ```javascript
862
1283
  import { mapTransform, transform } from 'map-transform'
863
1284
 
864
- const username = name => name.replace(/\s+/, '.').toLowerCase()
1285
+ const username = (name) => name.replace(/\s+/, '.').toLowerCase()
865
1286
 
866
1287
  const def23 = [
867
1288
  'data.customers[]',
868
1289
  {
869
1290
  id: 'customerNo',
870
1291
  name: 'fullname',
871
- 'name/1': ['username', rev(transform(username))]
872
- }
1292
+ 'name/1': ['username', rev(transform(username))],
1293
+ },
873
1294
  ]
874
1295
 
875
1296
  const dataInTargetState = [{ id: 'cust1', name: 'Fred Johnsen' }]
@@ -884,6 +1305,60 @@ const dataInSourceState = mapTransform(def23).rev(dataInTargetState)
884
1305
  // }
885
1306
  ```
886
1307
 
1308
+ In some cases, the reverse transform is more complex than the forward transform.
1309
+ For that reason, there is a `$flip` property that may be set to `true` on a
1310
+ transform object, to indicate that it is defined from the reverse perspective
1311
+ and should be flipped before running transformations.
1312
+
1313
+ A flipped transformation object will – in forward transformations – get with the
1314
+ properties on the object and set with the paths in the value. The order of paths
1315
+ and operations in a pipeline will also be reversed. Note that this will not
1316
+ affect any operations that behaves differently depending on direction, and they
1317
+ will run as if they were used on a non-flipped transformation object.
1318
+
1319
+ Also note that flipping will not affect the `get` and `set` operations, only
1320
+ path strings set directly as properties on the object or as part of the value
1321
+ (the pipeline). This might change in the future, so you should not use `get` and
1322
+ `set` directly on a flipped transformation object.
1323
+
1324
+ This flipped defintion:
1325
+
1326
+ ```javascript
1327
+ const def33flipped = {
1328
+ $flip: true,
1329
+ id: 'key',
1330
+ attributes: {
1331
+ title: ['headline', transform(threeLetters)],
1332
+ age: ['unknown'],
1333
+ },
1334
+ relationships: {
1335
+ author: value('johnf'),
1336
+ },
1337
+ }
1338
+ ```
1339
+
1340
+ ... is identical to:
1341
+
1342
+ ```javascript
1343
+ const def33 = {
1344
+ key: 'id',
1345
+ headline: ['attributes.title', transform(threeLetters)],
1346
+ unknown: ['attributes.age']
1347
+ },
1348
+ 'none/1': ['relationships.author': value('johnf')]
1349
+ }
1350
+ ```
1351
+
1352
+ The flipped definition is (in this case) easier to read.
1353
+
1354
+ Note also the `'none/1'` property in `def33`, that will stop this property from
1355
+ being set when going forward. This is not necessary on the flipped definition,
1356
+ but also results in a definition that will not work as expected going forward.
1357
+ This is a weakness in how MapTransform treats pipelines right now, and will
1358
+ probably be resolved in the future. For now, make sure to always have a path
1359
+ at the beginning of all pipelines if you plan to reverse transform – and the
1360
+ same goes for flipped transform objects if you want to forward transform.
1361
+
887
1362
  ### Mapping without fallbacks
888
1363
 
889
1364
  MapTransform will try its best to map the data it gets to the state you want,
@@ -905,7 +1380,7 @@ import { mapTransform, alt, value } from 'map-transform'
905
1380
 
906
1381
  const def24 = {
907
1382
  id: 'customerNo',
908
- name: ['fullname', alt(value('Anonymous'))]
1383
+ name: ['fullname', alt(value('Anonymous'))],
909
1384
  }
910
1385
 
911
1386
  const mapper = mapTransform(def24)