Parse Platform on iOS: best way to replace changed local values with more-recently changed server values? - ios

So imagine the following scenario, using the Parse platform on iOS:
I get a PFObject from the server, let's call it GlassChalice.
Someone else, let's say Bill Blofeld, changes GlassChalice from a different location.
Later, I make some changes to my local GlassChalice, but don't save them to the server.
Still later, I want to update GlassChalice, but I want to update it to the current server values, in other word Bill Blofeld's values. I do not want to replace the server values with my local values, and also do not want to reset my local values to the values GlassChalice was loaded with.
So if I use revert(), will I get what I want?
According to the Parse docs:
- revert Clears any changes to this object made since the last call to save and sets it back to the server state.
...but, as in my example, clearing "changes made since the last call to save" and setting it "back to the server state" aren't always the same thing.
So far this seems like the only way to guarantee the results I want, but it has one obvious problem:
public func updateObjectFromServer(_ objectToUpdate: PFObject, then doThis: (()->Void)? = nil) {
let query = PFObject.query()
query?.whereKey("objectId", equalTo: objectToUpdate.objectId!)
query?.getFirstObjectInBackground (block: {
(serverObject, error) in
if error.isNil() {
objectToUpdate["numberOfLimbs"] = serverObject?["numberOfLimbs"]
objectToUpdate["eyePlacement"] = serverObject?["eyePlacement"]
objectToUpdate["crossStitchingTalentRating"] = serverObject?["crossStitchingTalentRating"]
objectToUpdate["clamsEaten"] = serverObject?["clamsEaten"]
} else {
//handle error...
}
doThis?()
})
}
But the huge problem here is that I have to know all the key names, and type them in explicitly, for this to work.
Is there a better, more generic, way?

Related

Acumatica Purchase Receipt Return: Open PO Line Default to false

After clicking the "Return" action on a selected item from a completed Purchase Receipt, we're trying to get the "Open PO Line" value to default to false. Anyone know how customize this?
The field defaulting seems to be overwritten when we press the "Return" button. We've tried several different events in the grid but none of the seem to work.
The desired result is to default the "Open PO Line" to false after a return and once the return is released the Purchase Order line associated with the return should remain completed.
Research
The AllowOpen field on POReceiptLine is a PXBool. This means that the value must be populated via a PXDBScalar, PXFormula, etc. or via some business logic in the graph. To see what is happening, let's look at the DAC for POReceiptLine...
#region AllowOpen
public abstract class allowOpen : PX.Data.BQL.BqlBool.Field<allowOpen> { }
protected Boolean? _AllowOpen;
[PXBool()]
[PXUIField(DisplayName = "Open PO Line", Visibility = PXUIVisibility.Service, Visible = true)]
public virtual Boolean? AllowOpen
{
get
{
return this._AllowOpen;
}
set
{
this._AllowOpen = value;
}
}
#endregion
As you can see, there isn't any logic to this field in the DAC, so we need to turn to the graph to see how it is used. (Even if there were logic in the DAC, we would need to see if the graph does something. However, logic in the DAC might have been an easy override with CacheAttached - unfortunately, not in this case.)
Let's turn to POReceiptEntry where the return is handled. We find AllowComplete and AllowOpen being set in the POReceiptLine_RowSelected event, as we would expect since it must be populated on the graph side of code having no logic in the DAC.
if ((row.AllowComplete == null || row.AllowOpen == null) && fromPO)
{
POLineR source = PXSelect<POLineR,
Where<POLineR.orderType, Equal<Required<POLineR.orderType>>,
And<POLineR.orderNbr, Equal<Required<POLineR.orderNbr>>,
And<POLineR.lineNbr, Equal<Required<POLineR.lineNbr>>>>>>
.Select(this, row.POType, row.PONbr, row.POLineNbr);
// Acuminator disable once PX1047 RowChangesInEventHandlersForbiddenForArgs [Legacy, moved the exception here from PX.Objects.acuminator because the condition was changed]
row.AllowComplete = row.AllowOpen = (row.Released == true) ?
(row.ReceiptType != POReceiptType.POReturn ? source?.Completed == true : source?.Completed != true) :
(source?.AllowComplete ?? false);
The field is populated in the row.AllowComplete = row.AllowOpen = (row.Released == true) ?... section of code.
Subsequently, we see that the CopyFromOrigReceiptLine method sets this value to false on the "destLine" being created.
destLine.AllowOpen = false;
As that isn't "true" then we know this isn't our spot. Continuing on, we see in UpdatePOLineCompletedFlag that AllowComplete and AllowOpen are being set. This could be our spot (or one of them).
row.AllowComplete = row.AllowOpen = poLineCurrent.AllowComplete;
Side note: It is worth noting that this line appears twice in an if then else. In both cases it is going to be executed, therefore it would be better coding practice to place this identical statement AFTER the if then else since both the if and else conditions will execute this same statement regardless of the if.
This particular use appears to be pulling the value from the AllowComplete field of the PO Line being received. At this point, you should consider if you need to look upstream at the PO Line to see if the field there needs to be manipulated as well. I cannot answer that for you as your business case will drive the decision.
There also is a line in the Copy method that sets AllowComplete and AllowOpen.
aDest.AllowComplete = aDest.AllowOpen = (aSrc.CompletePOLine == CompletePOLineTypes.Quantity);
The Copy method is overloaded, and the other signature of the method sets the values to true.
aDest.AllowComplete = true;
aDest.AllowOpen = true;
Both of these cases may need customization as well, but I don't think it's the primary issue.
Next Steps
At this point, we see that either the field is being set in UpdatePOLineCompletedFlag or in methods that seem related to copying records. You will need to investigate further if the copy related methods warrant a change as well. However, I think the initial focus should be on the UpdatePOLineCompletedFlag method.
If we find the other points identified require customization, we likely will handle them all the same way... Override the base method/event, invoke the original method/event in our override, and then force the values to fit our business case. Careful testing will be needed since forcibly altering these values may create unforeseen negative ripples.
Something to try
We want to update (or create) a graph extension for POReceiptEntry to override the UpdatePOLineCompleteFlag method. This compiles, but it is completely untested on my part. We need to create a delegate and specify the PXOverride attribute. Then we want to execute the base method before we override the field(s) in question.
Note the extra code (commented out) as a reminder that you need to be careful of methods (typically events) updating our record in the cache and needing to be located so that we don't use a stale copy of the record. I don't think that's necessary in this case, but it seems to be somewhat obscure in code samples that I see. Of course, that is because I'm always looking at the code repository which rarely has graph extensions overriding event handlers!
#region CreateReceiptEntry Override
public delegate void UpdatePOLineCompleteFlagDelegate(POReceiptLine row, bool isDeleted, POLine aOriginLine);
[PXOverride]
public virtual void UpdatePOLineCompleteFlag(POReceiptLine row, bool isDeleted, POLine aOriginLine, UpdatePOLineCompleteFlagDelegate baseMethod)
{
//Execute original logic
baseMethod(row, isDeleted, aOriginLine);
/* If the base method has updated the cache, then we would need to locate the updated record in the cache to proceed
* This tends to be the case more often with event handlers, so it probably isn't needed in this case.
* This is just for reference as a training reminder
//If row has been updatd in the baseMethod, let's go get the updated cache values
POReceiptLine locateRow = Base.transactions.Locate(row);
if (locateRow != null) row = locateRow;
*/
//Override the fields to false - need to test to see if this creates any issues with breaking existing business logic
row.AllowComplete = row.AllowOpen = false;
}
#endregion
If this doesn't get you the specific answer you need, I hope it at least gives you some insight into how to hunt down "the spot" to change. I suspect you may need to update the POLine for a complete solution as hinted above. (See the event handler POReceiptLine_AllowOpen_FieldUpdated for the code that leads me to that conclusion.)
Good luck with your customization, and happy coding!

Firebase Firestore Swift, Timestamp but server time?

With Firestore, I add a timestamp field like this
var ref: DocumentReference? = nil
ref = Firestore.firestore()
.collection("something")
.addDocument(data: [
"name": name,
"words": words,
"created": Timestamp(date: Date())
]) { ...
let theNewId = ref!.documentID
...
}
That's fine and works great, but it's not really correct. Should be using the "server timestamp" which Firestore supplies.
Please note this is on iOS (Swift) and Firestore, not Firebase.
What is the syntax to get a server timestamp?
The syntax you're looking for is:
"created": FieldValue.serverTimestamp()
This creates a token which itself has no date value. The value is assigned by the server when the write is actually written to the database, which could be much later if there are network issues, so keep that in mind.
Also keep in mind that because they are tokens, they can present different values when you read them, to which we can configure how they should be interpreted:
doc.get("created", serverTimestampBehavior: .none)
doc.get("created", serverTimestampBehavior: .previous)
doc.get("created", serverTimestampBehavior: .estimate)
none will give you a nil value if the value hasn't yet been set by the server. For example, if you're writing a document that relies on latency-compensated returns, you'll get nil on that latency-compensated return until the server eventually executes the write.
previous will give you any previous values, if they exist.
estimate will give you a value, but it will be an estimate of what the value is likely to be. For example, if you're writing a document that relies on a latency-compensated returns, estimate will give you a date value on that latency-compensated return even though the server has yet to execute the write and set its value.
It is for these reasons that dealing with Firestore's timestamps may require handling more returns by your snapshot listeners (to update tokens). A Swift alternative to these tokens is the Unix timestamp:
extension Date {
var unixTimestamp: Int {
return Int(self.timeIntervalSince1970 * 1_000) // millisecond precision
}
}
"created": Date().unixTimestamp
This is definitely the best explanation of how the timestamps work (written by the same Doug Stevenson who actually posted an answer): https://medium.com/firebase-developers/the-secrets-of-firestore-fieldvalue-servertimestamp-revealed-29dd7a38a82b
If you want a server timestamp for a field's value, use FieldValue.serverTimestamp(). This will return a token value that gets interpreted on the server after the write completes.

Swift Firebase database overwriting

I am making a real-time messenger using Firebase. Currently, whenever I press a button I want a new message to be appended to the channel with the index of the message, but currently, whenever I press the button a new message is created that overwrites the old message. I know that setValue is usually the issue, but I really cannot tell what I'm doing wrong. What the database looks like before I add my new message. This is what it looks like after I add a new message here, and then the code I am using to add to the database.
#IBAction func sendMessageTapped(_ sender: Any) {
if messageTextField.text == "" {
print("blank")
return
} else {
// First we will update the amount of messages that the channel has.
ref.child("channels").child(channelName!).setValue(["numberOfMessages" : numberOfMessages+1 ])
numberOfMessages += 1
// after we have updated the amount of messages we will try to create a new message.
ref.child("channels").child(channelName!).child("messages").child(String(numberOfMessages)).child("message").child("content").setValue(messageTextField.text)
ref.child("channels").child(channelName!).child("messages").child(String(numberOfMessages)).child("message").child("name").setValue("Buddy")
}
}
ok, Firebase is not a traditional table based database, is a DOCUMENT based database. At the very top you have a thing called a "collection" which is just a list of "document" things. In your case, you'd have several collection things to serve as channels: "General", "TopicQ", "InterstingStuff" etc, and within them each message as a document. No need to have a document, to then list the messages within it.
Second, you don't need indexes as you're using them, make the message id an attribute of the message, because firebase support querying by field, and even then is questionable because if you make each message a document, they will have their own auto generated id's if you want.
Third, in your code you're rewriting the whole document each time, this is why you lose your previous messages, so if you keep it, you need to add a merge option:
// Update one field, creating the document if it does not exist.
db.collection("cities").document("BJ").setData([ "capital": true ], merge: true)
you probably want to do something like this. This is what I did for my app, hope this helps someone. This rootRef.childByAutoId() generates a new entry with unique id. You can use this as reference for your case.
let rootRef = Database.database().reference(withPath: "channels")
let childRef = rootRef.childByAutoId()
let values = ["Type": self.textField.text!, "message": self.textView.text!] as? [String : Any]
childRef.updateChildValues(values)

Filter a realm results list with a Swift Set or array

I am trying to figure out how to delete a local stored set of Entities which are excluded from what I receive from a server api. In this case we have let's say 10 entities stored locally in realm, and the server sends me 8 entities. 6 of them corresponds to entities I already have locally, 2 are new. Locally I have 4 entities which are "outdated" because the server doesn't provide them.
Of course I could delete everything and save the new entities, but I was trying to make things more efficient.
However I don't understand, once I created a swift Set which represents the difference between the local and remote situation, how can I delete the local objects, because realm tells me i can only delete objects which belong to the realm.
I've tried working with resultset, but I can't figure out a way to filter object with a "IN" clause to which is provided a set of objects, even if I recall doing something similar in the past.
let realm = try! Realm()
try! realm.write{
let oldEvents = Array(self.events!)
var newEvents = [Event]()
for event in events{
if let e = Mapper<Event>().map(JSONObject: event){
newEvents.append(e)
}
}
//here I create a set which contains the difference between remote and local entitites, and create an Array with them
let difference = Array(Set(newEvents).subtracting(Set(oldEvents)))
//this is obviously wrong
let objToDelete = realm.objects(Event.self).filter("event in %#", difference)
realm.delete(objToDelete)
//realm.delete(difference)->this gives the error that you can delete only objects that belong to the realm
//here I would like to create or update the new events
for newEvent in newEvents{
realm.create(Event.self, value: event, update: true)
}
}
Any hint would be greatly appreciated on how to deal with this situation. Thanks!

Returning Updated Results from DBSet.SqlQuery

I want to use the following method to flag people in the Person table so that they can be processed. These people must be flagged as "In Process" so that other threads do not operate on the same rows.
In SQL Management Studio the query works as expected. When I call the method in my application I receive the row for the person but with the old status.
Status is one of many navigation properties off of Person and when this query returns it is the only property returned as a proxy object.
// This is how I'm calling it (obvious, I know)
var result = PersonLogic.GetPeopleWaitingInLine(100);
// And Here is my method.
public IList<Person> GetPeopleWaitingInLine(int count)
{
const string query =
#"UPDATE top(#count) PERSON
SET PERSON_STATUS_ID = #inProcessStatusId
OUTPUT INSERTED.PERSON_ID,
INSERTED.STATUS_ID
FROM PERSON
WHERE PERSON_STATUS_ID = #queuedStatusId";
var queuedStatusId = StatusLogic.GetStatus("Queued").Id;
var inProcessStatusId = StatusLogic.GetStatus("In Process").Id;
return Context.People.SqlQuery(query,
new SqlParameter("count", count),
new SqlParameter("queuedStateId", queuedStateId),
new SqlParameter("inProcessStateId", inProcessStateId)
}
// update | if I refresh the result set then I get the correct results
// but I'm not sure about this solution since it will require 2 DB calls
Context.ObjectContext().Refresh(RefreshMode.StoreWins, results);
I know it is an old question but this could help somebody.
It seems you are using a global Context for your query, EF is designed to retain cache info, if you allways need fresh data must use a fresh context to retrieve it. as this:
using (var tmpContext = new Contex())
{
// your query here
}
This create the context and recycle it. This means no cache was stored and next time it gets fresh data from database not from cache.

Resources