/
context.clj
189 lines (169 loc) · 7.15 KB
/
context.clj
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
(ns phrag.context
"Context from DB schema data to construct Phrag's GraphQL."
(:require [camel-snake-kebab.core :as csk]
[clojure.string :as s]
[clojure.pprint :as pp]
[inflections.core :as inf]
[phrag.db.adapter :as db-adapter]
[phrag.table :as tbl]
[phrag.field :as fld]))
;;; Relation Context (field names & columns)
(defn- has-many-field
"Checks if a given table is a bridge table of cicular many-to-many or not,
and if it is, adds FK column name to the field key of nested object."
[table fk]
(let [tbl-name (:name table)
rscs (inf/plural tbl-name)
fk-from (:from fk)]
(if (tbl/circular-m2m-fk? table fk-from)
(str rscs "_on_" fk-from)
rscs)))
(defn- has-one-field
"If a FK column has `_id` naming, nested objects get field keys with trailing
`_id` removed. If not, FK destination is added to FK origin column.
Example: `user_id` => `user` / `created_by` => `created_by_user`"
[fk]
(let [from (:from fk)]
(if (s/ends-with? from "_id")
(s/replace from #"_id" "")
(str from "_" (inf/singular (:table fk))))))
(defn- nest-fk-map
"Fk map of both directions for resolving nested queries."
[rel-type table-key fk]
(-> (reduce-kv (fn [m k v] (assoc m k (keyword v))) {} fk)
(assoc :from-table table-key)
(assoc :type rel-type)))
(defn- assoc-has-one-maps
"assoc has-one on FK origin table"
[m table-key fks]
(reduce (fn [m fk]
(let [has-1-fld (keyword (has-one-field fk))]
(assoc-in m [:nest-fks table-key has-1-fld]
(nest-fk-map :has-one table-key fk))))
m
fks))
(defn- assoc-has-many-maps
"assoc has-many inverse relation on FK destination tables"
[m table-key table fks]
(reduce
(fn [m fk]
(let [has-many-key (keyword (has-many-field table fk))
has-many-aggr-key (keyword (str (name has-many-key) "_aggregate"))
to-tbl-key (keyword (:table fk))
n-fk (nest-fk-map :has-many table-key fk)
n-aggr-fk (nest-fk-map :has-many-aggr table-key fk)]
{:nest-fks (-> (:nest-fks m)
(assoc-in [to-tbl-key has-many-key] n-fk)
(assoc-in [to-tbl-key has-many-aggr-key] n-aggr-fk))}))
m
fks))
(defn- relation-ctx-per-table [table]
(let [fks (:fks table)
tbl-key (keyword (:name table))
has-one-mapped (assoc-has-one-maps {:nest-fks {tbl-key {}}} tbl-key fks)]
(assoc-has-many-maps has-one-mapped tbl-key table fks)))
(defn- relation-context [tables]
(reduce (fn [m table]
(let [rel-ctx (relation-ctx-per-table table)]
{:nest-fks (merge-with merge (:nest-fks m) (:nest-fks rel-ctx))}))
{:fields {} :columns {} :nest-fks {}}
tables))
;; FK Context
(defn- fk-field-keys [fk table to-table-name]
(let [has-many-fld (has-many-field table fk)
to-rsc-name (csk/->PascalCase (inf/singular to-table-name))]
{:to (keyword to-rsc-name)
:has-many (keyword has-many-fld)
:has-many-aggr (keyword (str has-many-fld "_aggregate"))
:has-one (keyword (has-one-field fk))}))
(defn- fk-context [table]
(let [fks (:fks table)
fk-map (zipmap (map #(keyword (:from %)) fks) fks)]
(reduce-kv (fn [m from-key fk]
(assoc m from-key
{:field-keys (fk-field-keys fk table (:table fk))}))
{} fk-map)))
;;; Signal functions
(defn- conj-items [v]
(reduce (fn [v fns]
(if (coll? fns)
(into v fns)
(conj v fns)))
[] v))
(defn- signal-per-type
"Signal functions per resource and operation."
[signal-map table-key op]
(let [all-tbl-fns (:all signal-map)
all-op-fns (get-in signal-map [table-key :all])
all-timing-fns (get-in signal-map [table-key op :all])
pre-fns (get-in signal-map [table-key op :pre])
post-fns (get-in signal-map [table-key op :post])]
{:pre (filter fn? (conj-items [all-tbl-fns all-op-fns
all-timing-fns pre-fns]))
:post (filter fn? (conj-items [all-tbl-fns all-op-fns
all-timing-fns post-fns]))}))
;;; Lacinia Schema Context from Table Data
(defn- table-context
"Compiles resource names, Lacinia fields and relationships from table data."
[tables signals]
(let [table-map (zipmap (map #(keyword (:name %)) tables) tables)]
(reduce-kv
(fn [m k table]
(let [table-name (:name table)
obj-keys (fld/lcn-obj-keys table-name)
pk-keys (tbl/pk-keys table)]
(assoc
m k
(-> m
(assoc :col-keys (tbl/col-key-set table))
(assoc :fks (fk-context table))
(assoc :pk-keys pk-keys)
(assoc :lcn-obj-keys obj-keys)
(assoc :lcn-qry-keys (fld/lcn-qry-keys table-name))
(assoc :lcn-mut-keys (fld/lcn-mut-keys table-name))
(assoc :lcn-descs (fld/lcn-descs table-name))
(assoc :lcn-fields (fld/lcn-fields table obj-keys pk-keys))
(assoc :signals {:query (signal-per-type signals k :query)
:create (signal-per-type signals k :create)
:delete (signal-per-type signals k :delete)
:update (signal-per-type signals k :update)})))))
{} table-map)))
(defn- view-context [views signals]
(let [view-map (zipmap (map #(keyword (:name %)) views) views)]
(reduce-kv
(fn [m k view]
(let [view-name (:name view)
obj-keys (fld/lcn-obj-keys view-name)]
(assoc m k
(-> m
(assoc :lcn-obj-keys obj-keys)
(assoc :lcn-qry-keys (fld/lcn-qry-keys view-name))
(assoc :lcn-descs (fld/lcn-descs view-name))
(assoc :lcn-fields (fld/lcn-fields view obj-keys nil))
(assoc :signals
{:query (signal-per-type signals k :query)})))))
{} view-map)))
(defn options->config
"Creates a config map from user-provided options."
[options]
(let [signals (:signals options)
config {:router (:router options)
:db (:db options)
:db-adapter (db-adapter/db->adapter (:db options))
:tables (:tables options)
:signal-ctx (:signal-ctx options)
:middleware (:middleware options)
:scan-tables (:scan-tables options true)
:scan-views (:scan-views options true)
:default-limit (:default-limit options)
:max-nest-level (:max-nest-level options)
:use-aggregation (:use-aggregation options true)}
db-scm (tbl/db-schema config)]
(-> config
(assoc :relation-ctx (relation-context (:tables db-scm)))
(assoc :tables (table-context (:tables db-scm) signals))
(assoc :views (view-context (:views db-scm) signals)))))
(def ^:no-doc init-schema {:enums fld/sort-op-enum
:input-objects fld/filter-input-objects
:objects fld/result-object
:queries {}})