map-transform 0.3.12 → 0.4.0-alpha.11

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