Elm: Iterate list performing multiple HTTP requests - http

I'm wondering if I can get some help with iterating a list of groups, making a POST request for each group to create a 'room', iterating the users for each group and making a POST request to assign them to this specific room.
I have the following model.
model = {
groups = [
{
title = "Foo"
, users = [
{ name = "Joe" }
, { name = "Mary" }
]
},
{
title = "Bar"
, users = [
{ name = "Jill" }
, { name = "Jack" }
]
}
]
}
The desired result is that the room Foo was created and Joe and Mary were assigned, and Bar was created and Jill and Jack were assigned.
The view, for now, would just be a simple button that triggers an action.
div []
[ button [ onClick InviteUsersToRoom ] [ text "Invite users to room" ] ]
I've created 2 POST requests:
createRoom: take a title, create a room using the title and return the room_id
addUser: take a room_id and a user's name, add the the users to the room and return the status of ok
example:
-- create a room for each group
-- passing in `title` as the room name
-- which will return the room id from `decodeCreateRoomResponse`
createRoom : String -> String -> Cmd Msg
createRoom title =
Task.perform
CreateRoomsFail
CreateRoomsSuccess
(Http.post
decodeCreateRoomResponse
("https://some_api?room=" ++ title)
Http.empty
)
decodeCreateRoomResponse : Json.Decoder String
decodeCreateRoomResponse =
Json.at ["room", "id"] Json.string
-- add a user to a room using a `room_id` and the user's name
-- returns a bool from `decodeAddUserResponse`
addUser : String -> String -> Cmd Msg
addUser room_id user =
Task.perform
AddUserFail
AddUserSuccess
(Http.post
decodeCreateChannelResponse
("https://some_api?room=" ++ room_id ++ "&user=" ++ user)
Http.empty
)
decodeAddUserResponse : Json.Decoder String
decodeAddUserResponse =
Json.at ["ok"] Json.bool
I'm wondering how you'd go about stitching this up altogether, so that onclick :
iterate each group
make the POST to create the Room
take the room_id from the response and iterate the users
POST the room_id and the users name
Any help is appreciated.

You've got a few scattered errors that I won't explicitly point out because the compiler will help you, but you're off to a good start. You already have some Http handling Cmds built up so you just need to wire things up with your update function.
Let's define your Model explicitly (you may already be doing this but it isn't in your example):
type alias User =
{ name : String }
type alias Group =
{ title : String
, users : List User
}
type alias Model =
{ groups : List Group }
Based off your functions, here's how I interpret your Msg type, with one small change which is to add a list of users as a parameter to CreateRoomsSuccess.
type Msg
= InviteUsersToRoom
| CreateRoomsFail Http.Error
| CreateRoomsSuccess (List User) String
| AddUserFail Http.Error
| AddUserSuccess Bool
Now we can tweak createRoom in order to pass along the list of users to create. Note that this isn't creating any users at this time. It is using currying to create a partially-applied function so that when the CreateRoomsSuccess case is handled in the update function, it already has the list of users that need to be created (rather than having to look them up in the model list):
createRoom : Group -> Cmd Msg
createRoom group =
Task.perform
CreateRoomsFail
(CreateRoomsSuccess group.users)
(Http.post
decodeCreateRoomResponse
("https://some_api?room=" ++ group.title)
Http.empty
)
To create the list of rooms, you simply map the list of groups to a list of Cmds that perform the post. This will happen when the button is clicked:
case action of
InviteUsersToRoom ->
model ! List.map createRoom model.groups
...
You'll have to implement the update cases for when errors occur. Next up, we have to handle the CreateRoomsSuccess message. This is where you'll need to look up the list of users for a group. Again, you'll map over the function you already created that handles the Http task:
case action of
...
CreateRoomsSuccess users roomID ->
model ! List.map (addUser roomID << .name) users
...
You'll have to handle the AddUserFail and AddUserSuccess cases, but the above examples should help you understand how to post multiple messages and act accordingly based on the success or failure of each.

Related

Destructuring records in Elm with "as" word

I'm doing the Elm exercises in Exercism again and there's a thing unclear to me so far. How does the "as" destructuring work? In the beginning I didn't understand anything. After read Yang Wei's Elm destructuring (or pattern matching) cheatsheet, more specifically this part:
myRecord = { x = 1, y = 2, z = 3}
computeSomething ({x, y} as wholeRecord) =
-- x and y refer to the x and y fields of the passed in record
-- wholeRecord is the complete record
-- i.e. x and wholeRecord.x refer to the same field
-- but z is only accessible as wholeRecord.z
A lot of things became more cleat to me. The problem happened when I tried to make it by myself. So, the Ticket, Please! problem imports three types, called Ticket, Status and User and I should write a functions that receives a ticket and an user and returns a modified ticket, as the given example:
assignTicketTo (User "Danny")
(Ticket
{ status = New
, createdBy = ( User "Jesse", 3 )
, assignedTo = Just (User "Alice")
, comments = [ ( User "Jesse", "I've been waiting for 6 months!!" ) ]
}
)
-- => Ticket
-- { status = InProgress
-- , createdBy = ( User "Jesse", 3 )
-- , assignedTo = Just (User "Danny")
-- , comments = [ ( User "Jesse", "I've been waiting for 6 months!!" ) ]
-- }
The big problem comes here, because after descruture the Ticket variable and use as to attribute it to a different variable (as below), the language tells me that the new variable has a different type, i.e. it is not a Ticket.
assignTicketTo : User -> Ticket -> Ticket
assignTicketTo user ({ status, assignedTo } as ticket) =
if (status == New) then
{ ticket | status = InProgress, assignedTo = Just user }
else
ticket
What am I doing wrong? I read the answer about Type mismatch when trying to destructure type in Elm, but the only thing that I can imagine I could possibly be doing wrong, the desctructuring, is not correct either:
assignTicketTo : User -> Ticket -> Ticket
assignTicketTo user (Ticket { status, assignedTo } as ticket) =
if (status == New) then
{ ticket | status = InProgress, assignedTo = Just user }
else
ticket
Edit: The error messages are:
This is not a record, so it has no fields to update!
45| Ticket { ticket | status = InProgress, assignedTo = Just user }
^^^^^^
This `ticket` value is a:
Ticket
But I need a record!
and
The 2nd argument to `assignTicketTo` is weird.
43| assignTicketTo user ({ status, assignedTo } as ticket) =
^^^^^^^^^^^^^^^^^^^^^^
The argument is a pattern that matches record values of type:
{ c | assignedTo : a, status : b }
But the type annotation on `assignTicketTo` says the 2nd argument should be:
Ticket
The problem here is that Ticket is not a record type. It's a custom type with a single variant containing a record. Its definition (which is essential context that you should have posted in the question) is:
type Ticket
= Ticket
{ status : Status
, createdBy : ( User, Int )
, assignedTo : Maybe User
, comments : List ( User, String )
}
To destructure this you have to first unwrap the custom type, then unwrap the record and give just that a name:
Ticket ({ status, assignedTo } as ticket)
And then to return a Ticket again you also have to wrap the record back up in the Ticket constructor after updating it. The full working function therefore is:
assignTicketTo : User -> Ticket -> Ticket
assignTicketTo user (Ticket ({ status, assignedTo } as ticket)) =
if status == New then
Ticket { ticket | status = InProgress, assignedTo = Just user }
else
Ticket ticket
Why the Ticket type has been designed in this way I don't know. It seems unnecessarily convoluted and invites confusion. So please don't feel too bad about that!

Prioritize an Object within a Firebase Query

I have the following structure in my Firebase Database
-- user-posts
---- -KeKDik4k3k5Wjnc
------ title: "Batman is the Greatest Hero"
------ body: "There is basically no other hero to compare here..."
---- -K34idfgKlksdCxq
------ title: "Superman is Weak"
------ body: "Let's talk about a shoddy, overrated alien for a m..."
Say I want to query all objects from the /user-posts node, but with post -KeKDik4k3k5Wjnc set/sorted as the very first element. Can this also be done in Firebase? If so, could it also be combined with limitToFirst? I don't see this exact functionality in the documentation but I may have overlooked.
I'm looking to avoid manipulating the array myself if I can help it.
Any input is appreciated?
Say you have a usersRef node in your database, and a Post Object with an id, title and body in your project, you could combine queryOrderedByKey and queryLimited like so:
func userPostObserver(_ completion: #escaping () -> ()) {
guard let currentUserId = Auth.auth().currentUser?.uid else {
return
}
usersRef.child(currentUserId).child("posts").queryOrderedByKey.queryLimited(toLast: 10).observe(.childAdded, with: { [weak self] (snapshot) in
guard let title = snapshot.childSnapshot(forPath: "title").value as? String,
let body = snapshot.childSnapshot(forPath: "body").value as? String,
else {
return
}
self?.posts.append(Post(id: snapshot.key, title: title, body: body))
completion()
})
}
This would retrieve the last 10 posts for this specific users ordered by key (Firebase unique push id is generated from the data at which the data was created) so you would get an ordered list !
Note: queryLimited(toLast: 10) will grab the last 10 posts that were added in this node, which means it will grab the most recent posts. It will also get fired for every new post that is added to this node.

How to perform multiple Http requests (Tasks) in bulk in Elm lang

I want to load user profile before rendering something into the page but the whole user profile is composed of different parts that are loaded by multiple HTTP requests.
So far I'm loading user profile in sequence (one by one)
type alias CompanyInfo =
{ name: String
, address: ...
, phone: String
, ...
}
type alias UserProfile =
{ userName: String
, companyInfo: CompanyInfo
, ...
}
Cmd.batch
[ loadUserName userId LoadUserNameFail LoadUserNameSuccess
, loadCompanyInfo userId LoadCompanyInfoFail LoadCompanyInfoSuccess
...
]
But that's not very effective. Is there a simple way how to perform a bunch of Http requests and return just one complete value?
Something like this
init =
(initialModel, loadUserProfile userId LoadUserProfileFail LoadUserProfileSuccess)
....
You can achieve this using Task.map2:
Edit: Updated to Elm 0.18
Task.attempt LoadUserProfile <|
Task.map2 (\userName companyInfo -> { userName = userName, companyInfo = companyInfo })
(Http.get userNameGetUrl userDecoder |> Http.toTask)
(Http.get companyInfoGetUrl companyInfoDecoder |> Http.toTask)
You can then get rid of the individual LoadUserName... and LoadCompanyInfo... Msgs. In Elm 0.18, the need for separate Fail and Succeed Msgs is addressed by Task.attempt expecting a Result Error Msg type, so that LoadUserProfile is defined like this:
type Msg
= ...
| LoadUserProfile (Result Http.Error UserProfile)
map2 will only succeed once both tasks succeed. It will fail if any of the tasks fail.

Technique to get an id back with an http query when empty array is returned

Let me first say I have a solution to this problem, but I'm interested in knowing whether there is a better way, and whether I'm doing something wrong.
I have a table of objects on the front-end of a webapp, I need to asynchronously load some data for the objects as it is needed on a per-object basis. The server returns a JSON array containing the data for that object, and the data contains the object's key, so I can update the object on the front-end with its data. When there is no data, I just get an empty array, which unfortunately presents no way of updating the object, since I don't have the key to update it with. This can result in another query later, which is a waste of time/resources. I can't modify the server, is there a way to do this nicely?
My current solution is to just set the object's data to an empty array before sending the request, then just update when the result is received if the result is nonempty.
I was wondering if there is a better/more idiomatic way to do this.
For reference, I'm using Elm with PostgREST as the backend.
You can use currying and partial function application to indicate which object ID should be updated.
I'm assuming you have some code similar to this:
type Msg
= ...
| FetchData Int
| DataFetched [Data]
| DataFetchFail Http.Error
-- inside the update function
update msg model =
case msg of
...
FetchData id =
model ! [ Task.perform DataFetchFail DataFetched (Http.post ...) ]
If you define your DataFetched constructor to include the ID as the first parameter, you can use partial application to include the ID for future lookup, regardless of what the server returns.
Here's the same code chunks with this idea:
type Msg
= ...
| FetchData Int
| DataFetched Int [Data]
| DataFetchFail Http.Error
-- inside the update function
update msg model =
case msg of
...
FetchData id =
model ! [ Task.perform DataFetchFail (DataFetched id) (Http.post ...) ]
You could also add the ID to the Fail message for more fine-grained error messages.

How to publish a view/transform of a collection in Meteor?

I have made a collection
var Words = new Meteor.Collection("words");
and published it:
Meteor.publish("words", function() {
return Words.find();
});
so that I can access it on the client. Problem is, this collection is going to get very large and I just want to publish a transform of it. For example, let's say I want to publish a summary called "num words by length", which is an array of ints, where the index is the length of a word and the item is the number of words of that length. So
wordsByLength[5] = 12;
means that there are 12 words of length 5. In SQL terms, it's a simple GROUP BY/COUNT over the original data set. I'm trying to make a template on the client that will say something like
You have N words of length X
for each length. My question boils down to "I have my data in form A, and I want to publish a transformed version, B".
UPDATE You can transform a collection on the server like this:
Words = new Mongo.Collection("collection_name");
Meteor.publish("yourRecordSet", function() {
//Transform function
var transform = function(doc) {
doc.date = new Date();
return doc;
}
var self = this;
var observer = Words.find().observe({
added: function (document) {
self.added('collection_name', document._id, transform(document));
},
changed: function (newDocument, oldDocument) {
self.changed('collection_name', oldDocument._id, transform(newDocument));
},
removed: function (oldDocument) {
self.removed('collection_name', oldDocument._id);
}
});
self.onStop(function () {
observer.stop();
});
self.ready();
});
To wrap transformations mentioned in other answers, you could use the package I developed, meteor-middleware. It provides a nice pluggable API for this. So instead of just providing a transform, you can stack them one on another. This allows for code reuse, permissions checks (like removing or aggregating fields based on permissions), etc. So you could create a class which allows you to aggregate documents in the way you want.
But for your particular case you might want to look into MongoDB aggregation pipeline. If there is really a lot of words you probably do not want to transfer all of them from the MongoDB server to the Meteor server side. On the other hand, aggregation pipeline lacks the reactivity you might want to have. So that published documents change counts as words come in and go.
To address that you could use another package I developed, PeerDB. It allows you to specify triggers which would be reactively called as data changes, and stored in the database. Then you could simply use normal publishing to send counts to the client. The downside is that all users should be interested in the same collection. It works globally, not per user. But if you are interested in counts of words per whole collection, you could do something like (in CoffeesScript):
class WordCounts extends Document
#Meta
name: 'WordCounts'
class Words extends Document
#Meta
name: 'Words'
triggers: =>
countWords: #Trigger ['word'], (newDocument, oldDocument) ->
# Document has been removed.
if not newDocument._id
WordCounts.update
length: oldDocument.word.length
,
$inc:
count: -1
# Document has been added.
else if not oldDocument._id
WordCounts.update
length: newDocument.word.length
,
$inc:
count: 1
# Word length has changed.
else if newDocument.word.length isnt oldDocument.word.length
WordCounts.update
length: oldDocument.word.length
,
$inc:
count: -1
WordCounts.update
length: newDocument.word.length
,
$inc:
count: 1
And then you could simply publish WordCounts documents:
Meteor.publish 'counts', ->
WordCounts.documents.find()
You could assemble the counts by going through each document in Words, (cursor for each)
var countingCursor = Words.find({});
var wordCounts = {};
countingCursor.forEach(function (word) {
wordCounts[word.length].count += 1;
wordCounts[word.length].words = wordCounts[word.length].words || []
wordCounts[word.length].words.push(word);
});
create a local collection,
var counts = new Meteor.Collection('local-counts-collection', {connection: null});
and insert your answers
var key, value;
for (key in wordCounts) {
value = object[key];
counts.insert({
length: key,
count: value.count,
members: value.words
});
}
Counts is now a collection, just not stored in Mongo.
Not tested!

Resources