DEV Community

creuser
creuser

Posted on • Edited on

🗃️ Building a Pocket Database with Telegra.ph — create your non-local database in just a single function! 😱

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.

// 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);
Enter fullscreen mode Exit fullscreen mode

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.

await pocketdb(); // create new database
await pocketdb("258a7dcd77c81..."); // use an existing database
Enter fullscreen mode Exit fullscreen mode

After initializing, an instance will return with this structure:

{
  cache: Array,
  token: String,
  list: Array,
  async get: Function,
  async set: Function
}
Enter fullscreen mode Exit fullscreen mode

Codebase

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

if(token != null) {
  // use existing database
} else {
  // create new database
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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.

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})`;
Enter fullscreen mode Exit fullscreen mode

Now, after the initialization, we need to update the cache and list properties.

var cache = [];
var list = [];
(req.result.pages || []).forEach(page => {
  list.push(page.title);
  cache.push({
    title: page.title,
    path: page.path
  });
});
Enter fullscreen mode Exit fullscreen mode

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.

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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
})();
Enter fullscreen mode Exit fullscreen mode

Extra

If you want to add the string compressor that I made, update the code of get and set:

// parse the content
var val = JSON.parse(this.decompress(unescape(atob(req.result.content[0].children[0]))));
Enter fullscreen mode Exit fullscreen mode
// parse the value
var content = JSON.stringify([{
  tag: "p",
  children: [btoa(escape(this.compress(JSON.stringify(val))))]
}]);
Enter fullscreen mode Exit fullscreen mode

Remember to define compress and decompress on the instance!

{
  compress: Function,
  decompress: Function,
  cache: Array,
  token: String,
  list: Array,
  async get: Function,
  async set: Function
}
Enter fullscreen mode Exit fullscreen mode

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 of this project.

Top comments (0)