I8               ,dPYb,                
                                      I8               IP'`Yb                
                                   88888888            I8  8I                
                                      I8               I8  8bgg,             
 gg,gggg,      ,gggg,gg    ,gggg,gg   I8     ,gggg,gg  I8 dP" "8    ,gggg,gg 
 I8P"  "Yb    dP"  "Y8I   dP"  "Y8I   I8    dP"  "Y8I  I8d8bggP"   dP"  "Y8I 
 I8'    ,8i  i8'    ,8I  i8'    ,8I  ,I8,  i8'    ,8I  I8P' "Yb,  i8'    ,8I 
,I8 _  ,d8' ,d8,   ,d8b,,d8,   ,d8b,,d88b,,d8,   ,d8b,,d8    `Yb,,d8,   ,d8b,
PI8 YY88888PP"Y8888P"`Y8P"Y8888P"`Y88P""Y8P"Y8888P"`Y888P      Y8P"Y8888P"`Y8
 I8                                                                          
 I8                                                                          
 I8                                                                          
 I8                                                                          
 I8                                                                          
 I8                                                                          

What is Paataka

Paataka is a tool to host simple HTTP APIs for students that store and query JSON objects.

Right now, we've just learned how to consume external APIs but we won't start building our own until Monday. So we're relying on external APIs.

When practicing with external APIs it can be hard to find free APIs that allow you to read and write. So it's hard to get practice with useMutationor the HTTP verbs POST, PUT, PATCH and DELETE.

You also have to shop around for an API with a topic that interests you.

With paataka, you create your own collections with your own seed data and you can both read and write that data, and you don't need to worry about how the server works (yet).

Getting Started

Once you have an invitation link, you can start creating collections.

Your organisation name will be baked into the link, it'll look something like this:

https://paataka.cloud/charlottes-weblog?code=iajsdokkd

You create collections by pasting json into the <textarea />, that json should be an object, where each key is an array of objects.

{
  "posts": [],
  "comments": []
}

This will create two empty collections "posts" and "comments"

If you want to seed items into this, just put some json objects into the arrays.

Paataka always wants to set the id, so don't give your records built in IDs

The other item on the page you'll want is the API Key, unlike the invite code, you can easily reset the API key to get a brand new one, you'll use this to authenticate all your requests.

We'll call it TOKEN.

Let's go through the CRUD operations, starting with "read".

Read

You can read a list of all items from a collection with a GET request.

(Scroll down to the pagination section to see how to break that result into pages)

const res = await request
  .get('https://paataka.cloud/api/_/charlottes-weblog/posts')
  .auth(TOKEN, { type: 'bearer' })

const posts = await res.body

or you can get a specific item by its id:

const res = await request
  .get('https://paataka.cloud/api/_/charlottes-weblog/posts/2')
  .auth(TOKEN, { type: 'bearer' })

const post = await res.body

In your react app, you might want to wrap that with useQuery:

import { useQuery } from '@tanstack/react-query'
// ! if you're copying this... I'm just guessing about where
//   you keep your models and what they're called
import type { Post } from '../../models/Post.ts'

export function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const res = await request
        .get('https://paataka.cloud/api/_/charlottes-weblog/posts')
        .auth(TOKEN, { type: 'bearer' })

      return res.body as Post[]
    }
  })
}

export function usePostById(id: number) {
  return useQuery({
    // ! the prefix of this query key should match
    //   the one for your whole collection 
    queryKey: ['posts', id],
    queryFn: async () => {
      const res = await request
        .get(`https://paataka.cloud/api/_/charlottes-weblog/posts/${id}`)
        .auth(TOKEN, { type: 'bearer' })

      return res.body as Post[]
    }
  })
}

Create

You can create a new item in a collection by posting to that collection, e.g.

const res = await request
  .post('https://paataka.cloud/api/_/charlottes-weblog/posts')
  .send({ text: 'Hello' })
  .auth(TOKEN, { type: 'bearer' })

In a react app you probably want to wrap that up in a useMutation:

import request from 'superagent'
import { useQueryClient, useMutation } from '@tanstack/react-query'
import type { Post } from '../../models/Post.ts'

export function useCreatePost() {
  const qc = useQueryClient()

  return useMutation({
    mutationFn: async (data: Post) => {
      await request
        .post('https://paataka.cloud/api/_/charlottes-weblog/posts')
        .send(data)
        .auth(TOKEN, { type: 'bearer' })
    },

    onSuccess: () => {
      qc.invalidateQueries(['posts'])
    }
  })
}

Update

You can use either PUT to replace an item

const res = await request
  .put('https://paataka.cloud/api/_/charlottes-weblog/posts/2')
  .send({ text: 'I changed my mind' })
  .auth(TOKEN, { type: 'bearer' })

...or PATCH to merge another JSON object into it

const res = await request
  .put('https://paataka.cloud/api/_/charlottes-weblog/posts/2')
  .send({ urgency: 1 })
  .auth(TOKEN, { type: 'bearer' })

const updatedPost = await res.body 

For your react frontend, you can wrap these up in useMutation:

import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Post } from '../../models/Post.ts'

export function useReplacePost(id: number) {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: async (data: Post) => {
      await request
        .put(`https://paataka.cloud/api/_/charlottes-weblog/posts/${id}`)
        .send(data)
        .auth(TOKEN, { type: 'bearer' })
    },

    onSuccess: () => {
      // ! make sure queries that reference this specific
      //   post are invalidated
      qc.invalidateQueries(['posts', id])
    }
  })
}

export function usePatchPost(id: number) {
  const qc = useQueryClient()
  return useMutation({
    // ! we're using a Partial because you don't need to pass
    //   every property of Post when doing a patch update
    mutationFn: async (data: Partial<Post>) => {
      await request
        .patch(`https://paataka.cloud/api/_/charlottes-weblog/posts/${id}`)
        .send(data)
        .auth(TOKEN, { type: 'bearer' })
    },

    onSuccess: () => {
      qc.invalidateQueries(['posts', id])
    }
  })
}

Delete

To remove an item from a collection, use DELETE

const res = await request
  // or .del()
  .delete('https://paataka.cloud/api/_/charlottes-weblog/posts/2')
  .auth(TOKEN, { type: 'bearer' })

In your react frontend, you can wrap this in a useMutation:

import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Post } from '../../models/Post.ts'

export function useDeletePost(id: number) {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: async (data: Post) => {
      await request
        .delete(`https://paataka.cloud/api/_/charlottes-weblog/posts/${id}`)
        .auth(TOKEN, { type: 'bearer' })
    },

    onSuccess: () => {
      qc.invalidateQueries(['posts', id])
    }
  })
}

Pagination of results

By default when you GET a collection, e.g. https://paataka.cloud/api/_/clown/shoes you'll get all of the items in that collection and a count property telling you how many there are.

This is a bit redundant because you can just check items.length.

{
  "count": 200,
  "items": [...] // 200 items
}

However if you set page in the query-string, you get paginated results. The payload will now include a "page" property which should be the same as the "page" query key. It will also include a pageCount and itemsPerPage property to make it a little easier to implement paging controls.

{
  "count": 200,
  "items": [ ... ] // 20 items,
  "page": 1,
  "pageCount": 20,
  "itemsPerPage": 10
}

Paataka's Expression Language

When getting collections from paataka, results can be filtered and sorted using a DSL that mimics a very small subset of JavaScript.

These ultimately are compiled to SQL and executed inside sqlite, so they don't have all the capabilities of real JavaScript

Identifiers

The basis of most expressions is one of two identifiers:

  • _ represents the current object being filtered or sorted
  • id represents the id of the current object

Literals

Numbers and strings can be written with the same syntax as JSON

Property accessors

dot-accessors and bracket access has the same basic syntax as JavaScript.

Consider this object:

{
  "name": "kevin",
  "height": 7,
  "urls": {
    "x": "https://x.com/kevin"
  }
}

These are some expressions we can write and their values when applied to this object

_.name == "kevin";
_["name"] == "kevin";
_.age == 7;
_.urls.x == "https://x.com/kevin";
_.urls["x"] == "https://x.com/kevin";
_["urls"].x == "https://x.com/kevin";

The .length property works more or as you might expect for strings and arrays. The length of a string with non-ascii characters will be different because of the difference between sqlite representation and how string.length works for JavaScript strings.

Accessing items in arrays by number does not yet work.

Comparisons

The following comparisons are available

== != >= > < <=

So we can ask questions like these of each row

_.age > 7; // is this older than 7?
_.height > _.width; // is this taller than it is wide?
_.author == "James"; // did James write this?
_.genre != "Country"; // is this song acceptable?

Logical combination

logical "and", "or" and "not" are written the same as in JavaScript

&& || !

This allows us to ask two or more questions in the same expression:

_.height < 1.5 && _.weightKg < 50; // could I carry this person?
!(_.height >= 1.5 || _.weightKg >= 50) // could I carry Augustus De Morgan?

Using expressions in URLs

Expressions are used in the query string of a URL.

Despite that our expression language is designed without concern for what it looks like in a URL, and therefore "how it looks" is "ugly".

For example, behold!

https://paataka.cloud/api/_/clown/shoes?where=_.size%20%3E%3D%205%20%26%26%20_.color%20%3D%3D%20%22blue%22

This is certainly not pretty, but most of the time when you're using the language you'll likely be using a library like superagent, so it will look like this:

const res = await request
  .get("https://paataka.cloud/api/_/clown/shoes")
  .query({
    where: '_.size >= 5 && _.color == "blue"',
  })
  .auth(API_TOKEN, { type: "bearer" });

const data = res.body;

or using web standard APIs:

const url = new URL("https://paataka.cloud/api/_/clown/shoes");
url.params.set("where", '_.size >= 5 && _.color == "blue"');
const res = await fetch(url);
const data = await res.json();

So, while there are cases where you end up looking at the URL and it's not-ideal, it is explicitly a non-goal of this project to care what the query string looks like.

Filtering objects with "where"

As seen in the above example, using the query key "where", we can filter our objects to a set for which that expression returned true (or truthy).

Sorting objects with orderBy

We can use the query key orderBy to sort our results by an expression and then choose the direction with the dir query key. dir should either be "asc" or "desc" ("asc" is used by default).

const res = await request
  .get("https://paataka.cloud/api/_/clown/shoes")
  .query({
    orderBy: "_.size",
    dir: "desc",
  })
  .auth(API_TOKEN, { type: "bearer" });

Functions and methods

We implement two methods on strings .toUpperCase() and .toLowerCase(), these behave similarly to their counterparts in JavaScript.

We implement array.includes() to check for specific values inside arrays

Array literals are available, mostly so you can use .includes() to check against multiple values.

["country", "western"].includes(_.genre)

Of course, these are not real method-calls, _.toUpperCase() is compiled to the SQL function UPPER(_) and _.toLowerCase() is compiled to LOWER(_), _.includes() is compiled to an EXISTS sub-query.

The syntax is purely to give the queries a more JavaScript-ish feel.

like(str, pattern) which compiles directly to the SQL function LIKE(pattern, str) which has the same semantics as the SQL builtin str LIKE pattern.

Weird stuff

The expressions are pretty general, so you can use them on either side of any operator and you can even look up the value of one property of an object on the same object.

So for this object ...

{
  "active": "green",
  "rangers": {
    "green": "t-rex",
    "pink": "pteradactyl"
  }
}

...this expression works

_.rangers[_.active] == "t-rex";