Server Library
Process workspace data by using Pine's API client.
The @pinecards/server
library provides an easy way to interact with the Pine API in a type-safe manner. To get started, you'll need to install the library in your project with your preferred package manager:
npm install @pinecards/server
You can then import the PineClient
class and construct it with your authorization token:
import { PineClient } from "@pinecards/server";
const client = new PineClient({ accessToken: "YOUR_TOKEN" });
The PineClient
uses tRPC under the hood to make network requests. As a result, this requires explicitly denoting whether a certain operation is a query or a mutation.
client.cards.list.query({ where: { limit: 500 } });
Queries
Read queries are the simplest operations as they only require a valid id
as part of their where
argument:
const deck = await client.decks.read.query({ where: { id: "..." } });
const card = await client.cards.read.query({ where: { id: "..." } });
List queries rely on cursor-based pagination and can thus optionally take a limit
(min 1, max 500) and a cursor
argument:
const response = await client.decks.list.query({ where: { limit: 500 } });
if (response.cursor) {
const { data } = await client.decks.list.query({
where: { cursor: response.cursor }
});
}
Mutations
Create mutations take data
inputs that depend on the model that is being operated on:
Decks require a
title
argument that accepts an input array that conforms to Pine's inline text editor.Cards require a
title
andbody
argument that conforms to Pine's block text editor.Associations (comments, etc..) require a
body
argument that conforms to Pine's block text editor and awhere
argument for specifying the parent model to which the association should be added.Connections (links, backlinks, etc..) require a
body
argument that conforms to Pine's block text editor and awhere
argument for specifying the parent model to which the connection should be added.
// create a deck with bolded inline text
const deck = await client.decks.create.mutate({
data: {
title: [
{
type: "text",
text: { text: "Example text" },
marks: [{ type: "bold" }]
}
]
}
});
// create a card with block elements that have inline text
const card = await client.cards.create.mutate({
data: {
title: [
{
type: "heading",
heading: { color: "gray" },
content: [{ type: "text", text: { text: "Question" } }]
}
],
body: [
{
type: "paragraph",
paragraph: { color: "gray" },
content: [{ type: "text", text: { text: "Answer" } }]
}
]
}
});
// create an association with a paragraph that indents another paragraph
const association = await client.cards.associations.create.mutate({
where: { parent: { id: card.id } },
data: {
body: [
{
type: "paragraph",
paragraph: { color: "gray" },
children: [{ type: "paragraph", paragraph: { color: "gray" } }]
}
]
}
});
// create a connection with a paragraph that indents another paragraph
const connection = await client.cards.connections.create.mutate({
where: { parent: { id: card.id } },
data: {
body: [
{
type: "paragraph",
paragraph: { color: "gray" },
children: [{ type: "paragraph", paragraph: { color: "gray" } }]
}
]
}
});
Update mutations are similar, except they require a where
argument and optional data
:
// update the target deck
const deck = await client.decks.update.mutate({
where: { id: "..." },
data: {}
});
// update the target card
const card = await client.cards.update.mutate({
where: { id: "..." },
data: {}
});
// update the target association
const association = await client.cards.associations.update.mutate({
where: { id: "...", parent: { id: "..." } },
data: {}
});
// update the target connection
const connection = await client.cards.connections.update.mutate({
where: { id: "...", parent: { id: "..." } },
data: {}
});
Delete mutations only require a where
argument:
await client.decks.delete.mutate({
where: { id: "..." }
});
await client.cards.delete.mutate({
where: { id: "..." }
});
await client.cards.associations.delete.mutate({
where: { id: "...", parent: { id: "..." } }
});
await client.cards.connections.delete.mutate({
where: { id: "...", parent: { id: "..." } }
});
Fields
The Fields API allows you to query a workspace's configured fields. This will return an object/dictionary data structure with the appropriate field type matching the corresponding value type:
text
string
number
number
switch
boolean
date
string
Fields that haven't been assigned a value will return a null
value.
You can retrieve the value of a configured number field as follows:
const fields = await client.fields.list({ where: {} });
const value = fields.data.find(field => field.id === "ID_FROM_UI")
Webhooks
Pine provides webhooks for Deck
and Card
events, allowing you to listen to any workspace changes that affect these models.
Webhook events are sent to a publicly accessible HTTPS URL via a HTTP POST
request. The POST
request expects a HTTP 200
status code in response and will be retried only once if it fails to receive it.
The @pinecards/server
library exports a PineWebhooks
class for securely constructing a webhook payload. Here's a simple demonstration using the express
library:
import express from "express";
import bodyParser from "body-parser";
import { PineWebhooks } from "@pinecards/server";
const app = express();
const webhooks = new PineWebhooks({ secret: "SIGNING_SECRET" });
app.use(
bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
}
})
);
app.post("/", (req, res) => {
const event = webhooks.construct(req.rawBody, req.headers["pine-signature"]);
// ... do something with data ...
res.sendStatus(200);
});
app.listen(3001, () => {
console.log(`Webhook server is running at http://localhost:3001`);
});
Under the hood, Pine verifies that the signature contained in req.headers["pine-signature"]
matches the signature that is constructed from the raw body:
const constructedSignature = crypto
.createHmac("sha256", inputSecret)
.update(rawBody)
.digest("hex");
if (constructedSignature !== inputSignature) {
throw new TRPCClientError("Invalid webhook signature");
}
The event that is returned from the construct
function contains the following fields:
id
The unique identifier for the webhook event.
action
The create
, update
, or delete
action.
data
The Deck
or Card
data that changed.
webhookTimestamp
Timestamp of when the webhook was sent to help guard against replay attacks.
Last updated