elm-ssr 0.91.6 → 0.98.0

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.
@@ -0,0 +1,315 @@
1
+ module ElmSsr.Db.Elmto.Changeset exposing
2
+ ( Changeset
3
+ , cast, castRecord
4
+ , validateRequired
5
+ , validateFormat
6
+ , validateLength
7
+ , validateNumber
8
+ , applyChanges
9
+ , errors, isValid, changes
10
+ )
11
+
12
+ import Dict exposing (Dict)
13
+ import ElmSsr.Db.Elmto as Elmto exposing (Schema, Field)
14
+ import Json.Decode as Decode
15
+ import Json.Encode as Encode
16
+
17
+
18
+ type alias Changeset record =
19
+ { data : Maybe record
20
+ , changes : Dict String Encode.Value
21
+ , errors : List (String, String)
22
+ , isValid : Bool
23
+ }
24
+
25
+
26
+ cast : Schema record -> Dict String Decode.Value -> Changeset record
27
+ cast schema schemaAttrs =
28
+ let
29
+ schemaFields =
30
+ Elmto.fields schema
31
+
32
+ processField : Field record -> ( Dict String Encode.Value, List (String, String) ) -> ( Dict String Encode.Value, List (String, String) )
33
+ processField f ( accChanges, accErrors ) =
34
+ case Dict.get f.name schemaAttrs of
35
+ Just rawVal ->
36
+ case Decode.decodeValue f.decoder rawVal of
37
+ Ok encodedVal ->
38
+ ( Dict.insert f.name encodedVal accChanges, accErrors )
39
+
40
+ Err err ->
41
+ ( accChanges, ( f.name, "is invalid: " ++ Decode.errorToString err ) :: accErrors )
42
+
43
+ Nothing ->
44
+ ( accChanges, accErrors )
45
+
46
+ ( finalChanges, finalErrors ) =
47
+ List.foldl processField ( Dict.empty, [] ) schemaFields
48
+ in
49
+ { data = Nothing
50
+ , changes = finalChanges
51
+ , errors = finalErrors
52
+ , isValid = List.isEmpty finalErrors
53
+ }
54
+
55
+
56
+ castRecord : Schema record -> record -> Dict String Decode.Value -> Changeset record
57
+ castRecord schema record schemaAttrs =
58
+ let
59
+ initial =
60
+ cast schema schemaAttrs
61
+ in
62
+ { initial | data = Just record }
63
+
64
+
65
+ validateRequired : List String -> Changeset record -> Changeset record
66
+ validateRequired requiredFields changeset =
67
+ let
68
+ checkField : String -> List (String, String) -> List (String, String)
69
+ checkField fieldName acc =
70
+ let
71
+ changeVal =
72
+ Dict.get fieldName changeset.changes
73
+
74
+ isBlank =
75
+ case changeVal of
76
+ Just val ->
77
+ val == Encode.null
78
+
79
+ Nothing ->
80
+ case changeset.data of
81
+ Just _ ->
82
+ False
83
+
84
+ Nothing ->
85
+ True
86
+ in
87
+ if isBlank then
88
+ ( fieldName, "can't be blank" ) :: acc
89
+
90
+ else
91
+ acc
92
+
93
+ newErrors =
94
+ List.foldl checkField changeset.errors requiredFields
95
+ in
96
+ { changeset
97
+ | errors = newErrors
98
+ , isValid = List.isEmpty newErrors
99
+ }
100
+
101
+
102
+ validateFormat : String -> (String -> Bool) -> String -> Changeset record -> Changeset record
103
+ validateFormat fieldName formatFn msg changeset =
104
+ let
105
+ val =
106
+ Dict.get fieldName changeset.changes
107
+
108
+ isInvalid =
109
+ case val of
110
+ Just encodedVal ->
111
+ case Decode.decodeValue Decode.string encodedVal of
112
+ Ok str ->
113
+ not (formatFn str)
114
+
115
+ Err _ ->
116
+ False
117
+
118
+ Nothing ->
119
+ False
120
+ in
121
+ if isInvalid then
122
+ let
123
+ newErrors =
124
+ ( fieldName, msg ) :: changeset.errors
125
+ in
126
+ { changeset | errors = newErrors, isValid = False }
127
+
128
+ else
129
+ changeset
130
+
131
+
132
+ validateLength : String -> { min : Maybe Int, max : Maybe Int } -> Changeset record -> Changeset record
133
+ validateLength fieldName opts changeset =
134
+ let
135
+ val =
136
+ Dict.get fieldName changeset.changes
137
+
138
+ isInvalid =
139
+ case val of
140
+ Just encodedVal ->
141
+ case Decode.decodeValue Decode.string encodedVal of
142
+ Ok str ->
143
+ let
144
+ len =
145
+ String.length str
146
+
147
+ tooShort =
148
+ case opts.min of
149
+ Just m ->
150
+ len < m
151
+
152
+ Nothing ->
153
+ False
154
+
155
+ tooLong =
156
+ case opts.max of
157
+ Just m ->
158
+ len > m
159
+
160
+ Nothing ->
161
+ False
162
+ in
163
+ tooShort || tooLong
164
+
165
+ Err _ ->
166
+ False
167
+
168
+ Nothing ->
169
+ False
170
+ in
171
+ if isInvalid then
172
+ let
173
+ msg =
174
+ case ( opts.min, opts.max ) of
175
+ ( Just minVal, Just maxVal ) ->
176
+ "should be between " ++ String.fromInt minVal ++ " and " ++ String.fromInt maxVal ++ " characters"
177
+
178
+ ( Just minVal, Nothing ) ->
179
+ "should be at least " ++ String.fromInt minVal ++ " characters"
180
+
181
+ ( Nothing, Just maxVal ) ->
182
+ "should be at most " ++ String.fromInt maxVal ++ " characters"
183
+
184
+ _ ->
185
+ "invalid length"
186
+
187
+ newErrors =
188
+ ( fieldName, msg ) :: changeset.errors
189
+ in
190
+ { changeset | errors = newErrors, isValid = False }
191
+
192
+ else
193
+ changeset
194
+
195
+
196
+ validateNumber : String -> { greaterThan : Maybe Float, lessThan : Maybe Float } -> Changeset record -> Changeset record
197
+ validateNumber fieldName opts changeset =
198
+ let
199
+ val =
200
+ Dict.get fieldName changeset.changes
201
+
202
+ isInvalid =
203
+ case val of
204
+ Just encodedVal ->
205
+ case Decode.decodeValue Decode.float encodedVal of
206
+ Ok num ->
207
+ let
208
+ tooSmall =
209
+ case opts.greaterThan of
210
+ Just gtVal ->
211
+ num <= gtVal
212
+
213
+ Nothing ->
214
+ False
215
+
216
+ tooLarge =
217
+ case opts.lessThan of
218
+ Just ltVal ->
219
+ num >= ltVal
220
+
221
+ Nothing ->
222
+ False
223
+ in
224
+ tooSmall || tooLarge
225
+
226
+ Err _ ->
227
+ False
228
+
229
+ Nothing ->
230
+ False
231
+ in
232
+ if isInvalid then
233
+ let
234
+ msg =
235
+ case ( opts.greaterThan, opts.lessThan ) of
236
+ ( Just gtVal, Just ltVal ) ->
237
+ "must be greater than " ++ String.fromFloat gtVal ++ " and less than " ++ String.fromFloat ltVal
238
+
239
+ ( Just gtVal, Nothing ) ->
240
+ "must be greater than " ++ String.fromFloat gtVal
241
+
242
+ ( Nothing, Just ltVal ) ->
243
+ "must be less than " ++ String.fromFloat ltVal
244
+
245
+ _ ->
246
+ "invalid number"
247
+
248
+ newErrors =
249
+ ( fieldName, msg ) :: changeset.errors
250
+ in
251
+ { changeset | errors = newErrors, isValid = False }
252
+
253
+ else
254
+ changeset
255
+
256
+
257
+ applyChanges : Schema record -> Changeset record -> Result (List (String, String)) record
258
+ applyChanges schema schemaChangeset =
259
+ if not schemaChangeset.isValid then
260
+ Err schemaChangeset.errors
261
+
262
+ else
263
+ let
264
+ originalJson =
265
+ case schemaChangeset.data of
266
+ Just rec ->
267
+ let
268
+ schemaFields =
269
+ Elmto.fields schema
270
+
271
+ encField f acc =
272
+ ( f.name, f.encoder rec ) :: acc
273
+ in
274
+ List.foldl encField [] schemaFields
275
+ |> Encode.object
276
+
277
+ Nothing ->
278
+ Encode.object []
279
+
280
+ mergedJson =
281
+ case Decode.decodeValue (Decode.dict Decode.value) originalJson of
282
+ Ok dict ->
283
+ let
284
+ mergedDict =
285
+ Dict.foldl (\k v acc -> Dict.insert k v acc) dict schemaChangeset.changes
286
+ in
287
+ Encode.dict identity (\v -> v) mergedDict
288
+
289
+ Err _ ->
290
+ originalJson
291
+
292
+ decoded =
293
+ Decode.decodeValue (Elmto.decoder schema) mergedJson
294
+ in
295
+ case decoded of
296
+ Ok rec ->
297
+ Ok rec
298
+
299
+ Err err ->
300
+ Err [ ( "changeset", "Failed to apply changes: " ++ Decode.errorToString err ) ]
301
+
302
+
303
+ errors : Changeset record -> List (String, String)
304
+ errors changeset =
305
+ changeset.errors
306
+
307
+
308
+ isValid : Changeset record -> Bool
309
+ isValid changeset =
310
+ changeset.isValid
311
+
312
+
313
+ changes : Changeset record -> Dict String Encode.Value
314
+ changes changeset =
315
+ changeset.changes
@@ -0,0 +1,241 @@
1
+ module ElmSsr.Db.Elmto.Compiler exposing
2
+ ( Dialect(..)
3
+ , compileSelect
4
+ , compileInsert
5
+ , compileUpdate
6
+ , compileUpdateAll
7
+ , compileDelete
8
+ , compileDeleteAll
9
+ , toDialectSql
10
+ )
11
+
12
+ import Dict
13
+ import ElmSsr.Db.Elmto as Elmto exposing (Schema, Field)
14
+ import ElmSsr.Db.Elmto.Changeset as Changeset exposing (Changeset)
15
+ import ElmSsr.Db.Elmto.Query as Query exposing (Query)
16
+ import Json.Decode as Decode
17
+ import Json.Encode as Encode
18
+
19
+
20
+ type Dialect
21
+ = PostgreSQL
22
+ | SQLite
23
+
24
+
25
+ toDialectSql : Dialect -> String -> String
26
+ toDialectSql dialect baseSql =
27
+ case dialect of
28
+ SQLite ->
29
+ baseSql
30
+
31
+ PostgreSQL ->
32
+ let
33
+ parts =
34
+ String.split "?" baseSql
35
+
36
+ joinParts index list acc =
37
+ case list of
38
+ [] ->
39
+ acc
40
+
41
+ x :: xs ->
42
+ if index == 0 then
43
+ joinParts 1 xs x
44
+
45
+ else
46
+ joinParts (index + 1) xs (acc ++ "$" ++ String.fromInt index ++ x)
47
+ in
48
+ joinParts 0 parts ""
49
+
50
+
51
+ compileSelect : Dialect -> Query record selection -> { sql : String, params : List Encode.Value, decoder : Decode.Decoder selection }
52
+ compileSelect dialect query =
53
+ let
54
+ castPostgresNumbers =
55
+ case dialect of
56
+ PostgreSQL ->
57
+ True
58
+
59
+ SQLite ->
60
+ False
61
+
62
+ compiled =
63
+ Query.rawSelect castPostgresNumbers query
64
+
65
+ q =
66
+ Query.toParts query
67
+ in
68
+ { sql = toDialectSql dialect compiled.sql
69
+ , params = compiled.params
70
+ , decoder = q.decoder
71
+ }
72
+
73
+
74
+ compileInsert : Dialect -> Schema record -> Changeset record -> Result (List (String, String)) { sql : String, params : List Encode.Value }
75
+ compileInsert dialect schema changeset =
76
+ if not (Changeset.isValid changeset) then
77
+ Err (Changeset.errors changeset)
78
+
79
+ else
80
+ let
81
+ changeList =
82
+ Dict.toList (Changeset.changes changeset)
83
+ in
84
+ if List.isEmpty changeList then
85
+ case dialect of
86
+ SQLite ->
87
+ Ok
88
+ { sql = "INSERT INTO " ++ Elmto.tableName schema ++ " DEFAULT VALUES"
89
+ , params = []
90
+ }
91
+
92
+ PostgreSQL ->
93
+ Ok
94
+ { sql = "INSERT INTO " ++ Elmto.tableName schema ++ " DEFAULT VALUES RETURNING *"
95
+ , params = []
96
+ }
97
+
98
+ else
99
+ let
100
+ cols =
101
+ List.map Tuple.first changeList
102
+
103
+ vals =
104
+ List.map Tuple.second changeList
105
+
106
+ placeholders =
107
+ List.map (\_ -> "?") cols
108
+ |> String.join ", "
109
+
110
+ columnsStr =
111
+ String.join ", " cols
112
+
113
+ returningStr =
114
+ case dialect of
115
+ PostgreSQL ->
116
+ " RETURNING *"
117
+
118
+ SQLite ->
119
+ ""
120
+
121
+ rawSql =
122
+ "INSERT INTO " ++ Elmto.tableName schema ++ " (" ++ columnsStr ++ ") VALUES (" ++ placeholders ++ ")" ++ returningStr
123
+ in
124
+ Ok
125
+ { sql = toDialectSql dialect rawSql
126
+ , params = vals
127
+ }
128
+
129
+
130
+ compileUpdate : Dialect -> Schema record -> Changeset record -> Result (List (String, String)) { sql : String, params : List Encode.Value }
131
+ compileUpdate dialect schema changeset =
132
+ if not (Changeset.isValid changeset) then
133
+ Err (Changeset.errors changeset)
134
+
135
+ else
136
+ case changeset.data of
137
+ Nothing ->
138
+ Err [ ( "changeset", "cannot update a changeset without a loaded record" ) ]
139
+
140
+ Just record ->
141
+ let
142
+ idField =
143
+ List.filter (\f -> f.name == "id") (Elmto.fields schema)
144
+ |> List.head
145
+
146
+ idVal =
147
+ case idField of
148
+ Just f ->
149
+ f.encoder record
150
+
151
+ Nothing ->
152
+ Encode.null
153
+ in
154
+ if idVal == Encode.null then
155
+ Err [ ( "changeset", "record has no id field for updates" ) ]
156
+
157
+ else
158
+ let
159
+ changeList =
160
+ Dict.toList (Changeset.changes changeset)
161
+ in
162
+ if List.isEmpty changeList then
163
+ Err [ ( "changeset", "no changes to update" ) ]
164
+
165
+ else
166
+ let
167
+ setClauses =
168
+ List.map (\( col, _ ) -> col ++ " = ?") changeList
169
+ |> String.join ", "
170
+
171
+ params =
172
+ List.map Tuple.second changeList ++ [ idVal ]
173
+
174
+ returningStr =
175
+ case dialect of
176
+ PostgreSQL ->
177
+ " RETURNING *"
178
+
179
+ SQLite ->
180
+ ""
181
+
182
+ rawSql =
183
+ "UPDATE " ++ Elmto.tableName schema ++ " SET " ++ setClauses ++ " WHERE id = ?" ++ returningStr
184
+ in
185
+ Ok
186
+ { sql = toDialectSql dialect rawSql
187
+ , params = params
188
+ }
189
+
190
+
191
+ compileUpdateAll : Dialect -> Schema record -> Query.Expression record -> Changeset record -> Result (List (String, String)) { sql : String, params : List Encode.Value }
192
+ compileUpdateAll dialect schema expr changeset =
193
+ if not (Changeset.isValid changeset) then
194
+ Err (Changeset.errors changeset)
195
+
196
+ else
197
+ let
198
+ changeList =
199
+ Dict.toList (Changeset.changes changeset)
200
+ in
201
+ if List.isEmpty changeList then
202
+ Err [ ( "changeset", "no changes to update" ) ]
203
+
204
+ else
205
+ let
206
+ setClauses =
207
+ List.map (\( col, _ ) -> col ++ " = ?") changeList
208
+ |> String.join ", "
209
+
210
+ whereSql =
211
+ Query.expressionSqlFrom (Elmto.tableName schema) identity expr
212
+
213
+ rawSql =
214
+ "UPDATE " ++ Elmto.tableName schema ++ " SET " ++ setClauses ++ " WHERE " ++ whereSql
215
+ in
216
+ Ok
217
+ { sql = toDialectSql dialect rawSql
218
+ , params = List.map Tuple.second changeList ++ Query.expressionParams expr
219
+ }
220
+
221
+
222
+ compileDelete : Dialect -> Schema record -> Encode.Value -> { sql : String, params : List Encode.Value }
223
+ compileDelete dialect schema idVal =
224
+ let
225
+ rawSql =
226
+ "DELETE FROM " ++ Elmto.tableName schema ++ " WHERE id = ?"
227
+ in
228
+ { sql = toDialectSql dialect rawSql
229
+ , params = [ idVal ]
230
+ }
231
+
232
+
233
+ compileDeleteAll : Dialect -> Schema record -> Query.Expression record -> { sql : String, params : List Encode.Value }
234
+ compileDeleteAll dialect schema expr =
235
+ let
236
+ rawSql =
237
+ "DELETE FROM " ++ Elmto.tableName schema ++ " WHERE " ++ Query.expressionSqlFrom (Elmto.tableName schema) identity expr
238
+ in
239
+ { sql = toDialectSql dialect rawSql
240
+ , params = Query.expressionParams expr
241
+ }