Tana
This gem is a monadic API client for the Tana Personal Knowledge Management (PKM) system. This allows you to build more sophisticated workflows atop the Input API using a design which leverages Function Composition for a powerful, fault tolerant, workflow.
With this gem, you have a convenient building block to automate your workflows or even use this gem to transfer data from other PKM systems into Tana. 🎉
- Features
- Requirements
- Setup
- Usage
- Initialization
- Environment
- Identification
- Endpoints
- Add
- Development
- Tests
- License
- Security
- Code of Conduct
- Contributions
- Developer Certificate of Origin
- Versions
- Community
- Credits
Features
-
Provides an API client which implements Tana’s limited, early access Input API.
-
Provides HTTP request and response verification using Dry Schema and Dry Validation.
-
Uses Function Composition — coupled with Pipeable — to process each HTTP request and response.
Setup
To install with security, run:
# đź’ˇ Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install tana --trust-policy HighSecurity
To install without security, run:
gem install tana
You can also add the gem directly to your project:
bundle add tana
Once the gem is installed, you only need to require it:
require "tana"
Usage
All interaction is via a Tana
instance.
Initialization
You can initialize the API client — using the defaults as described in the Environment section below — as follows:
client = Tana.new
Further customization can be done via a block:
client = Tana.new do |config|
config.accept = "application/json" # Use custom HTTP header.
config.token = "abc123" # Use custom/personal API key.
config.url = "https://api.tana.inc" # Use custom API root.
end
Environment
Environment variable support can be managed using direnv. These are the defaults:
TANA_API_ACCEPT=application/json
TANA_API_TOKEN=
TANA_API_URL=https://europe-west1-tagr-prod.cloudfunctions.net
You must provide a TANA_API_TOKEN
value to make authenticated API requests. This can be done by creating an API token via the API tokens UI:
Here are the steps to view and make use of the UI shown above:
-
Click on Settings within your Tana client.
-
Click on API tokens.
-
Click
Create
if you haven’t already. -
Copy and paste your API token as value for
TANA_API_TOKEN
key. -
Refresh your environment accordingly.
Identification
When making API requests, you’ll often need to acquire the IDs of the nodes you wish to add. This can be done two ways. The first is by selecting the node you are interested in, using CONTROL + K
to launch the command prompt, and fuzzy type for Copy link. Example:
Once copied the URL might look like https://app.tana.inc?nodeid=z-p8LdQk6I76
but you’ll only need the ID (i.e. z-p8LdQk6I76
) for API requests.
For supertags/fields, you can select the node you are interested in using CONTROL + K
to launch the command prompt and fuzzy type for Show API schema. Example:
Endpoints
At the moment, Tana only provides the Input API which is a single endpoint for adding nodes only. This API has the following limitations:
-
Rate Limiting
-
One call per second per token.
-
Max 100 nodes created per call.
-
Max 5,000 characters in one request.
-
-
Additional Limitations
-
Can’t target a relative Today node.
-
Must know the IDs of the supertag.
-
Each payload is capped at five kilobytes.
-
Can’t add a checkbox child to a normal node.
-
No support for child templates.
-
No support for in-application links (i.e. anything that is not a http/https scheme).
-
Add
To add nodes (i.e. Input API), you only need to send the #add
message. Here’s a quick example of adding a simple node to your Tana Inbox.
client = Tana.new
result = client.add(
{
targetNodeId: "INBOX",
nodes: [
{
name: "With plain node",
description: "A demonstration."
}
]
}
)
result
# Success(#<data Tana::Models::Root children=[#<data Tana::Models::Node id="agite1C3Tben", name="With plain node", description="A demonstration.", type="node", children=[]>]>)
The above will yield the following in your Tana Inbox:
You’ll also notice, the result is a monad (i.e. Dry Monads) which means you’ll only get a Success
or Failure
which you can pipe with additional functionality or use Pattern Matching.
For successes, you’ll be given a Data object with a simple Object API for accessing the children of the response. At the root, you’ll have a Tana::Models::Root
instance which can be one or more Tana::Models::Node
children. When you unpack the Success
 — and to illustrate further — you’ll end up with the following:
Tana::Models::Root[
children: [
Tana::Models::Node[
id: "agite1C3Tben",
name: "With plain node",
description: "A demonstration.",
type: "node",
children: []
]
]
]
This simplifies and reduces the amount of work you have to do in your own program when processing the API result. For a Failure
, you either get a HTTP::Response
or a structured response that is a plain Hash
. Example:
{
"formErrors" => ["Invalid input"],
"fieldErrors" => {}
}
Usually, errors are due to invalid authentication credentials or wrong data format. To experiment further, you can use this Bundler Inline script:
#! /usr/bin/env ruby
# frozen_string_literal: true
# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.
require "bundler/inline"
gemfile true do
source "https://rubygems.org"
gem "amazing_print"
gem "debug"
gem "tana"
end
require "base64"
include Dry::Monads[:result]
render = lambda do |result|
case result
in Success(record) then puts record
in Failure(HTTP::Response => error) then puts error.body
in Failure(error) then ap error.errors
else abort "Unable to process result."
end
end
client = Tana.new
When you save the above and run it locally, you have a quick way to experiment with the API print out the results by using the render
function which uses Pattern Matching that I hinted at earlier. The following are additional examples you can experiment with by adding to the above script:
With Nesting
The following will allow you to create a deeply nested set of nodes. At the moment, your are limited to ten levels deep due to recursion limitations with the Dry Schema and Dry Validation gems.
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
name: "One",
children: [
{
name: "Two",
children: [
{
name: "Three",
children: [
{
name: "Four",
children: [
{
name: "Five",
children: [
{
name: "Six",
children: [
{
name: "Seven",
children: [
{
name: "Eight",
children: [
name: "Nine",
children: [
{name: "Ten"}
]
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
)
With Field
The following allows you to create a node with a field (you’ll want to replace the attribute ID with your ID).
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
name: "With field",
description: "A demonstration.",
children: [
{
attributeId: "zM582yzfcs-q",
type: "field",
children: [
{name: "đź’ˇ Idea"}
]
}
]
}
]
}
)
With Supertags
The following allows you to create a node with supertags (you’ll want to replace the IDs with your own IDs).
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
name: "With supertags",
description: "A demonstration.",
supertags: [
{id: "S9aMn7puHzUT"},
{id: "iWKs80kHI0SK"}
]
}
]
}
)
With Reference
The following will allow you to create a node with a reference to another node (you’ll want to replace with your own ID):
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
dataType: "reference",
id: "H-vAUdPi8taR"
}
]
}
)
With Date
The following will allow you to create a node with a date:
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
dataType: "date",
name: "2024-01-01T00:00:00Z"
}
]
}
)
With URL
The following will allow you to create a node with a URL field (you’ll want to replace with your own ID):
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
type: "field",
attributeId: "OceDtN8c0CbR",
children: [
{
dataType: "url",
name: "https://alchemists.io"
}
]
}
]
}
)
With Checkbox
The following will allow you to create a node with a checkbox:
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
name: "With checkbox",
dataType: "boolean",
value: true
}
]
}
)
With Attachment
The following will allow you to create a node with an attachment. This requires the Base64 gem as shown required in the script above because you need to encode your attachment before making the API request.
render.call client.add(
{
targetNodeId: "INBOX",
nodes: [
{
dataType: "file",
file: Base64.strict_encode64(Pathname("sunglasses.jpeg").read),
filename: "sunglasses.jpeg",
contentType: "image/jpeg"
}
]
}
)
With Schema Field
The following will allow you to create a Schema field:
render.call client.add(
{
targetNodeId: "SCHEMA",
nodes: [
{
name: "demo",
description: "With Schema field.",
supertags: [{id: "SYS_T02"}]
}
]
}
)
With Schema Supertag
The following will allow you to create a Schema supertag:
render.call client.add(
{
targetNodeId: "SCHEMA",
nodes: [
{
name: "demo",
description: "With Schema supertag.",
supertags: [{id: "SYS_T01"}]
}
]
}
)
Development
To contribute, run:
git clone https://github.com/bkuhlmann/tana
cd tana
bin/setup
You can also use the IRB console for direct access to all objects:
bin/console
Tests
To test, run:
bin/rake
Credits
-
Built with Gemsmith.
-
Engineered by Brooke Kuhlmann.