Current section
Files
Jump to
Current section
Files
lib/cloud_kit.ex
defmodule CloudKit do
@moduledoc """
Elixir client for the [Apple CloudKit API](https://developer.apple.com/documentation/cloudkit/).
The public surface is intentionally small:
CloudKit.query_records(%{recordType: "Items"})
CloudKit.fetch_record("record_name")
CloudKit.create_record(%{recordType: "Items", fields: %{...}})
CloudKit.update_record("record_name", %{fields: %{...}})
CloudKit.delete_record("record_name")
CloudKit.upload_asset("file_path", "record_type", "field_name")
CloudKit.token()
## Configuration
config :cloud_kit,
container_id: System.get_env("CLOUDKIT_CONTAINER_ID"),
environment: "development", # or "production"
server_key_id: System.get_env("CLOUDKIT_SERVER_KEY_ID"),
private_key: System.get_env("CLOUDKIT_PRIVATE_KEY"),
base_url: "https://api.apple-cloudkit.com"
Every function also accepts per-call `opts` that override the application config.
"""
alias CloudKit.{Asset, Client, ServerKey}
@type opts :: keyword()
@type response :: {:ok, map()} | {:error, term()}
@doc "Return a cached-per-call CloudKit **access token** (after the JWT → token exchange)."
@spec token(opts) :: {:ok, String.t()} | {:error, term()}
def token(opts \\ []), do: ServerKey.access_token(opts)
@doc """
Query records from the CloudKit database.
## Parameters
- `query`: Map containing:
- `recordType` - The record type to query
- `filterBy` - Optional filter specification
- `sortBy` - Optional sort specification
- `limit` - Maximum records to return (default 200)
- `desiredKeys` - Optional list of field names to return
- `opts`:
- `:zone_id` - Zone ID for record (defaults to default zone)
## Examples
CloudKit.query_records(%{
recordType: "Items",
filterBy: [%{fieldName: "name", comparator: "EQUALS", fieldValue: %{value: "Test"}}],
limit: 50
})
"""
@spec query_records(map(), opts) :: {:ok, map()} | {:error, term()}
def query_records(query, opts \\ []) when is_map(query) do
Client.post("/public/records/query", %{query: query}, opts)
end
@doc """
Fetch a single record by record name.
## Parameters
- `record_name`: The unique record name
- `opts`:
- `:record_type` - Optional record type
- `:zone_id` - Zone ID for record
- `:desired_keys` - List of field names to return
"""
@spec fetch_record(String.t(), opts) :: response
def fetch_record(record_name, opts \\ []) when is_binary(record_name) do
body = %{
records: [%{recordName: record_name}]
}
body =
if record_type = Keyword.get(opts, :record_type) do
put_in(body, [:records, Access.at(0), :recordType], record_type)
else
body
end
Client.post("/public/records/lookup", body, opts)
end
@doc """
Create a new record.
## Parameters
- `record`: Map containing:
- `recordType` - The record type
- `recordName` - Optional unique record name (generated if not provided)
- `fields` - Map of field name to value
- `opts`:
- `:zone_id` - Zone ID for record
## Examples
CloudKit.create_record(%{
recordType: "Items",
fields: %{
name: %{value: "New Item"},
price: %{value: 99.99, type: "DOUBLE"}
}
})
"""
@spec create_record(map(), opts) :: response
def create_record(record, opts \\ []) when is_map(record) do
Client.post(
"/public/records/modify",
%{operations: [%{action: "CREATE", record: record}]},
opts
)
end
@doc """
Update an existing record.
## Parameters
- `record_name`: The unique record name
- `updates`: Map of fields to update
- `opts`:
- `:record_type` - The record type
- `:zone_id` - Zone ID for record
## Examples
CloudKit.update_record("record_123", %{
fields: %{
price: %{value: 149.99, type: "DOUBLE"}
}
})
"""
@spec update_record(String.t(), map(), opts) :: response
def update_record(record_name, updates, opts \\ [])
when is_binary(record_name) and is_map(updates) do
record = Map.merge(updates, %{recordName: record_name})
record =
if type = Keyword.get(opts, :record_type),
do: Map.put(record, :recordType, type),
else: record
Client.post(
"/public/records/modify",
%{operations: [%{action: "UPDATE", record: record}]},
opts
)
end
@doc """
Delete a record by record name.
## Parameters
- `record_name`: The unique record name
- `opts`:
- `:record_type` - The record type
- `:zone_id` - Zone ID for record
"""
@spec delete_record(String.t(), opts) :: :ok | {:error, term()}
def delete_record(record_name, opts \\ []) when is_binary(record_name) do
record = %{recordName: record_name}
record =
if type = Keyword.get(opts, :record_type),
do: Map.put(record, :recordType, type),
else: record
case Client.post(
"/public/records/modify",
%{operations: [%{action: "DELETE", record: record}]},
opts
) do
{:ok, _} -> :ok
error -> error
end
end
@doc """
Upload an asset to CloudKit.
## Parameters
- `file_path`: Path to the file to upload
- `record_type`: The record type that will use this asset
- `field_name`: The field name in the record
- `opts`:
- `:zone_id` - Zone ID
- `:record_name` - Optional record name to associate
## Examples
CloudKit.upload_asset("/path/to/image.png", "Items", "image")
"""
@spec upload_asset(String.t(), String.t(), String.t(), opts) :: response
def upload_asset(file_path, record_type, field_name, opts \\ [])
when is_binary(file_path) and is_binary(record_type) and is_binary(field_name) do
Asset.upload(file_path, record_type, field_name, opts)
end
@doc """
Download an asset from CloudKit.
## Parameters
- `download_url`: The download URL from an asset field
- `opts`: Download options
## Examples
{:ok, content} = CloudKit.download_asset(record["fields"]["image"]["downloadURL"])
"""
@spec download_asset(String.t(), opts) :: {:ok, binary()} | {:error, term()}
def download_asset(download_url, opts \\ []) when is_binary(download_url) do
Asset.download(download_url, opts)
end
@doc """
List zones in the database.
## Parameters
- `opts`: Optional configuration overrides
"""
@spec list_zones(opts) :: {:ok, list(map())} | {:error, term()}
def list_zones(opts \\ []) do
case Client.post("/public/zones/list", %{}, opts) do
{:ok, %{"zones" => zones}} -> {:ok, zones}
{:ok, other} -> {:ok, other}
error -> error
end
end
@doc """
Create a custom zone.
## Parameters
- `zone_name`: The zone name
- `opts`: Optional configuration
"""
@spec create_zone(String.t(), opts) :: response
def create_zone(zone_name, opts \\ []) when is_binary(zone_name) do
Client.post(
"/public/zones/modify",
%{
operations: [%{action: "CREATE", zone: %{zoneID: %{zoneName: zone_name}}}]
},
opts
)
end
@doc """
Delete a zone.
## Parameters
- `zone_name`: The zone name to delete
- `opts`: Optional configuration
"""
@spec delete_zone(String.t(), opts) :: :ok | {:error, term()}
def delete_zone(zone_name, opts \\ []) when is_binary(zone_name) do
case Client.post(
"/public/zones/modify",
%{
operations: [%{action: "DELETE", zone: %{zoneID: %{zoneName: zone_name}}}]
},
opts
) do
{:ok, _} -> :ok
error -> error
end
end
@doc """
Create a subscription for change notifications.
## Parameters
- `subscription`: Map containing:
- `subscriptionType` - Type ("query" or "zone")
- `recordType` - For query subscriptions, the record type
- `filters` - Optional filters
- `notification` - Notification configuration
- `opts`: Optional configuration
## Examples
CloudKit.create_subscription(%{
subscriptionType: "query",
recordType: "Items",
notification: %{
alertBody: "A new item was created"
}
})
"""
@spec create_subscription(map(), opts) :: response
def create_subscription(subscription, opts \\ []) when is_map(subscription) do
Client.post(
"/public/subscriptions/modify",
%{
operations: [%{action: "CREATE", subscription: subscription}]
},
opts
)
end
@doc """
List active subscriptions.
## Parameters
- `opts`: Optional configuration
"""
@spec list_subscriptions(opts) :: {:ok, list(map())} | {:error, term()}
def list_subscriptions(opts \\ []) do
case Client.post("/public/subscriptions/list", %{}, opts) do
{:ok, %{"subscriptions" => subs}} -> {:ok, subs}
{:ok, other} -> {:ok, other}
error -> error
end
end
@doc """
Delete a subscription.
## Parameters
- `subscription_id`: The subscription ID to delete
- `opts`: Optional configuration
"""
@spec delete_subscription(String.t(), opts) :: :ok | {:error, term()}
def delete_subscription(subscription_id, opts \\ []) when is_binary(subscription_id) do
case Client.post(
"/public/subscriptions/modify",
%{
operations: [
%{action: "DELETE", subscription: %{subscriptionID: subscription_id}}
]
},
opts
) do
{:ok, _} -> :ok
error -> error
end
end
end