How to save one value of Parse object without overwriting entire object? - swift

I have two users accessing the same object. If userA saves without first fetching the object to refresh their version, data that userB has already successfully saved would be overwritten. Is there any way(perhaps cloud code?) to access and update one, and only one, data value of a PFObject?
I was thinking about pushing the save out to the cloud, refreshing the object once it gets there, updating the value in the cloud, and then saving it back. However that's a pain and still not without it's faults.

This seems easy enough, but to me was more difficult than it should have been. Intuitively, you should be able to filter out the fields you don't want in beforeSave. Indeed, this was the advice given in several posts on Parse.com. In my experience though, it would actually treat the filtering as deletions.
My goal was a bit different - I was trying to filter out a few fields and not only save a few fields, but translating to your context, you could try querying the existing matching record, and override the new object. You can't abort via response.failure(), and I don't know what would happen if you immediately save the existing record with the field of interest and null out the request.object property - you could experiment on your own with that:
Parse.Cloud.beforeSave("Foo", function(request, response) {
// check for master key if client is not end user etc (and option you may not need)
if (!request.master) {
var query = new Parse.Query("Foo");
query.get(request.object.id).then(function(existing) {
exiting.set("some_field", request.object.get("some_field"));
request.object = exiting; // haven't tried this, otherwise, set all fields from existing to new
response.success();
}, function(error) {
response.success();
});
}
});

Related

CKModifyRecordsOperation - create referenced records simultaneously?

I'm working to refactor a somewhat clunky iterative save loop for CloudKit to use CKModifyRecordsOperation and bulk save records.
I have a Course, which has 1+ Weeks, each which has 1+ Lessons. Previously I'd create the Course in CloudKit, then create the Weeks, then the Lessons and circle back to update the Weeks with the Lesson references once created. And also fetch and save the Course record with the references to the Weeks once Weeks were created.
I've refactored to create all (Course, Week and Lesson) records locally, with the relevant references set up. E.g., course["weeks"] contains the record references for each week I've created locally, for example:
course["weeks"] = getWeekRefsForCourse(for: allWeeks)
func getWeekRefsForCourse(for allWeeks: [CKRecord]) -> [CKRecord.Reference] {
var weekRefsArray: [CKRecord.Reference] = []
for each in allWeeks {
let weekRef = CKRecord.Reference(record: each, action: .deleteSelf)
weekRefsArray.append(weekRef)
return weekRefsArray
}
The issue is when I go to save, the error I get back is:
Invalid list of records: Cycle detected in record graph
This suggests that I've got a record referring to itself, but I've gone record by record and I I can't see anything. The Weeks reference the Course and the Lessons, but not themselves, etc. So my only theory is that because I'm trying to save items that refer to other items that haven't yet been created, what I'm trying to do isn't possible.
Is the correct protocol here actually my original approach? Or is there something I'm missing?
Original approach:
Save Course
Save Week
Save Lessons
Update Weeks with Lesson references
Update Course with Week references
CKModifyRecordsOperation code:
let bulkSaveQueryOp = CKModifyRecordsOperation()
bulkSaveQueryOp.recordsToSave = [courseRecord]
bulkSaveQueryOp.recordsToSave?.append(contentsOf: weeks)
bulkSaveQueryOp.recordsToSave?.append(contentsOf: lessons)
//note I've confirmed I have the correct number of records
bulkSaveQueryOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let error = error as? CKError {
log.error(error)
} else { // success }
}
CKContainer.default().publicCloudDatabase.add(bulkSaveQueryOp)
I'm fairly certain you can create all these records together. I suspect there must be something else wrong and that's why you are getting that error.
You can create a CKReference to an object that doesn't even exist and CloudKit will still create it. A CKReference is little more than a pointer to a recordName of another object in the container.
Combining all those records into a CKModifyRecordsOperation is the right thing to do and you shouldn't have to be careful about the order of your CKRecord saves. I think another issue must be lurking somewhere.
I was able to find a similar question over in the apple dev forums for someone that had a similar issue and and it helped identify the source of the problem - that I had effectively created a loop with my .deleteSelf actions I was creating on various references.
So while the references were fine, the actions were what was causing the error.
Once I double checked and adjusted those, the error went away and I was able to save.
Spotting this was made more complex by the fact that I wasn't changing the .deleteSelf actions version what I had previously done - and was working fine - when my saves were done in sequence rather than a bulk CKModifyRecordsOperation save.
So it seams that another added benefit of CKModifyRecordsOperation with a bulk save is that it adds a layer of stupidity checking that creating items individually doesn't :)

Use Firestore rules to limit list fails on array key

I'm trying to use Firestore rules to return only documents where the current user has some sort of rights, following the advice given in https://firebase.google.com/docs/firestore/solutions/role-based-access.
However, when I implement the rule I get the dreaded "[code=permission-denied]: Missing or insufficient permissions" error message which obviously tells me nothing, I was wondering if anyone can spot what is going wrong.
My rules:
//Specific project rules - authorised users who appear in the project list
match /documents/{document} {
function isSignedIn() {
return request.auth.uid != null;
}
function getUser(rsc) {
return rsc.data.users[request.auth.uid];
}
function isOneOfUsers(rsc, array) {
return isSignedIn() && (getUser(rsc) in array);
}
allow list: if isOneOfUsers(resource, ['user','admin']);
The data stores the users information in a field on the document (12345 in the example below. The field is of type Object which allows me to put a key (the userid, 76544 in the example below) and a value against it, such as "admin".
My data:
documents/12345/users{76544:"admin"}
Now when I log on and try to get a list of the documents, I'm expecting to see this document coming back, but I get the error. I can change the function getUser to return "user" and that works, so the problem is somewhere in the evaluation of
rsc.data.users[request.auth.uid]
I would normally accept that I'm trying something that can't be done but it is a near direct copy of the official docs so I must be missing something!
Thanks in advance for your help
Here's what I think is going on: LIST is explicitly not checking every document that it is picking up, rules are not filters etc.
When writing queries to retrieve documents, keep in mind that security
rules are not filters—queries are all or nothing. To save you time and
resources, Cloud Firestore evaluates a query against its potential
result set instead of the actual field values for all of your
documents. If a query could potentially return documents that the
client does not have permission to read, the entire request fails.
Therefore, LIST will always return the full list of /documents/, there is nothing you can write in there beyond authorisation rules that will stop it from returning all of them. If this is true, if any malicious actor gets hold of an authentication, they can download the full list of all of your /documents/.
The only sensible approach to this is to lock down any attempt to use LIST (deny all) and keep your list accessible /documents/ against an individual user instead. This seems onerous, but may be the only way of doing it.

Firebase: how to generate a unique numeric ID for key?

I need numeric IDs for human readability. How do I get it in Firebase?
I want numeric ID for keys, e.g. "000000001", "000000002","00000003","00000004".
The reason I need it is because these IDs will become the permanent object ID both online and offline. I want users to be able to browse that object page by just entering URL "/objects/00000001" without efforts.
I am asking here, because I want to know if this can be done without using .priority, sub-properties, etc. I guess set method can do it somehow. If it is not possible, just tell me no, I can accept that answer.
I'd suggest reading through the Firebase documentation. Specifically, see the Saving Data portion of the Firebase JavaScript Web Guide.
From the guide:
Getting the Unique ID Generated by push()
Calling push() will return a reference to the new data path, which you can use to get the value of its ID or set data to it. The following code will result in the same data as the above example, but now we'll have access to the unique push ID that was generated
// Generate a reference to a new location and add some data using push()
var newPostRef = postsRef.push({
author: "gracehop",
title: "Announcing COBOL, a New Programming Language"
});
// Get the unique ID generated by push() by accessing its key
var postID = newPostRef.key;
Source: https://firebase.google.com/docs/database/admin/save-data#section-ways-to-save
A push generates a new data path, with a server timestamp as its key. These keys look like -JiGh_31GA20JabpZBfa, so not numeric.
If you wanted to make a numeric only ID, you would make that a parameter of the object to avoid overwriting the generated key.
The keys (the paths of the new data) are guaranteed to be unique, so there's no point in overwriting them with a numeric key.
You can instead set the numeric ID as a child of the object.
You can then query objects by that ID child using Firebase Queries.
From the guide:
In JavaScript, the pattern of calling push() and then immediately calling set() is so common that we let you combine them by just passing the data to be set directly to push() as follows. Both of the following write operations will result in the same data being saved to Firebase:
// These two methods are equivalent:
postsRef.push().set({
author: "gracehop",
title: "Announcing COBOL, a New Programming Language"
});
postsRef.push({
author: "gracehop",
title: "Announcing COBOL, a New Programming Language"
});
Source: https://firebase.google.com/docs/database/admin/save-data#getting-the-unique-key-generated-by-push
As explained above, you can use the Firebase default push id.
If you want something numeric you can do something based on the timestamp to avoid collisions
f.e. something based on date,hour,second,ms, and some random int at the end
01612061353136799031
Which translates to:
016-12-06 13:53:13:679 9031
It all depends on the precision you need (social security numbers do the same with some random characters at the end of the date). Like how many transactions will be expected during the day, hour or second. You may want to lower precision to favor ease of typing.
You can also do a transaction that increments the number id, and on success you will have a unique consecutive number for that user. These can be done on the client or server side.
(https://firebase.google.com/docs/database/android/read-and-write)
Adding to the #htafoya answer.
The code snippet will be
const getTimeEpoch = () => {
return new Date().getTime().toString();
}
As the docs say, this can be achieved just by using set instead if push.
As the docs say, it is not recommended (due to possible overwrite by other user at the "same" time).
But in some cases it's helpful to have control over the feed's content including keys.
As an example of webapp in js, 193 being your id generated elsewhere, simply:
firebase.initializeApp(firebaseConfig);
var data={
"name":"Prague"
};
firebase.database().ref().child('areas').child("193").set(data);
This will overwrite any area labeled 193 or create one if it's not existing yet.
https://firebase.google.com/docs/firestore/manage-data/transactions
Use transactions and keep a number in the database somewhere that you can increase by one. This way you can get a nice numeric and simple id.

How to programmatically refresh a screen from onFinished callback function?

e.g. we have this one guy calling server function for creating a new entity:
function loadData() {
var vServerController = mobileController.serverController();
var vJSONRequest = vServerController.createJSONRPCRequest();
vJSONRequest.setQueryMethod("createSomeNewElementBasedOnTwoIds");
vJSONRequest.addParameter("firstID", 1);
vJSONRequest.addParameter("secondID", 2);
vJSONRequest.setOnFinish(callBackOnFinish);
vServerController.addToQueue(vJSONRequest);
}
function callBackOnFinish() {
var vController = mobileController.activeController();
vController.showView(Screens.SomeScreen, true);
}
So how we can refresh a Screen after this call? The server will return not a full set of data but just a new one.
What is the best approach to doing this?
Assuming you want to update the currently displayed data, you have two options - and you propably don't need to use a callback for this
Replace the displayed dataset
Update the displayed record
(if you had user input or created a new entry) merge the entries
1: just return the data from the server with foundset.setDataMode(DataMode.REPLACE) - it will automatically refresh the displayed data. But In case you are working with multiple records for one entity and you only want to update one of them, use option 2.
2: Return only the specific record you want to update without datamode replace. The record is identified by it's key/id elements. You can find those marked with a little key symbol in the entity editor. So just make sure that those key elments and any additionaly elements you want are returned. The AppConKit will automatically merge the existing record with the data and display that.
3: If you created a new record on the device, that record will have a value called client_uuid. If you now return a record from the server that contains both the client_uuid AND the key element, the record created on the device will be merged with the server created record and the new record will displayed
Hope this helps!

Voting system with Backbone.js

I have a Book model that has the property upVotes. Book instances can be queried from the database (MongoDB), modified, and then saved. If a user upvotes a book, I update the upVotes count, and save the whole model back to the server.
The problem is that if someone else votes between the time the instance is loaded, and the time the instance is saved, then the two votes will be saved as just one vote. What I need is an easy way to say "increment the model by 1 server-side", instead of "increment the model by 1 client-side and hope there will be no conflict".
You don't have to save the whole model to the server just to change one thing, you can (and should in this case) add an upVote method to your model that does an "increment upvotes" AJAX call to your server. In your model you'd have something like this:
upVote: function() {
var self = this;
$.ajax({
url: '/some/upvote/path',
type: 'POST',
success: function(data) {
self.set('upVotes', data.upVotes);
},
// ...
});
}
And then the view would have this to handle the upvote action:
upVote: function() {
// Highlight the upvote button or provide some other feedback that
// the upvote has been seen.
this.model.upVote();
}
and you'd probably have a listener for change events on the model's upVotes property to properly increment the displayed upvote counter (if you have such a thing).
Furthermore, your /some/upvote/path on the server would just send an $inc update into MongoDB to avoid the same "two things happening at once" problem on your server. If you were using a relational database, you'd want to end up doing something like update t set upvotes = upvotes + 1 where id = ?.
There is no need for a "query, update, save" round trip on either the client or the server for a simple increment operation. Instead, treat the increment as a single increment operation and push that increment all the way down to your final persistent data storage layer.