How to configure sync. formula for public and private docs

As I cannot get any output from the sync function (like using console.log(....)) it really feels like a black box when trying to develop the formula.

Any good advice on how to debug the formula would be much appreciated! It is quite time consuming to make a change, stop/start the sync. gateway - run a sync, and “guess” if things went Ok - and mostly try to figure out why not?

I have a challenge in getting public and private documents replicated correctly. The scene is this:

  1. All can sync public documents (mostly meta data) to Couchbase Lite (CBL) - but cannot write back to the server
  2. A user can only sync his private docs to CBL - and delete/update them back to the server
  3. A user can mark some of his documents as “public” - but should still be able to update/delete as if his “private” document

I have made many attempts over the last couple of days. This is my current sync. formula:

function (doc, oldDoc) {
    function _log(t) {
        // Does not work - yet...
        // console.log('SG: ' + t);
    }
    function _getUserKey(d) {
        var key = null;
        if (d) {
            if (d.type == 'User') {
                key = d.key;
            } else if (d.type == 'FishingTrip' || d.type == 'Catch' || d.type == 'Photo' || d.type == 'Private' || d.type == 'Image' || d.type == 'Feedback') {
                // TODO: Not sure about Feedback here....??
                key = d.userkey;
            }
        }
        return key;
    }

    if (doc && doc._deleted && oldDoc) {
        // Doc. deleted -> if public then require
        var userkey = _getUserKey(oldDoc);
        if (userkey != null) {
            requireUser(userkey);
        } else {
            requireAdmin();
        }
        _log('doc deleted, id: ' + (doc._id || 'no id!') + ', ' + (oldDoc ? ('old key=' + oldDoc.key + ', userkey=' + oldDoc.userkey) : 'no oldDoc'));
        return;
    }
    // Document type is mandatory
    if (doc.type == undefined) {
        throw ({ forbidden: "Document type is required." });
    }
    // Some document types not allowed on mobile
    if (doc.type == 'EnvLake' || doc.type == 'EnvMeasurement' || doc.type == 'ActivityLog') {
        throw ({ forbidden: "Document type not allowed to sync to mobile..." });
    }
    // Document key is mandatory
    if (doc.key == undefined) {
        throw ({ forbidden: "Document key is required." });
    }
    // Allow anyone to create a Feedback on the server
    if (oldDoc == null && doc.type == 'Feedback') {
        _log('Created feedback: ' + (doc._id || 'no id!') + ', key: ' + doc.key + ', user: ' + doc.userkey);
        return;
    }

    // All public docs are available in the app
    if (doc.ispublic) {
        _log('public, id: ' + (doc._id || 'no id!'));
        channel('!');
    }
    // All non-club fishing trips and catches are available (for stats)
    if ((doc.type == 'FishingTrip' || doc.type == 'Catch') && doc.clubonlykey == undefined) {
        _log('non-club trips, id: ' + (doc._id || 'no id!'));
        channel('!');
    }
    // All non-specific user info is available (for stats)
    if (doc.type == 'User') {
        _log('User doc, id: ' + (doc._id || 'no id!'));
        channel('!');
    }

    // Only non-public docs "owned" by user can be replicated
    var userkey = _getUserKey(doc);
    if (userkey != null) {
        if (oldDoc != null) {
            // Update
            if (oldDoc.type != doc.type) {
                throw ({ forbidden: "Can't change doc type" });
            }
            if (oldDoc.key != doc.key) {
                throw ({ forbidden: "Can't change doc key" });
            }
            if (oldDoc.userkey && oldDoc.userkey != doc.userkey) {
                throw ({ forbidden: "Can't change user key" });
            }
        }
        _log('User owned, id: ' + (doc._id || 'no id!') + ', type: ' + doc.type + ', user: ' + doc.userkey);
        if(doc.ispublic){
            requireAdmin();
        }
        requireUser(userkey);
        channel('channel.' + userkey);
        access(userkey, 'channel.' + userkey);
    }

    /*
    if(oldDoc == null){
        // Creation
        if(doc.type == 'Feedback'){
          _log('Created feedback: ' + (doc._id || 'no id!') + ', key: ' + doc.key + ', user: ' + doc.userkey);
        }else{
          if(doc.key == undefined){
              throw({forbidden: "Can't create doc without key"});
          }
          if(doc.type == 'User'){
            // Ok
          }else {
            if(doc.userkey == undefined){
              //throw({forbidden: "Can't create doc without user key"});
            }
          }
          requireUser(_getUserKey(doc));
          _log('Create: ' + (doc._id || 'no id!') + ', type: ' + doc.type + ', user: ' + doc.userkey);
        }
      }else{
        // Update
        if(oldDoc.type != doc.type){
          throw({forbidden: "Can't change doc type"});
        }
        if(oldDoc.key != doc.key){
          throw({forbidden: "Can't change doc key"});
        }
        if(oldDoc.userkey && oldDoc.userkey != doc.userkey){
          throw({forbidden: "Can't change user key"});
        }
        requireUser(_getUserKey(oldDoc));
        _log('Update: ' + (doc._id || 'no id!') + ', type: ' + doc.type + ', user: ' + doc.userkey);
      }
 */
    // TODO: Check if covered:
    // Creation of new Feedback docs (perhaps without userkey)?
    // Creation of new docs?
    // Updates to existing docs?
    // Readonly for all non-user specific docs...
}

So at the moment deletions from the server are not sent to the CBL. The new document is written Ok.

Earlier today I had this part of the formula to handle deletions:

  if (doc && doc._deleted){
    _log('doc deleted, id: ' + (doc._id || 'no id!') + ', ' + (oldDoc ? ('old key=' + oldDoc.key + ', userkey=' + oldDoc.userkey) : 'no oldDoc'));
    return;
  }

But I want to try and add functionality to disallow the user from changing public documents on the server by using requireUser(userkey) and requireAdmin().

As I understand it all changes written to CBL are written as admin - and therefore I thought that using requireAdmin() would allow deletions locally - but not on the server. As I mentioned some documents could be public as well as private - but if I do requireUser(userkey) and this is not the specific user (but there is a userkey) then I should use requireAdmin() to write any deletion locally (initiated by the other user) - but of course not be able to send a deletion of another user’s document back to the server.

How can I do this? Obviously, my current formula is not working - and not even for my own documents.

I really would like to see some specific examples of sync. formulas to find out how to do things like this (that aren’t really that complex!)

I’m on Couchbase Community Server 6.0.0 and C# Couchbase Lite 2.5

When troubleshooting the above I see this in the sg_info.log:

2019-05-08T20:01:26.629+02:00 [INF] Import: Created new rev ID for doc "Bait:19" / "1-dd4db1d29ab261332c6fccc8191e6075"
2019-05-08T20:01:26.632+02:00 [INF] CRUD: 	Doc "Bait:19" / "1-dd4db1d29ab261332c6fccc8191e6075" in channels "{!}"
2019-05-08T20:01:26.635+02:00 [INF] CRUD: Stored doc "Bait:19" / "1-dd4db1d29ab261332c6fccc8191e6075" as #289435
2019-05-08T20:01:26.637+02:00 [INF] Cache: Received #289435 after   5ms ("Bait:19" / "1-dd4db1d29ab261332c6fccc8191e6075")
2019-05-08T20:01:26.637+02:00 [INF] Cache: #289435 ==> channels {!, *}
2019-05-08T20:01:26.637+02:00 [INF] Cache: c:[5cca6943] getCachedChanges("!", 289434) --> 1 changes valid from #289435
2019-05-08T20:01:26.649+02:00 [INF] Sync: c:[5cca6943] Sent 1 changes to client, from seq 289435.  User:587CE5200641ABD9C1257E500051DDCD
2019-05-08T20:01:39.231+02:00 [INF] Import: Created new rev ID for doc "Bait:19" / "2-6b68d5407541e11bd883cda9f8cace7c"
2019-05-08T20:01:39.233+02:00 [INF] Sync fn rejected doc "Bait:19" / "" --> 403 Document type is required.
2019-05-08T20:01:39.233+02:00 [INF] Import: Error importing doc "Bait:19": 403 Document type is required.

Can anyone explain why the code reaches the check for doc.type? I would have thought that the return inside the code to handle doc._deleted would avoid that or if one of the require... methods failed it would throw an error and also stop execution???

Is there any way I can determine the “direction” of a replication? How can I else use the right require.... to make some docs. readonly and others editable/deletable???

Ok, now I understand nothing!

I tried to comment out any logic controlling who can delete:

    if (doc && doc._deleted && oldDoc) {
        // Doc. deleted -> if public then require
/*
        var userkey = _getUserKey(oldDoc);
        if (userkey != null) {
            requireUser(userkey);
        } else {
	    channel('!');
            requireAdmin();
        }
*/
        _log('doc deleted, id: ' + (doc._id || 'no id!') + ', ' + (oldDoc ? ('old key=' + oldDoc.key + ', userkey=' + oldDoc.userkey) : 'no oldDoc'));
        return;
    }
    // Document type is mandatory
    if (doc.type == undefined) {
        throw ({ forbidden: "Document type is required." });
    }

It still throws the same “Document type is required” which is only thrown in my code after the return…???

NB: I have not run a “re-sync” as I understand from the documentation that I only need to do that for reads - not writes which I believe a delete is. I reset the situation prior to every test (by deleting the docs on the server and removing the app thus replicating all docs down to CBL again when re-launched)