Guides

Using the JavaScript SDK

The BAQ JavaScript SDK provides abstractions and helpers that make it easier to work with BAQ records instead of directly calling the HTTP API­.

The BAQ JavaScript SDK is distributed as an NPM package.

npm install @baqhub/sdk

The schemas for record types are expressed using io-ts­ to enable client-side validation of the data returned by servers and persisted locally.

The recommended way to define record types is to let the BAQ CLI­ do it automatically based on a record type JSON definition. But it’s also possible to do it by hand.

In this example, we define a task record type with a title and completion status.

import {IO, SchemaIO, RecordType, Record} from "@baqhub/sdk";

// Define the schema for the record content.
const RTaskRecordContent = SchemaIO.object({
  title: SchemaIO.string({maxLength: 128}),
  completed: SchemaIO.boolean(),
});

// Define the record type.
// These must match the published record type.
const [taskRecordType, RTaskRecordType] = RecordType.full(
  "baqtodotypes.baq.run",
  "8f37b56913fc4e739bc2f983b7eb70dd",
  "37463fd745d448068d0558a3225a26aa132626779b934f919b2e2f9757a38490",
  RTaskRecordContent
);

// Combine content schema and type to define the record.
const RTaskRecord = Record.io(
  taskRecordType,
  RTaskRecordType,
  RTaskRecordContent
);

// Export the record type object and type.
export interface TaskRecord extends IO.TypeOf<typeof RTaskRecord> {}
export const TaskRecord = Record.ioClean<TaskRecord>(RTaskRecord);

Authentication is a 3-step process:

  1. Register the app.

  2. Direct the user to the authorization flow.

  3. Build the authentication state with the resulting authorization ID.

Here, we use the record type we defined earlier to request permission to read and write task records.

import {Authentication} from "@baqhub/sdk";
import {TaskRecord} from "./taskRecord.js";

async function authenticate(entity: string) {
  //
  // Step 1: Register the app.
  //
  const {flowUrl, state} = await Authentication.register(entity, {
    name: "BAQ Todo",
    description: "Manage your daily tasks with ease.",
    uris: {
      website: "https://baqtodo.app",
      redirect: "https://baqtodo.app/auth{/authorization_id}",
    },
    scopeRequest: {
      read: [TaskRecord.link],
      write: [TaskRecord.link],
    },
  });

  //
  // Step 2: Direct the user to the authorization flow.
  //
  const authorizationId = await showFlowBrowser(flowUrl);

  //
  // Step 3: Combine the authorization ID and partial authentication state.
  //
  return Authentication.complete(state, authorizationId);
}

Now that we have an authentication state, we can use it to build a Client­ to fetch data and perform actions on behalf of our user.

import {Client, AuthenticationState} from "@baqhub/sdk";

const authState = await authenticate("https://alice.baq.run");
const client = Client.authenticated(authState);

The state can be persisted in JSON format to be re-used in between sessions.

import {Client, AuthenticationState} from "@baqhub/sdk";

const authStateJson = localStorage.getItem("auth_state");
const authState = AuthenticationState.decodeJSON(authStateJson);
const client = Client.authenticated(authState);

When only wanting to query public records, an unauthenticated client can be created with just the user’s entity:

const client = Client.ofEntity("https://alice.baq.run");

The newly created Client­ can now be used to fetch a record. In this case a task record that our user created.

import {IO} from "@baqhub/sdk";
import {TaskRecord} from "./taskRecord.js";

const {record} = await client.getRecord(
  IO.unknown,
  TaskRecord,
  "alice.baq.run",
  "9f5e0c5dc5e0451cbd64eea32e5e38c6"
);

Thanks to the record type we defined, the resulting record is properly typed.

Fetching multiple records works in the same way. Filtering what records to get is done by building a Query­ object.

In this example, we want all the tasks created by our own user.

import {IO, Q} from "@baqhub/sdk";
import {TaskRecord} from "./taskRecord.js";

const {records} = await client.getRecords(
  IO.unknown,
  TaskRecord,
  {
    pageSize: 30,
    filter: Q.and(Q.author("alice.baq.run"), Q.type(TaskRecord)),
  }
);

The record type we defined earlier provides helpers to help us create new records of that type.

We then use the Client­ to push that record to the server.

import {IO} from "@baqhub/sdk";
import {TaskRecord} from "./taskRecord.js";

const task = TaskRecord.new("alice.baq.run", {
  title: "Write more guides on BAQ.DEV",
  completed: false,
});

const {record} = await client.postRecord(
  IO.unknown,
  TaskRecord,
  task
);

Creating a new version of an existing record is very similar. We use a record type helper to create the updated record, and then push it to the server with the help of the Client­.

import {IO} from "@baqhub/sdk";
import {TaskRecord} from "./taskRecord.js";

async function updateTask(task: TaskRecord) {
  const updatedTask = TaskRecord.update("alice.baq.run", task, {
    ...task.content,
    completed: true,
  });

  const {record} = await client.putRecord(
    IO.unknown,
    TaskRecord,
    updatedTask
  );

  return record;
}

Deletion works in the same way, but with no need to create the deleted record tombstone, the server does that for us and returns it.

import {IO} from "@baqhub/sdk";

async function deleteTask(task: TaskRecord) {
  const {record} = await client.deleteRecord(IO.unknown, task);
  return record;
}
© 2024 Quentez