🗃️ Building a Pocket Database with Telegra.ph — create your non-local database in just a single function! 😱
Tags: javascript, database, tutorial, opensource
Published: 2024-01-24 08:20:32

**Telegra.ph**, an open-source publishing tool by Telegram, not only allows basic HTML writing but also provides an API. My ingenious project involves utilizing Telegra.ph as a pocket database.

```javascript
// create new database
var mydb = await pocketdb();
console.log("Access Token:", mydb.token);

// create new data
await mydb.set("users", {
  name: "John Doe",
  email: "johndoe@gmail.com"
});

// retrieve the data
var data = await mydb.get("users");
console.log("Data (Users):", data);
```

*I still can't believe how awesome this concept really is. You can literally create a non-local database by just triggering a single function. How cool is that?*

***



## Structure
A database consists of multiple `data`, and each data contains a single JSON-compatible value (key-value structure).

For initialization, you can either use an existing database by providing the access key or create a new database by passing `undefined`.

```javascript
await pocketdb(); // create new database
await pocketdb("258a7dcd77c81..."); // use an existing database
```

After initializing, an instance will return with this structure:

```javascript
{
  cache: Array,
  token: String,
  list: Array,
  async get: Function,
  async set: Function
}
```

***

## Codebase

Before returning an instance, we need to find the requested database or create a new one.

```javascript
if(token != null) {
  // use existing database
} else {
  // create new database
}
```

According to Telegra.ph, we need to create a new account to be able to write a story. To do this, we fetch the endpoint `createAccount`.

```javascript
try {
  var req = await fetch(`https://api.telegra.ph/createAccount?short_name=${Math.random()}`);
  req = await req.json();
} catch(e) {
  throw `Failed to connect (${e})`;
}
if(req.ok != true) throw `Failed to connect (${req.error})`;
// set the access token
token = req.result.access_token;
```

Creating new database is done. Let's move on initializing an existing database.

According to telegra.ph, the endpoint `getPageList` will return the list of pages created by the account. We will be using this to list all the stored data.

```javascript
try {
  var req = await fetch(`https://api.telegra.ph/getPageList?access_token=${token}&limit=200`);
  req = await req.json();
} catch(e) {
  throw `Failed to connect (${e})`;
}
if(req.ok != true) throw `Failed to connect (${req.error})`;
```

Now, after the initialization, we need to update the `cache` and `list` properties.
 
```javascript
var cache = [];
var list = [];
(req.result.pages || []).forEach(page => {
  list.push(page.title);
  cache.push({
    title: page.title,
    path: page.path
  });
});
```

Okay, we're done on initializing an existing database. We will move on to `get` and `set` function.

Data relies on pages, so we will use the endpoint `createPage` to create a page and `editPage` to modify a page. `set` handles both modification and creation. Thus, we need to check if the data exists. If it does, we will modify the data; otherwise, we will create a new one. `list` and `cache` come in as heroes to reduce HTTP request usage.

```javascript
async set(key, val) {
  // parse the value
  var content = JSON.stringify([{
    tag: "p",
    children: [JSON.stringify(val)]
  }]);
  if(this.list.includes(key)) {
    // edit a page
    var path = this.cache.find(i => i.title == key).path;
    try {
      var req = await fetch(`https://api.telegra.ph/editPage?access_token=${token}&title=${encodeURI(key)}&path=${path}&content=${encodeURI(content)}`);
      req = await req.json();
    } catch(e) {
      throw `Failed to perform the operation (${e})`;
    }
    if(req.ok != true) throw `Failed to perform the operation (${req.error})`;
    // update the cache
    var that = this;
    this.cache.forEach(function(c, i) {
      if(c.title == key) {
        that.cache[i].content = val;
      }
    });
  } else {
    // create new page
    try {
      var req = await fetch(`https://api.telegra.ph/createPage?access_token=${token}&title=${encodeURI(key)}&content=${encodeURI(content)}`);
      req = await req.json();
    } catch(e) {
      throw `Failed to perform the operation (${e})`;
    }
    if(req.ok != true) throw `Failed to perform the operation (${req.error})`;
    // update the cache and list
    this.cache.push({
      title: req.result.title,
      path: req.result.path,
      content: val
    });
    this.list.push(req.result.title);
  }
}
```



Now, the last part is `get`. Let's start by using the endpoint `getPage`.

```javascript
async get(key, def, nocache) {
  // if the key does not exist on the list, return the default value
  if(!this.list.includes(key)) return def;
  // find the cache
  var cache = this.cache.find(i => i.title == key);
  // if nocache is not true and the cache exists, use the cache
  if(!nocache && cache?.content != null) {
    return cache.content;
  }
  // get the page
  try {
    var req = await fetch(`https://api.telegra.ph/getPage?access_token=${token}&path=${cache.path}&return_content=true`);
    req = await req.json();
  } catch(e) {
    throw `Failed to perform the operation (${e})`;
  }
  if(req.ok != true) throw `Failed to perform the operation (${req.error})`;
  // parse the content
  var val = JSON.parse(req.result.content[0].children[0]);
  // update the cache
  var that = this;
  this.cache.forEach(function(c, i) {
    if(c.title == key) {
      that.cache[i].content = val;
    }
  });
  return val;
}
```

***


And we're done! Now, let's finish it up by combining all the code.

```javascript
async function pocketdb(token) {
  if (token != null) {
    // use existing database
    try {
      var req = await fetch(`https://api.telegra.ph/getPageList?access_token=${token}&limit=200`);
      req = await req.json();
    } catch(e) {
      throw `Failed to connect (${e})`;
    }
    if (req.ok != true) throw `Failed to connect (${req.error})`;
  } else {
    try {
      var req = await fetch(`https://api.telegra.ph/createAccount?short_name=${Math.random()}`);
      req = await req.json();
    } catch(e) {
      throw `Failed to connect (${e})`;
    }
    if (req.ok != true) throw `Failed to connect (${req.error})`;
    // set the access token
    token = req.result.access_token;
  }
  var cache = [];
  var list = [];
  (req.result.pages || []).forEach(page => {
    list.push(page.title);
    cache.push({
      title: page.title,
      path: page.path
    });
  });
  return {
    cache,
    list,
    token,
    async set(key, val) {
      // parse the value
      var content = JSON.stringify([{
        tag: "p",
        children: [JSON.stringify(val)]
      }]);
      if (this.list.includes(key)) {
        // edit a page
        var path = this.cache.find(i => i.title == key).path;
        try {
          var req = await fetch(`https://api.telegra.ph/editPage?access_token=${token}&title=${encodeURI(key)}&path=${path}&content=${encodeURI(content)}`);
          req = await req.json();
        } catch(e) {
          throw `Failed to perform the operation (${e})`;
        }
        if (req.ok != true) throw `Failed to perform the operation (${req.error})`;
        var that = this;
        this.cache.forEach(function(c, i) {
          if(c.title == key) {
            that.cache[i].content = val;
          }
        });
      } else {
        // create new page
        try {
          var req = await fetch(`https://api.telegra.ph/createPage?access_token=${token}&title=${encodeURI(key)}&content=${encodeURI(content)}`);
          req = await req.json();
        } catch(e) {
          throw `Failed to perform the operation (${e})`;
        }
        if (req.ok != true) throw `Failed to perform the operation (${req.error})`;
        // update the cache and list
        this.cache.push({
          title: req.result.title,
          path: req.result.path,
          content: val
        });
        this.list.push(req.result.title);
      }
    },
    async get(key, def, nocache) {
      // if the key does not exist on the list, return the default value
      if (!this.list.includes(key)) return def;
      // find the cache
      var cache = this.cache.find(i => i.title == key);
      // if nocache is not true and the cache exists, use the cache
      if (!nocache && cache?.content != null) {
        return cache.content;
      }
      // get the page
      try {
        var req = await fetch(`https://api.telegra.ph/getPage?access_token=${token}&path=${cache.path}&&return_content=true`);
        req = await req.json();
      } catch(e) {
        throw `Failed to perform the operation (${e})`;
      }
      if (req.ok != true) throw `Failed to perform the operation (${req.error})`;
      // parse the content
      var val = JSON.parse(req.result.content[0].children[0]);
      // update the cache
      var that = this;
      this.cache.forEach(function(c, i) {
        if (c.title == key) {
          that.cache[i].content = val;
        }
      });
      return val;
    }
  }
}

// Example usage:
(async function() {
  // create new database
  var db = await pocketdb();
  console.log(db);
  // create a data
  await db.set("users", {
    name: "John Doe",
    email: "johndoe@example.com"
  });
  console.log(db);
  // get the data (skip cache by setting true on 'nocache')
  var data = await db.get("users", null, true);
  console.log(data);
  // total http requests: 3
})();
```

***

## Extra

If you want to add the [string compressor](https://dev.to/creuserr/cantor-pairing-for-string-compression-1534) that I made, update the code of `get` and `set`:

```javascript
// parse the content
var val = JSON.parse(this.decompress(unescape(atob(req.result.content[0].children[0]))));
```

```javascript
// parse the value
var content = JSON.stringify([{
  tag: "p",
  children: [btoa(escape(this.compress(JSON.stringify(val))))]
}]);
```

**Remember to define `compress` and `decompress` on the instance!**

```javascript
{
  compress: Function,
  decompress: Function,
  cache: Array,
  token: String,
  list: Array,
  async get: Function,
  async set: Function
}
```

*This extra feature might not effectively compress a string but it will provide a efficient usage as it properly encode the value on the URL.*

***

That's it, thank you for reading this blog. If you are interested, visit the [gist](https://gist.github.com/creuserr/a6b445c27ff248b3971c0a65bcd89a35) of this project.