Get The Plugin

If you don't already have it, get the Coronium SkyTable Plugin from the Corona Marketplace.

Adding The Plugin

Add the plugin by adding an entry to the plugins table of build.settings file:

settings =
{
    plugins =
    {
        ["plugin.skytable"] =
        {
            publisherId = "com.develephant"
        },
    },
}

You're now ready to use the Coronium SkyTable plugin.

Getting Started

A SkyTable is a user scoped data table. This means the data is tied to a single user. It is less of a database, and more of a secure per user table datastore.

Important

Make sure you understand how Coronium SkyTable works before commiting to using it in your project. You cannot access any other data than the user provided credentials allow.

While a SkyTable is tied to a specific user, you can create as many specific, per user, SkyTables as you need for a given application (See open).

Example of basic usage:

--Require plugin
local skytable = require("plugin.skytable")

--Initialize SkyTable
skytable:init({
  user = "<user-email>",
  password = "<user-password>",
  base = "app1",
  key = "<server-key>",
  host = "http://<server-host>:7173",
  debug = true
})

--Open a "profile" SkyTable
local profile = skytable:open("profile")

--Get action listener
local function onResult(evt)
  if evt.isError then
    print(evt.error)
  else
    print(evt.data.name) -- Jimmy
  end
end

--Set action listener
local function onSetResult(evt)
  if evt.isError then
    print(evt.error)
  else
    if evt.success then
      print('saved')
    end
  end
end

--Setting data
local function setData()
  --Set action
  profile:set({name="Jimmy", age=23}, onSetResult)
end

--Getting data
local function getData()
  --Get action
  profile:get(onResult)
end

--Set the data
setData()

--OR, get the data
--getData()

User Scope

A SkyTable, and its data, is scoped per user. When first initalizing the SkyTable client, you must provide a username (usually an email address), and a password. These values are the responsibility of the developer to gather.

Note

The username and password are never stored on the SkyTable server. Instead a unique key is generated to identify the user.

The username and password are passed to the init API method:

local skytable = require("plugin.skytable")

skytable:init({
  user = "user@email.com",
  password = "<user-password>",
  ...
})

This feature allows your user to run your application on different devices, and have their data "synced" between sessions.

See the init API method for more details.

Warning

If a username and/or password are changed, it is the responsibilty of the developer to delete the old data, and initalize the new tables using the updated username and password.


Base Scope

You can also assign a base scope to the SkyTable. This allows the saving of different data tables, per user, for multiple applications. A user can use the same username and password across all of your different applications.

Tip

Even if you don't think you will have the same users on different applications, it is a best practice to assign a base scope anyway. This future-proofs the SkyTable data.

The base scope is assigned in the init Api method:

local skytable = require("plugin.skytable")

skytable:init({
  user = "user@email.com",
  password = "<user-password>",
  base = "app1",
  ...
})

Now when we want to access data from a different app (in this case "app2"), for the same user, we can do:

local skytable = require("plugin.skytable")

skytable:init({
  user = "user@email.com",
  password = "<user-password>",
  base = "app2", --different base scope
  ...
})

See the init API method for more details.


Listeners

Because network requests are asynchronus actions, listeners must be supplied to all SkyTable API calls. The listener is where you will recieve the response from the SkyTable server.

There are two different event types returned from a SkyTable server. One shaped for the set call, and another for the remaining value based API calls; get, delete, and keys.

While you can use one listener for all the API calls, in general its easier to create two different listeners:

local profile = skytable:open("profile")

--Set
local function onSetResult(evt)
  if evt.isError then
    print(evt.error)
  else
    print(evt.success)
  end
end

--Get, Delete, Keys
local function onResult(evt)
  if evt.isError then
    print(evt.error)
  else
    local data = evt.data --String, Number, Boolean, Table, or nil
  end
end

--Set something
profile:set("name", "Marco", onSetResult)

--Get something
profile:get("address.street", onResult)

The main difference between the events are the properties available.

Set event properties:

Name Description Type
isError An error has occured. Boolean
error A descriptive error string. String
success A flag noting a successful set action. Boolean
user The user key of the calling client. String

Note

If set, the success flag will almost always be true. An unsuccessful call will usually be propagated to the error property.

Get, Delete, Keys event properties:

Name Description Type
isError An error has occurred. Boolean
error A descriptive error string. String
data The returned data. String, Number, Boolean, or Table
user The user key of the calling client. String

Note

The data property can potentially be nil.

Best practices

While you can set up general listeners, it's best to assign a specific listener per action:

local profile = skytable:open("profile")

local onSetName(evt)
  if evt.success then
    print('name saved')
  end
end

local onSetColors(evt)
  if evt.success then
    print('colors saved')
  end
end

profile:set("name", "Dave", onSetName)
profile:set("colors", {"red","green","blue"}, onSetColors)

This is the recommended approach for the other value based API calls; get, delete, and keys.

Another option is using Tags.


Data Paths

Data paths allow you to "path" to a value in the SkyTable, both to set and get the underlying value. Data paths can be used in all of the value based API calls. This includes get, set, delete, and keys.

When calling any of the value methods without a data path, a "root" path is implied. For example, let's assume the following data exists in a "profile" SkyTable:

{
  name = "Jim",
  age = 34,
  active = true,
  address = {
    street = "123 Main St.",
    city = "San Diego",
    state = "CA",
    zip = 92037
  },
  colors = {"red", "green", "blue"}
}

Pathing with Get

Then following call brings back the entire SkyTable (its "root" path):

local profile = skytable:open("profile")

local function onResult(evt)
  local data = evt.data --the entire data table
  print(data.name) -- Jim
end

profile:get(onResult)

To gain access to an individual value in the SkyTable, we can supply a path:

local profile = skytable:open("profile")

local function onResult(evt)
  local data = evt.data --holds "name" value
  print(data) -- Jim
end

profile:get("name", onResult)

To dive even deeper into the SkyTable, just path to the key, using a "dot" separator:

local profile = skytable:open("profile")

local function onResult(evt)
  local data = evt.data --holds "city" value
  print(data) -- San Diego
end

profile:get("address.city", onResult)

If we wanted the entire "address" table:

local profile = skytable:open("profile")

local function onResult(evt)
  local data = evt.data --entire address table
  print(address.state) -- CA
end

profile:get("address", onResult)

Or the "colors" table array:

local profile = skytable:open("profile")

local function onResult(evt)
  local data = evt.data --entire colors array
  for i=1, #data.colors  do
    print(data.colors[i]) --red, green, blue
  end
end

profile:get("colors", onResult)

You can path as far as you need:

gear:get("set1.arms.def", onResult)

Pathing with Set

When using paths with the set API call, the usage outlined above stays the same, with some implementation differences when setting a "root" path.

When first populating a SkyTable, you must pass it a data table to start with. This can be as little as one property, or a whole predefined table. To create the inital SkyTable we do like so:

local profile = skytable:open("profile")

local function onSetResult(evt)
  if evt.success then
    print('saved')
  end
end

local data_tbl = 
{
  name = "Jim"
}

profile:set(data_tbl, onSetResult)

Once we have a data table saved to a SkyTable, we can set values on it at a later time by using a path:

local profile = skytable:open("profile")

local function onSetResult(evt)
  if evt.success then
    print('saved')
  end
end

profile:set("name", "Sally", onSetResult)

To add an "address" data table to our SkyTable, we can do:

local profile = skytable:open("profile")

local address = {
  street = "123 Main St.",
  city = "San Diego",
  state = "CA",
  zip = 92037
}

local function onSetResult(evt)
  if evt.success then
    print('saved')
  end
end

profile:set("address", address, onSetResult)

And to change a value in the "address" data table:

local profile = skytable:open("profile")

local function onSetResult(evt)
  if evt.success then
    print('saved')
  end
end

profile:set("address.city", "San Francisco", onSetResult)

Or adding an array table:

local profile = skytable:open("profile")

local function onSetResult(evt)
  if evt.success then
    print('saved')
  end
end

profile:set("colors", {"red","green","blue"}, onSetResult)

Important

Once the "root" data table has been set for a SkyTable, you cannot overwrite it without specifying a flag.

To overwrite a "root" data table in a SkyTable, you must pass a special flag. This is not required when creating the initial data.

profile:set(newDataTable, onSetResult, {flag="XX"})

This helps prevent overwriting your "root" data table by accident. Flags can also be used with data paths, but the pathed value will be overwritten on any set call.

See Flags for more details.


Tags

When using a general listener (see Listeners), you can mark an API call with a tag to filter the response.

A tag is added to a call using the options parameter, which takes a table:

local profile = skytable:open("profile")

local function onResult(evt)
  if not evt.isError then
    print(evt.error)
  else
    local data = evt.data
    if evt.tag == "get-name" then
      print(data) -- name string
    elseif evt.tag == "get-colors" then
      print(data[1]) -- colors table array
    end
  end
end

profile:get("name", onResult, {tag="get-name"})
profile:get("colors", onResult, {tag="get-colors"})

You can also use this method to create one main generalized response listener for both the set call, as well as get, delete, and keys:

local profile = skytable:open("profile")

local function onResult(evt)
  if not evt.isError then
    print(evt.error)
  else
    if evt.tag == "set-name" then
      if evt.success then
        print('name saved')
      end
    elseif evt.tag == "get-colors" then
      print(evt.data[1]) -- colors table array
    end
  end
end

profile:set("name", "Sam", onResult, {tag="set-name"})
profile:get("colors", onResult, {tag="get-colors"})

Flags

When using the set API call, you can supply a flag to the options parameter, as a table.

Flag Types

Name Description Type
NX Set the path value only if the path does not exist. String
XX Set the path value only if the path does exist. String
local profile = skytable:open("profile")

local function onSetResult(evt)
  if evt.success then
    print('saved')
  end
end

--set the name only if the path exists.
profile:set("name", onSetResult, {flag="XX"})

Note

When calling a set without a flag, the value will be created if the path does not already exists, or replaced if it does.


Expiry

When using the set API call, you can also supply an expiry to "expire" the SkyTable at a specified time.

Warning

Once a SkyTable has been removed via an expiry, it is no longer accessible.

Expiry Properties

Name Description Type
seconds The seconds until the SkyTable will be removed. Number
local profile = skytable:open("profile")

local onSetResult(evt)
  if evt.success then
    print('saved', 'expiry:', tostring(evt.expiry))
  end
end

--remove the SkyTable in 5 minutes
profile:set(data_tbl, onSetResult, {expiry=300})

Note

When a path is updated using the set API call, the expiry will be reset to its initial value. For example, if you set a value after 2 minutes on a 5 minute exipry, the expiry will be reset to 5 minutes.

Tip

To automatically clean out inactive SkyTables, set a high expiry value.


Delete

The delete API call will remove a value at the given path.

Warning

This method should be used with caution. When operating on the "root" path, the entire data table will be cleared, and the path will no longer exist. Unlike set, with its flag protection, a delete method will do its job as long as the path exists.

To delete a value at the specified path:

local profile = skytable:open("profile")

local function onResult(evt)
  if not evt.isError then
    if evt.data > 0 then
      print('name deleted from profile')
    end
  end
end

profile:delete("name", onResult)

Note

A delete event will contain a data property with a number noting the value paths removed. Most often this will be a one (1). In the case of a failed attempt, or invalid path, the data will contain the number zero (0).


Keys

The keys API call will return a tables keys in the SkyTable at the given path. Using our "profile" example, we can return all the keys at the "root" path like so:

local profile = skytable:open("profile")

local function onResult(evt)
  if not evt.isError then
    local keys = evt.data
    --keys = {"name","age","active","address","colors"}
  end
end

profile:keys(onResult)

Using a path to a nested table:

local profile = skytable:open("profile")

local function onResult(evt)
  if not evt.isError then
    local keys = evt.data
    --keys = {"street","city","state","zip"}
  end
end

profile:keys("address", onResult)

Note

You can only access keys from data tables.


Syncing Data

With the use of data paths (see Data Paths), you can get and set values from different levels of the SkyTable.

As a best practice, and to keep your data synced between client and server, it is best to create or cache a local data table first, make changes to that table, and then push it to the server. This helps keep your data in sync easier.

local profile_data = {}
local profile = skytable:open("profile")

local function onSetProfile(evt)
  if evt.isError then
    print(evt.error)
  else
    if evt.success then
      print('profile saved')
    end
  end
end

local function onGetProfile(evt)
  if evt.isError then
    print(evt.error)
  else
    --check if we have existing data
    if evt.data ~= nil then
      profile_data = evt.data
    else
      --there is no data table yet, create a default one
      profile_data = {
        name = "Tammy",
        age = 45
      }
      profile:set(profile_data, onSetProfile)
    end
  end
end

profile:get(onGetProfile)

Once you have the local data table, use it to manage the data, and push it back to the server after any changes:

profile_data.active = true
profile_data.colors = {"red","green","blue"}

profile:set(profile_data, onSetProfile, {flag="XX"})

Note

When replacing a "root" value (as above), you must specify the "XX" flag. See Flags.