/
many.go
295 lines (263 loc) · 7.62 KB
/
many.go
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package argo
import (
"fmt"
sql "github.com/aodin/aspect"
)
// ManyElem is the internal representation of an included Many resource.
// The included table must be connected by a Foreign key to the parent
// resource table.
type ManyElem struct {
name string // name where values will be added to parent table
fk sql.ForeignKeyElem
table *sql.TableElem
resource *ResourceSQL
selects Columns
showFK bool // By default, foreign key fields will be dropped
detailOnly bool
asMap *struct {
Key string
Value string
}
}
// AsMap converts the list of many elements into a map of the given key: value.
func (elem ManyElem) AsMap(key, value string) ManyElem {
// TODO How to guarantee that the key is unique per result?
// Both key and value must be selectable columns
if !elem.selects.Has(key) {
panic(fmt.Sprintf(
"argo: the column %s is not a valid key - it either does not exist or has been excluded",
key,
))
}
if !elem.selects.Has(value) {
panic(fmt.Sprintf(
"argo: the column %s is not a valid value - it either does not exist or has been excluded",
value,
))
}
// Save the new mapping
elem.asMap = &struct {
Key string
Value string
}{
Key: key,
Value: value,
}
return elem
}
// DetailOnly will attach the ManyElem to only the detail views of the API.
func (elem ManyElem) DetailOnly() ManyElem {
elem.detailOnly = true
return elem
}
// Exclude removes the given fields by name from the included ManyElem.
func (elem ManyElem) Exclude(names ...string) ManyElem {
// TODO The foreign key column cannot be excluded! it is needed for
// matching - it can be dropped automatically later
for _, name := range names {
if _, ok := elem.table.C[name]; !ok {
panic(fmt.Sprintf(
"argo: cannot exclude %s, table %s does not have column with this name",
name,
elem.table.Name,
))
}
// Remove the column from the list of selected columns
if err := elem.selects.Remove(name); err != nil {
panic(fmt.Sprintf(
"argo: the column %s cannot be excluded - it either does not exist or has already been excluded",
name,
))
}
}
return elem
}
// Modify implements the Modifier resource that allows an element to
// modify a resource. It will add the ManyElem to the list of included
// elements for the given resource.
func (elem ManyElem) Modify(resource *ResourceSQL) error {
if resource.table == nil {
return fmt.Errorf("argo: Many statements can only modify resources with an existing table")
}
// Search the foreign keys of the included element to find a
// foreign key that matches the resource table
// TODO It doesn't need to be only foreign keys
for _, fk := range elem.table.ForeignKeys() {
if fk.ReferencesTable() == resource.table {
elem.fk = fk
break
}
}
if elem.fk.Name() == "" {
return fmt.Errorf(
"argo: could not match the many field '%s' to any foreign key column in '%s'",
elem.name,
resource.Name,
)
}
// The include name can't also be taken
// TODO set a field to prevent multiple includes at the same name
if _, exists := resource.table.C[elem.name]; exists {
return fmt.Errorf(
"argo: the parent table already has a field named %s",
elem.name,
)
}
// Set the resource of the include
elem.resource = resource
// TODO Create a common field struct, with validation / create?
// Add the included table to the requested methods
// TODO specify the HTTP methods were the include should be active
resource.detailIncludes = append(resource.detailIncludes, elem)
if !elem.detailOnly {
resource.listIncludes = append(resource.listIncludes, elem)
}
return nil
}
// Query is the database query method used for single result detail methods.
func (elem ManyElem) Query(conn sql.Connection, values sql.Values) error {
// TODO panic or errors
// TODO Query by a value that doesn't exist in values?
fkValue, ok := values[elem.fk.ForeignName()]
if !ok {
panic(fmt.Sprintf(
"argo: cannot query an included table by a values key '%s' - it does not exist in the given values map",
elem.fk.ForeignName(),
))
}
stmt := sql.Select(
elem.selects,
).Where(
elem.table.C[elem.fk.Name()].Equals(fkValue),
)
results := make([]sql.Values, 0)
if err := conn.QueryAll(stmt, &results); err != nil {
panic(fmt.Sprintf(
"argo: error while querying included many for key '%d' (%s): %s",
fkValue,
stmt,
err,
))
}
if !elem.showFK {
// TODO multiple fks
for _, result := range results {
delete(result, elem.fk.Name())
}
}
FixValues(results...)
if elem.asMap == nil {
values[elem.name] = results
return nil
}
// Convert to a map
values[elem.name] = valuesToMap(results, elem.asMap.Key, elem.asMap.Value)
return nil
}
// TODO if not unique allow mapping as map[string][]interface{}
func valuesToMap(results []sql.Values, k, v string) map[string]interface{} {
mapping := make(map[string]interface{})
for _, result := range results {
// All key results must be of type string
keyValue, ok := result[k].(string)
if !ok {
panic(fmt.Errorf(
"argo: cannot create mapping using key '%s' - it is non-string type %T",
k,
keyValue,
))
}
// TODO error for non-unique?
mapping[keyValue] = result[v]
}
return mapping
}
// QueryAll is the database query method used for building a many
// relationship with many tables.
func (elem ManyElem) QueryAll(c sql.Connection, values []sql.Values) error {
// Get all foreign name values
fkValues := make([]interface{}, 0)
// TODO panic or errors
// TODO Query by a value that doesn't exist in values?
for _, value := range values {
fkValue, ok := value[elem.fk.ForeignName()]
if !ok {
panic(fmt.Sprintf(
"argo: cannot query an included table by a values key '%s' - it does not exist in the given values map",
elem.fk.ForeignName(),
))
}
fkValues = append(fkValues, fkValue)
}
// If there are no values to query, stop here
if len(fkValues) == 0 {
return nil
}
// The included fk field must be selected even if it is removed
// later - it is needed to match resources
// TODO custom order bys
// TODO conditional query toggles
// TODO composite primary keys
stmt := sql.Select(
elem.selects,
).Where(
elem.table.C[elem.fk.Name()].In(fkValues),
).OrderBy(elem.table.C[elem.table.PrimaryKey()[0]])
results := make([]sql.Values, 0)
if err := c.QueryAll(stmt, &results); err != nil {
panic(fmt.Sprintf(
"argo: error in query all for many with keys '%v' (%s): %s",
fkValues, // TODO pretty print value array?
stmt,
err,
))
}
FixValues(results...)
// Separate them by fk value
byFkValue := make(map[interface{}][]sql.Values)
for _, result := range results {
key := result[elem.fk.Name()]
if !elem.showFK {
// TODO multiple fks
delete(result, elem.fk.Name())
}
byFkValue[key] = append(byFkValue[key], result)
}
// Add them back into the original values array
// TODO as map
for _, value := range values {
fkValues, ok := byFkValue[value[elem.fk.ForeignName()]]
if ok {
if elem.asMap == nil {
value[elem.name] = fkValues
} else {
value[elem.name] = valuesToMap(
fkValues,
elem.asMap.Key,
elem.asMap.Value,
)
}
} else {
if elem.asMap == nil {
value[elem.name] = make([]sql.Values, 0) // JSON output as []
} else {
value[elem.name] = sql.Values{}
}
}
}
return nil
}
// Many creates a new Many respresentation of the given table at the given name
func Many(name string, table *sql.TableElem) ManyElem {
if table == nil {
panic("argo: tables in many statements cannot be nil")
}
if err := validateFieldName(name); err != nil {
panic(err.Error())
}
return ManyElem{
name: name,
table: table,
selects: ColumnSet(table.Columns()...),
}
}