-
-
Notifications
You must be signed in to change notification settings - Fork 137
/
denormalize.cljc
205 lines (187 loc) · 9.12 KB
/
denormalize.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
(ns com.fulcrologic.fulcro.algorithms.denormalize
"The algorithm and support functions for converting a normalized Fulcro database to a tree of denormalized props."
(:require
[edn-query-language.core :as eql]))
(def ^:dynamic *denormalize-time* 0)
(defn link-ref?
"Is the given `v` a link ref query (e.g. `[:table '_]) element."
[v]
(and
(vector? v)
(= 2 (count v))
(keyword? (first v))
(= '_ (second v))))
(defn lookup-ref?
"Is the given `v` a lookup ref query (i.e. ident)?"
[v]
(and (vector? v) (= 2 (count v)) (keyword? (first v))))
(defn follow-ref
"Returns the value defined by the `ref` from `state-map`. Works for link refs and
lookup refs."
[state-map [table id :as ref]]
(if (= '_ id)
(get state-map table)
(get-in state-map ref)))
(defn ref-key
"Returns the key to use in results for the given ref (ident of lookup ref). For link refs this is just
the first element, and for idents it is the ident."
[[table id :as ref]]
(if (= '_ id)
table
ref))
(declare denormalize)
(defn with-time
"Associates time metadata with the given props. This time can be used by rendering optimizations to decide when
stale props are passed to it from a parent in cases where props tunnelling was used for localized refresh."
[props t]
(vary-meta props assoc ::time t))
(defn- add-props!
"Walk the given AST children (which MUST be prop nodes), and add their values from `current-entity`
(if found)."
[transient-node entity ast-prop-children state-map]
(reduce
(fn [n {:keys [key]}]
(if (lookup-ref? key)
(if-let [x (follow-ref state-map key)]
(assoc! n (ref-key key) x)
n)
(if-let [entry (and (coll? entity) (find entity key))]
(assoc! n key (second entry))
n)))
transient-node
ast-prop-children))
(defn- reduce-depth
"Reduce the query depth on `join-node` that appears within the children of `parent-node`."
[parent-node join-node]
(let [join-node-index (reduce
(fn [idx n] (if (identical? join-node n)
(reduced idx)
(inc idx)))
0
(:children parent-node))]
(update-in parent-node [:children join-node-index :query] (fnil dec 1))))
(defn- add-join! [n {:keys [query key] :as join-node} entity state-map parent-node idents-seen]
(let [link-join? (lookup-ref? key)
v (if link-join? (follow-ref state-map key) (get entity key))
key (if (link-ref? key) (first key) key)
is-ref? (lookup-ref? v)
join-entity (if is-ref? (follow-ref state-map v) v)
to-many? (and (not is-ref?) (vector? join-entity))
depth-based? (int? query)
recursive? (or depth-based? (= '... query))
stop-recursion? (and recursive? (or (= 0 query)
(and is-ref?
;; NOTE: allows depth-based to ignore loops
(not depth-based?)
(contains? (get idents-seen key) v))))
parent-node (if (and depth-based? (not stop-recursion?))
(reduce-depth parent-node join-node)
parent-node)
target-node (if recursive? parent-node join-node)
;; NOTE: fixed bug with old db->tree, so behavior is different
idents-seen (if is-ref?
(update idents-seen key (fnil conj #{}) v)
idents-seen)]
(cond
stop-recursion? n
to-many? (assoc! n key
(into []
(keep (fn [x]
(let [e (if (lookup-ref? x)
(follow-ref state-map x)
x)]
(denormalize target-node e state-map idents-seen))))
join-entity))
(and recursive? join-entity) (if depth-based?
(let [join-node-index (reduce
(fn [idx n] (if (identical? join-node n)
(reduced idx)
(inc idx)))
0
(:children parent-node))
parent-node (update-in parent-node [:children join-node-index :query] (fnil dec 1))]
(assoc! n key (denormalize parent-node join-entity state-map idents-seen)))
(assoc! n key (denormalize parent-node join-entity state-map idents-seen)))
(map? join-entity) (assoc! n key (denormalize target-node join-entity state-map idents-seen))
(and (contains? entity key)
(not recursive?)
(not link-join?)) (assoc! n key v)
:otherwise n)))
(defn- add-union! [n {:keys [key] :as join-node} entity state-map idents-seen]
(let [link-join? (lookup-ref? key)
v (if link-join? key (get entity key))
union-node (-> join-node :children first)
union-key->query (reduce
(fn [result {:keys [union-key] :as node}]
(assoc result union-key node))
{}
(:children union-node))
is-ref? (lookup-ref? v)
to-many? (and (not is-ref?) (vector? v))]
(cond
to-many? (assoc! n key
(into []
(keep (fn [lookup-ref]
(if-let [e (and (lookup-ref? lookup-ref)
(follow-ref state-map lookup-ref))]
(let [[table] lookup-ref]
(if-let [target-ast-node (union-key->query table)]
(denormalize target-ast-node e state-map idents-seen)
{}))
{})))
v))
is-ref? (if-let [e (follow-ref state-map v)]
(if-let [target-ast-node (union-key->query (first v))]
(assoc! n key (denormalize target-ast-node e state-map idents-seen))
(assoc! n key {}))
n)
(and (contains? entity key)
(not link-join?)) (assoc! n key v)
:otherwise n)))
(defn- add-joins! [transient-node entity state-map parent-node ast-join-nodes idents-seen]
(reduce
(fn [n join-node]
(let [union? (map? (:query join-node))]
(if union?
(add-union! n join-node entity state-map idents-seen)
(add-join! n join-node entity state-map parent-node idents-seen))))
transient-node
ast-join-nodes))
(defn denormalize
"Internal implementation of `db->tree`. You should normally use `db->tree` instead of this function.
- `top-node`: an AST for the query.
- `current-entity`: The entity to start denormalization from.
- `state-map`: a normalized database.
- `idents-seen`: a map of the idents seen so far (for recursion loop tracking)."
[{:keys [type children] :as top-node} current-entity state-map idents-seen]
(assert (not= type :prop))
(let [current-entity (if (lookup-ref? current-entity)
(follow-ref state-map current-entity)
current-entity)
grouped-children (group-by :type children)
nil-nodes (get grouped-children nil false)
;; NOTE: wildcard works better than the old db->tree (which ignores wildcard when joins are present)
wildcard? (and nil-nodes (= '* (some-> nil-nodes first :key)))
result-node (add-props! (transient (if wildcard? current-entity {})) current-entity (:prop grouped-children) state-map)
result-node (add-joins! result-node current-entity state-map
top-node
(:join grouped-children)
idents-seen)]
(some-> result-node (persistent!) (with-time *denormalize-time*))))
(defn db->tree
"Pull a tree of data from a fulcro normalized database as a tree corresponding to the given query.
query - EQL.
starting-entity - A map of data or ident at which to start.
state-map - The overall normalized database from which idents can be resolved.
Returns a tree of data where each resolved data node is also marked with the current
*denormalize-time* (dynamically bound outside of this call). Users of this function that
are hydrating the UI should ensure that this time is bound to Fulcro's current internal
basis-time using `binding`."
[query starting-entity state-map]
(let [ast (eql/query->ast query)]
(some-> (denormalize ast starting-entity state-map {})
(with-time *denormalize-time*))))
(defn denormalization-time
"Gets the time at which the given props were processed by `db->tree`, if known."
[props]
(some-> props meta ::time))