CloudKit — Databases, Security Roles and Zones

This post is an overview of what I have learned about security and scope when working with CloudKit.



Databases

At the top of the silo is a single Cloudkit container and it has three databases: Public, Private and Shared

With this post, I’ll focus on the first two and go into the Shared database in another post

The Public database is one big database that all of the app’s users have access to. The Private database is unique to each user of the app and is tied to that user’s iCloud account. The storage limits of each user’s iCloud account applies to their private database for your app.


Permissions and Security Roles

Each Record Types has a Security setting. A matrix with roles (World, Authenticated, Creator, plus any custom roles you add) and types (create, read, write). By default, create is allowed for Creator and Authenticated. Read is allowed for World, and write is limited to just Creator.

These settings can be changed in the CloudKit dashboard. Each Record Type has its own security role settings (Individual records do NOT have security settings, only the record type).

World is what you would expect, everyone has access.

Creator refers to the creator of a particular record instance (NOT the owner of the CloudKit account).

Authenticated refers to any user who is logged in with their Apple account. No authorization is applied, any and all users with an Apple account will have permission.

Custom allows you to create your own roles in the Security Roles section. After creating a new security role, add users to the role in the User Records section. If your app doesn’t implement discovery, then you will only see users represented as hashes.

You can identify your own hash by creating a record, find it in the CloudKit database, look at the value in the Created By field and find the matching entry in the User Records section.

Security roles only apply to the PUBLIC database. Since the Private database is unique to an app’s user, then that user will be the creator for all the data and subsequently the sole person to read and write to the database.


Zones

Multiple records live in a zone and multiple zones live in a database. The Public database has only the default zone.
While Private databases have a default zone, they are also allowed custom zones. Your application can create and upload custom zones into the database.

The choice of the database and zone will have an effect on the types of remote notifications that are available.


Apple Tutorial Videos

In can be difficult finding answers to questions about CloudKit on the web. But A lot of CloudKit information is covered in the WWDC videos. Here’s a brief description of the videos available and what they cover.

WWDC 2015

What’s New in CloudKit - Session 704
High level overview of working with CloudKit: limits, fees, subscriptions, notifications, and an introduction to the Javascript API

CloudKit Tips and Tricks - Session 715
Descriptions and clarifications of databases
A detailed walkthrough of a CloudKit exchange (emphasis on error handling)
A description of the two types of subscriptions:
• zone subscriptions
• query subscriptions (based on a predicate).
Using CKOperations and batching requests

WWDC 2016

What’s New with CloudKit - Session 226
Improvements to APIs:
• long-live operations
• improved queries across multiple zones
A new Shared database
Overview of zones
Permissions and permission profiles
Deep dive into sharing

CloudKit Best Practices - Session 231
More depth on using CKOperations
Workflow description between client and server
• custom and shared zones
• integration with subscriptions
• remote notifications
• change tokens
Automatic authentication
References between records for one-to-many relationships
More on error handling and query retries

WWDC 2017

Build Better Apps with CloudKit Dashboard - Session 226
Overview of changes to CloudKit dashboard

Tech Talks 2018

GDPR & CloudKit - Session 703

MySQL Table Subqueries

Learning to use table subqueries in MySQL has really helped to advance my understanding of SQL and to demonstrate its versatility. I recently refactored a project to make use of a SQL query to derive the number of available pieces of gear from an inventory database. At the time when someone is making a reservation for gear, they need to know how much of each item is available. The quantity changes as equipment accumulates damage from wear and tear.


Three tables describe the equipment inventory. The EquipTitle table has an entry of each type of gear. The related EquipUnique table has a row representing each piece of gear. For example, we have 20 identical Canon XA10 video cameras. When people make a request for an XA10 camera, they don’t care which of the 20 they receive, the choice is simply “XA10 Camera” (the record in the EquipTitle) and they can tap on it to increase the quantity (up to the maximum number of records in the EquipUnique table). But a complication to the available quantities are service issues that force the staff to pull an item out of circulation. Issues with pieces of gear are tracked in the ServiceIssue table, which maintains a relationship to the EquipUnqiue table. In this simple example, the inventory includes one camera (camera A) and two microphones (mics A and B).

EquipTitle

id name
1 Fancy Camera
2 Cool Microphone


EquipUnique

id EquipTitle_id identifier
1 1 A
2 2 A
3 2 B


ServiceIssue

id EquipUnique_id status desc
1 1 3 Flip out screen is loose
2 3 3 Handle scratched
3 3 5 Power Capsule Broken
4 3 6 Audio has static


Status indicates the severity of an issue. The Fancy Camera has a minor issue that needs to be documented, but doesn’t affect its availability. One of the Cool Microphones (Mic B) is a hot mess with one minor issue and two serious issues (status 5 or higher) that prevent it from being in circulation — a bad power capsule and a loose internal connection that’s causing static.

The result should be the following table rows:

1
2
3
4
id | name | available
----------------------------------
1 | "Fancy Camera" | 1
2 | "Cool Microphone" | 1

The query needs to recognize that the issue attached to the camera should be ignored, there are no issues on Mic A which indicate that it’s fully available and Mic B needs to be evaluated based on the maximum status of its many issues.

Here is the MySQL query I used to derive that result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SELECT ETitle.id,
ETitle.name,
IFNULL(EUnique.available, 0)
FROM EquipTitle AS ETitle
LEFT JOIN (
SELECT EquipTitle_id,
COUNT(id) AS available
FROM EquipUnique
LEFT JOIN(
SELECT EquipUniqueItem_id,
MAX(status) AS maxStatus
FROM ServiceIssue
GROUP by EquipUnique_id
) AS MaxIssue
ON EquipUnique.id = MaxIssue.EquipUnique_id
WHERE maxStatus < 5 OR maxStatus IS NULL
GROUP BY EquipTitle_id
) AS EUnique
ON ETitle.id = EUnique.EquipTitle_id

To work through this, I constructed it in steps. Let’s start with a list of all EquipTitles (returning columns name and id):

1
2
SELECT ETitle.id, ETitle.name
FROM EquipTitle AS ETitle

Now let’s add to that a count of EquipUnique items for each EquipTitle. A simple join to related EquipUniques is going to expand the number of rows to include all records in EquipUniques.

1
2
3
4
SELECT ETitle.id, ETitle.name, EUnique.key_id
FROM EquipTitle AS ETitle
JOIN EquipUnique AS EUnique
ON ETitle.id = EUnique.EquipTitle.id

This is not what I want.

However a subquery that generates a virtual table will do the trick

1
2
3
4
5
6
7
8
9
SELECT ETitle.id, ETitle.name, EUnique.count
FROM EquipTitle AS ETitle
JOIN (
SELECT EquipTitle_id,
COUNT(id) AS count
FROM EquipUnique
GROUP BY EquipTitle_id
) AS EUnique
ON ETitle.id = EUnique.EquipTitle_id

EUnique is now a derived table and the fields SELECTED are available in the outer part of the query. Aliasing the derived table is required.

The final step is limiting the count of EquipUniques to just the ones that are below the “serious” status threshold in service issues. One of the nice things about working with subqueries is that they nest easily. Instead of adding more complexity to the query I’ve go so far, I’ll demonstrate the service issue subquery as a stand-alone query.

1
2
3
4
SELECT EquipUnique_id,
MAX(status) AS maxStatus
FROM ServiceIssue
GROUP by EquipUnique_id

This generates rows of EquipUnique items with the maximum status value from any related ServiceIssue records. The final step is inserting this into our previous query as another JOIN and filtering by the maxStatus value with a WHERE clause. I want to be sure to include gear that has no issues so the WHERE clause allows maxStatus to be NULL as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- SELECT ETitle.id, ETitle.name, EUnique.count
-- FROM EquipTitle AS ETitle
-- JOIN (
-- SELECT EquipTitle_id
-- COUNT(id) AS count
-- FROM EquipUnique
LEFT JOIN(
SELECT EquipUniqueItem_id,
MAX(status) AS maxStatus
FROM ServiceIssue
GROUP by EquipUnique_id
) AS MaxIssue
ON EquipUnique.id = MaxIssue.EquipUnique_id
WHERE maxStatus < 5 OR maxStatus IS NULL
-- GROUP BY EquipTitle_id
-- ) AS EUnique
-- ON ETitle.id = EUnique.EquipTitle_id

As the final touch, we’ll change “count” to “available” to better represent the meaning of the column and add in a default value of 0.

1
2
3
SELECT ETitle.id,
ETitle.name,
IFNULL(EUnique.available, 0)

This is only a demonstration of subqueries used in a JOIN clause. They can be used in other parts of a SQL query. This approach happens to solve the problem I had on my plate and I like how easy it is to reason about when the subquery is so neatly self-contained as a SQL query unto itself.

Looking For Properties On Null and Uncaught Errors In Asynchronous Code

I see a common mistake among students learning JavaScript when they try to write a conditional to test the value of an object’s property but the object itself is null. Instead of testing the condition of the property, the expression throws an error.


Conditional testing for a property on NULL

The assumption is made is that the condition will evaluate as false instead of throwing an error.

1
2
3
4
5
6
const foo = null;
if (foo.bar) {
console.log('if');
} else {
console.log('else');
}

No console logs happen. The following error is thrown:
Uncaught TypeError: Cannot read property 'bar' of null


Testing with NOT operator doesn’t work either

1
2
3
4
const foo = null;
if (!foo.bar) {
console.log('if');
}

No console logs happen. The same error is thrown:
Uncaught TypeError: Cannot read property 'bar' of null


Troubleshooting the problematic code above seems clear enough. The error message is useful, it tells us the cause of the problem. What we also see proven true is the following disclaimer about thrown errors…

From MDN:

Execution of the current function will stop (the statements after throw won’t be executed), and control will be passed to the first catch block in the call stack. If no catch block exists among caller functions, the program will terminate.

Uncaught errors will terminate the program.

The correct way to use a conditional for testing for the property of an object that could be NULL is to include a test for the object as well.

Use Logical AND, progress from parent to child

1
2
3
4
5
6
const foo = null;
if (foo && foo.bar){
console.log('if');
}else{
console.log('else');
}

Successfully logs else;


Or guard against the NULL case by using Logical OR

1
2
3
4
const foo = null;
if (!foo || !foo.bar){
console.log('if');
}

Successfully logs if;


Depending on the scenario, another solution is to catch the error.

Using TRY & CATCH

1
2
3
4
5
6
const foo = null;
try {
console.log(foo.bar);
} catch(err) {
console.log('catch');
}

Successfully logs catch
NOTE: even if you don’t use it, the err variable as the parameter for catch() is NOT optional!


An error that doesn’t appear to stop the execution of code

I think the nature of the problem gets concealed when promises come into play. Instead of an error showing up in the browser, the error will go to the catch block of a promise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const promise = new Promise( (resolve, reject) => {
console.log('inside the promise');
resolve('promise result');
});
promise
.then( result => {
console.log('inside .then() with: ', result);
const foo = null;
if (foo.bar) {
console.log('if');
} else {
console.log('else');
}
// Code after the offending IF doesn't execute
})
.catch( reject => {
console.log('code inside the catch executes');
});

inside the promise
inside .then() with: promise result
code inside the catch executes
Notice that the functions in both .then() and .catch() were invoked. There are no uncaught errors so subsequent code will continue to execute as normal.


Or if a catch block doesn’t exist, an uncaught error is thrown but code will appear to execute after the conditional, potentially hiding the cause of the error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promise = new Promise( (resolve, reject) => {
console.log('inside the promise');
resolve('promise result');
});
promise
.then( result => {
console.log('inside .then() with: ', result);
const foo = null;
if (foo.bar) {
console.log('if');
} else {
console.log('else');
}
// Code after the offending IF doesn't execute
})
console.log('outside the promise');

inside the promise
outside the promise
inside .then() with: promise result
After the logs, the following error is appears in the console:
Uncaught (in promise) TypeError: Cannot read property 'bar' of null

It’s the same error we saw before and it stops the execution of code inside the .then() function, but since it was asynchronous it doesn’t stop the execution of code below the promise invocation. If there is a lot of code following the promise and if the promise is an Ajax call (for example) that takes a sizable amount of time before invoking the .then() function, then the offending error won’t be as easy to track down. If I saw this error message I would search for “bar” and then look at any promises missing catches.

Image Picker with Simple Cropping

Image Picker is the result of a larger project I’ve been working on that needed an interface for allowing a user to select and upload an image to a CloudKit database as part of a web app for maintaining a list of movie screenings. I pulled out the relevant code to make this stand-alone HTML/CSS/JavaScript module.

My task is to give the user an easy way to select an image from their file system. But I need to keep things efficient for the CloudKit database and I have a specific design spec for how the images are displayed in the final product. Users are free to choose any image but the width and height must meet a minimum pixel size and the final image must adhere to a specific aspect ratio. In my case, I want square images at 440px by 440px.

I jumped in and started working with HTML’s <input> tag using the type: file. The input appears as a button with a label that identifies a chosen file. It’s an easy-to-implement feature but it comes with drawbacks. The developer has almost no ability to style the button and label it.

1
<input type="file" name="file" onchange="fileReadHandler(this.files)">

But a button isn’t enough, I also want a drop zone to allow a user to drag and drop an image file. Drag events are another HMTL5 feature that are relatively easy to implement. I added drag event handlers to a drop zone <div>. The ondrop handler passes in an event with a dataTransfer.files property.

1
2
3
4
<div ondragenter="event.stopPropagation(); event.preventDefault();"
ondragover="event.stopPropagation(); event.preventDefault();"
ondrop="event.preventDefault(); event.stopPropagation(); onDrop(event);">
</div>

Conveniently, the ondrop event can piggyback on the file-input onchange event handler function. A newly instantiated fileReader object will process the file as an arrayBuffer.

1
2
3
4
5
6
7
8
9
10
11
12
13
function onDrop(event) {
if (event.dataTransfer.files.length < 1) return;
fileReadHandler(event.dataTransfer.files);
function fileReadHandler(files){}
var fileReader = new FileReader();
fileReader.onloadend = element => {
const imageData = element.target.result;
doStuffWithImageData(imageData);
};
fileReader.readAsArrayBuffer(files[0]);
}

The user needs to be given an opportunity to see the original image as well as the final (cropped) version and make some adjustment to get the best composition from the original. There are a lot of examples online of JavaScript libraries for image cropping and resizing. But in my case they were overkill. I didn’t need the ability to grab and stretch the corners of a resize window or move an overlay in 360 degrees. I just need a simple overlay that conforms to the aspect ratio I desire and gives the user the ability to slide it across their image either up & down or side to side, whichever is the larger dimension.

The overlay slides side to side and highlights the final cropped composition.

Most of my time went into this aspect of the work. It involves two overlapping <canvas> elements (one to hold the selected image and the other to render an overlay box to indicate the cropped composition) and a container <div> to manage the mouse or touch events to move the overlay box.

1
2
3
4
5
6
7
8
9
10
11
12
<div class="l-position-absolute"
onmousedown="event.preventDefault(); event.stopPropagation(); interactionStart(event)"
onmousemove="event.preventDefault(); event.stopPropagation(); interactionMove(event)"
onmouseup="interactionEnd(event)"
onmouseleave="interactionCancel(event)"
ontouchstart="event.preventDefault(); event.stopPropagation(); interactionStart(event)"
ontouchmove="event.preventDefault(); event.stopPropagation(); interactionMove(event)"
ontouchend="interactionEnd(event)"
ontouchcancel="interactionCancel(event)">
<canvas id="canvas-image" class="l-position-absolute canvas-image variable-size"></canvas>
<canvas id="canvas-overlay" class="l-position-absolute canvas-overlay variable-size"></canvas>
</div>

When the image is loaded, the <canvas> and <div> change size to accommodate the aspect ratio of the selected image. A portrait oriented image will increase the height while a landscape oriented image will increase the width of the tags. It requires a final function to read the pixel dimensions from the selected files. No small task as you may know if you’ve ever tried to read the technical spec for evaluating the raw data of a JPEG. There’s a possible EXIF IFD header marker with an optional SubIFD marker and 16 available Start Of Frame markers — each of which may or may not have pixel dimension data that could refer to the actual image or it could refer to the thumbnail image (augghhhh!).

For all the effort the final product seems to have been worth it. The complete HTML/CSS/JavaScript files are at Codepen and GitHub. So far it handles any PNG or JPEG file I throw at it. I’ll add TIFF and GIF formats when I get around to it.

CloudKit JS Sample Code Error

While implementing Apple’s CloudKit Catalog, I discovered an error in the sample code for the saveRecords method under the Records item.

The example demoSaveRecords method takes a lot of possible arguments.

1
2
3
4
5
function demoSaveRecords(
databaseScope,recordName,recordChangeTag,recordType,zoneName,
forRecordName,forRecordChangeTag,publicPermission,ownerRecordName,
participants,parentRecordName,fields,createShortGUID
)

Using these parameters, the demoSaveRecords method implementation constructs a record object and an options argument that are passed to the saveRecords method on CloudKit.Database.

Within the method’s code is this offending block…

1
2
3
4
5
// Convert the fields to the appropriate format.
record.fields = Object.keys(fields).reduce(function(obj,key) {
obj[key] = { value: fields[key] };
return obj;
},{});

The is intended to create a property on the record object using the keys and values of the fields object passed in as a parameter. Presumably, the fields object you pass into the method will look like this example provided in the the CloudKit JS Reference documentation:

1
2
3
4
5
6
7
8
9
10
11
12
var record = {
recordName: '115',
recordType: 'Artwork',
fields: {
title: {
value: 'MacKerricher State Park'
},
address: {
value: 'Fort Bragg, CA'
}
}
};

Likewise, if you were to simply copy the fields property off of an existing record and provide it as the method parameter, this example is exactly how the fields property is constructed.

The problem is that the code block in Apple’s demoSaveRecords function adds an extra nested value: property to the mix. When the CloudKit.Database saveRecords method is expecting:

1
2
3
4
5
fields: {
title: {
value: 'MacKerricher State Park'
}
}

Instead it gets:

1
2
3
4
5
6
7
fields: {
title: {
value: {
value: 'MacKerricher State Park'
}
}
}

The evidence is a 400 Bad Request error that likely reads:
missing required field ‘recordName’
or
Invalid value, expected type STRING but actual value was object of class com.apple.cloudkit.ws.application.common.data.UnknownTypeModel

The fix is to simply to replace…
obj[key] = { value: fields[key] };
with
obj[key] = fields[key];