src/CompoundDocument.ts   A
last analyzed

Complexity

Total Complexity 6
Complexity/F 0

Size

Lines of Code 122
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 6
eloc 79
mnd 6
bc 6
fnc 0
dl 0
loc 122
ccs 32
cts 32
cp 1
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
rs 10
1 1
import {array} from 'fp-ts/lib/Array';
2 1
import {identity} from 'fp-ts/lib/function';
3 1
import {pipe, pipeable} from 'fp-ts/lib/pipeable';
4 1
import {getMonad, listen, pass, Writer} from 'fp-ts/lib/Writer';
5 1
import * as t from 'io-ts';
6 1
import {ArrayC} from './io/ArrayC';
7 1
import {EntityC} from './io/EntityC';
8 1
import {NonEmptyArrayC} from './io/NonEmptyArrayC';
9 1
import {ResourceIdentifierC} from './io/ResourceIdentifierC';
10 1
import {JsonApiData} from './JsonApiData';
11 1
import {RelationshipsCache} from './RelationshipsCache';
12
import {RelationshipsRecord} from './RelationshipsRecord';
13
import {UnknownRecord} from './UnknownRecord';
14
15
export type CompoundDocument<A> = Writer<RelationshipsCache | RelationshipsRecord, A>
16
17 1
const m = getMonad(RelationshipsCache.monoid.self);
18 1
const M = pipeable(m);
19
20 1
const fromUnknown = (u: unknown): CompoundDocument<unknown> =>
21 43
  m.of(u);
22
23 1
const fromArray = (u: Array<unknown>): CompoundDocument<Array<unknown>> =>
24 3
  array.traverse(m)(u, fromJson);
25
26 1
const fromRecord = (u: UnknownRecord): CompoundDocument<UnknownRecord> =>
27 18
  Object.keys(u)
28
    .reduce(
29
      (writer: CompoundDocument<UnknownRecord>, key: string): CompoundDocument<UnknownRecord> =>
30 31
        pipe(
31
          writer,
32
          M.chain(
33
            // The accumulator (bag of relationships) has to be modified depending on returned data (the actual JSON).
34 31
            attributes => pass(
35
              pipe(
36
                fromJson(u[key]),
37
                M.map(
38 31
                  data => !t.UnknownRecord.is(data) && !ArrayC<unknown>().is(data)
39
                    /**
40
                     * No transformation needed with a scalar, just map the value in the result (as an [attribute][1]).
41
                     *
42
                     * [1]: https://jsonapi.org/format/#document-resource-object-attributes
43
                     */
44
                    ? [
45
                      {...attributes, [key]: data},
46
                      identity
47
                    ]
48
                    // Beware: *non-empty* array.
49
                    : ResourceIdentifierC.is(data) || NonEmptyArrayC(ResourceIdentifierC).is(data)
50
                      /**
51
                       * Child resources must be added to the bag of [relationships][1] (the accumulator). Leave the
52
                       * attributes alone.
53
                       *
54
                       * [1]: https://jsonapi.org/format/#document-resource-object-relationships
55
                       */
56
                      ? [
57
                        attributes,
58 7
                        relationships => RelationshipsCache.monoid.self
59
                          .concat(relationships, {[key]: data})
60
                      ]
61
                      /**
62
                       * A nested non-resource object must be added to the attributes just like a scalar, while current
63
                       * relationships have to mirror the nesting.
64
                       */
65
                      : [
66
                        {...attributes, [key]: data},
67 4
                        relationships => RelationshipsCache.nestLocal(relationships, key)
68
                      ]
69
                )
70
              )
71
            )
72
          )
73
        ),
74
      fromUnknown({}) as CompoundDocument<UnknownRecord>
75
    );
76
77 66
const fromJson = (u: unknown, primaryData: boolean = false): CompoundDocument<unknown> =>
78 33
  !t.UnknownRecord.is(u) && !ArrayC<unknown>().is(u)
79
    ? fromUnknown(u)
80
    : pass( // pass() allows both Writer elements to be modified at once.
81
    pipe(
82
      listen( // Expose Writer accumulator.
83
        (
84
          ArrayC<unknown>().is(u)
85
            ? fromArray(u)
86
            : fromRecord(u)
87
        ) as CompoundDocument<unknown[] | UnknownRecord>
88
      ),
89
      M.map(
90 13
        ([data, relationships]) => {
91 13
          const cache = RelationshipsCache.fromRelationships(relationships);
92 13
          const locals = RelationshipsCache.lens.local.get(cache);
93
94
          /**
95
           * If resulting data is an entity - or if we're parsing [primary data][1] -, convert it to JSON:API format and
96
           * flush local relationships.
97
           * Otherwise, just forward everything to the upper level.
98
           *
99
           * [1]: https://jsonapi.org/format/#document-top-level
100
           */
101 13
          return primaryData &&
102
          !NonEmptyArrayC(ResourceIdentifierC).is(data) || // Prevent repeating the conversion.
103
          EntityC.is(data)
104
            ? [
105
              ArrayC<UnknownRecord>().is(data)
106 2
                ? data.map(record => JsonApiData.fromJson(record, locals))
107
                : JsonApiData.fromJson(data, locals),
108
              RelationshipsCache.emptyLocal
109
            ]
110
            : [data, identity];
111
        }
112
      )
113
    )
114
    );
115
116 1
export const CompoundDocument = {
117
  fromArray: fromArray,
118
  fromJson: fromJson,
119
  fromRecord: fromRecord,
120
  fromUnknown: fromUnknown
121
};
122