🗃️ Building a Pocket Database with Telegra.ph — create your non-local database in just a single function! 😱
creuser
Posted on January 24, 2024
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);
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
After initializing, an instance will return with this structure:
{
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.
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
.
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.
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.
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.
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
.
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.
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 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]))));
// 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!
{
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 of this project.
Posted on January 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 24, 2024