🔥 Infernus (nosql with MariaDB)

:fire:

Unimpressed with or cannot use the NoSql offerings from others on the internet?

Then: DIY a NoSql database using the Infernus framework

Over the past few months I have been "de-googling" my online/offline life. One of the gaps generated by this was a need for a replacement for Firebase. This replacement became available when looking at what mariaDB (and mySql) have on offer in terms of using and working with json in the database. My hosting provider offers mariaDB as the relational database, therefore I am using this (mariaDB) for my guide. The focus for this guide is on interacting between Infernus and AppInventor.

Why Infernus, well this is latin for the fire below which I thought was quite apt. I also use a table called ignis (fire), and another table called cella (storeroom or warehouse) used to replicate firebase storage.

In essence what you do is, create a database, then create a table with an id, a name, and a json_data field. You only need one record in your database, because all your nosql data will be built up in the json you set in the json_data field. Consider this to be your "firebase project". If you create another record, then this could be another firebase project. I have accessed the database with php, creating several files to help me view the data, and carry out CRUD functions on it (Create, Read, Update & Delete). You then create another table (cella) to store the filename and url of any files uploaded. With all this under my belt, I can then start to build applications with AppInventor.

Requirements

I am going to assume that you have a good understanding of AppInventor and its workings, and a reasonable understanding of working with php and mariadb. I am not going to spend too much time on the setup, in order to focus on the nosql activities. You should look at the Resources section for further background information.

An online http server running php and providing mariadb (or mySql) as a database. File storage on the same server is also required. I created a database called infernus and added a table called ignis. I then added a record with name Products and a json containing...products. I will work with these throughout this guide.

You can, of course, set up your own server locally on your computer.

All software used is open source, and non-google.

The database table ignis, is set with three fields: id, name, and json_data. name is not strictly required, but serves as a useful marker if you have multiple 'projects' in the database. (you could actually run without id as well (using the hidden _rowid instead). Each of your json_data rows would need to be unique.)

On the server, I have all the php files in a top line folder called infernus.

Every php file uses the config.php file, which contains the connection data:

<?php
//config.php

define('servername', 'localhost');
define('username',   'your user name');
define('password',   'your password');
define('dbname',     'your database name');
?>

In the blocks, we begin with setting variables that are used with each php file:

blocks (29)

blocks (25)

blocks (28)

blocks (26)

blocks (27)

Working with the data


showJson, will display the json_data in the app, in a "pretty" layout for readability. This works as a visual reminder of what is stored and available in the database. Should be used for viewing only.

showJson.php
<?php
//showJson.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$sql = "SELECT json_data FROM $tbl WHERE id = $id;";
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
  $rows[] = [$row["json_data"]];
}
header('Content-Type: application/json');
echo json_encode(json_decode($rows[0][0]),JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} else {
echo "0 results";
}
$conn->close();
?>

In the app we call showJson.php with these blocks

and they will return the json_data to the app like so:


getJson Much the same as showJson, but this brings back the json to the app in a non pretty format, as a json Array, intended for working with within the app.

getJson
<?php
//getJson.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$sql = "SELECT json_data FROM $tbl WHERE id = $id;";
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
 while($row = $result->fetch_assoc()) {
  $rows[] = [$row["json_data"]];
 }
 echo json_encode($rows,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
 } else {
 echo "0 results";
}
$conn->close();
?>

In the app we call getJson.php with these blocks:

and they will return the json_data to the app like so:


getDbData This is not one of the direct json functions, but will return all the "project" ids and names. Is a starting point for queries if you do not know the name / id of the project you want to work with.

getDbData.php
<?php
//getDbdata.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$sql = "SELECT id,name FROM $tbl WHERE id = $id;";
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
 while($row = $result->fetch_assoc()) {
     $rows[] = [$row["id"],$row["name"]]; 
 }
 echo json_encode($rows);
} else {
 echo "0 results";
}
$conn->close();
?>

In the app we call getDbData.php with these blocks:

and they will return the json_data to the app like so, as a stringified json Array:


getJsonKeys will return all the keys from the key:value pairs at the top line position (root = '$'), or wherever you set the jPath (e.g. '$.Products.Samsung')

getJsonKeys.php
<?php
//getJsonKeys.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$jPath = $_REQUEST["jPath"];
$sql = "SELECT JSON_KEYS(json_data,$jPath) AS 'keys' FROM $tbl WHERE id = $id;";
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
 while($row = $result->fetch_assoc()) {
     $rows[] = [$row["keys"]];
 }
 echo json_encode($rows);
} else {
 echo "0 results";
}
$conn->close();
?>

In the app we call getJsonKeys.php with these blocks:

and they will return the json_data to the app like so, as a stringified json Array:


getJsonValues will return the values for keys at the given jPath

getJsonValues.php
<?php
//getJsonValues

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$jPath = $_REQUEST["jPath"];
$sql = "SELECT JSON_EXTRACT(json_data,$jPath) AS 'values' FROM $tbl WHERE id = $id;";
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
 while($row = $result->fetch_assoc()) {
    $rows[] = [$row["values"]];
 }
 echo json_encode($rows);
} else {
 echo "0 results";
}
$conn->close();
?>

In the app we call getJsonValues.php with these blocks (you will see that I was able to use a wildcard '*' for the brand name, thus returning the model for all products in the json):

and they will return the json_data to the app like so, as a stringified json Array:


insertJson will insert a key:value pair at the jPath you specify. You set the key in the path and the value as a ... value. If the key:value pair already exists at that location, nothing will happen, but the php will still return "success".

insertJson.php
<?php
//insertJson.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$value = $_REQUEST['value'];
$jPath = $_REQUEST['jPath'];
$sql = "UPDATE $tbl SET json_data = JSON_INSERT(json_data, $jPath, $value) WHERE id = $id;";
if ($conn->query($sql) === TRUE) {
  echo "New json inserted successfully";
} else {
  echo "Error: " . $sql . "<br>" . $conn->error;
}
$conn->close();
?>

In the app we call insertJson with these blocks:

and return a success message on completion:


replaceJson will update an existing key:value pair in the json at the path specified

replaceJson.php
<?php
//replaceJson.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$value = $_REQUEST['value'];
$jPath = $_REQUEST['jPath'];
$sql = "UPDATE $tbl SET json_data = JSON_REPLACE(json_data, $jPath, $value) WHERE id = $id;";
if ($conn->query($sql) === TRUE) {
  echo "json replaced successfully";
} else {
  echo "Error: " . $sql . "<br>" . $conn->error;
}
$conn->close();
?>

In the app we call replaceJson.php with these blocks:

and return a success message on completion:


removeJson will remove an existing key:pair value in the json at the path specified

removeJson.php
<?php
//removeJson.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$jPath = $_REQUEST['jPath'];
$sql = "UPDATE $tbl SET json_data = JSON_REMOVE(json_data, $jPath) WHERE id = $id;";
if ($conn->query($sql) === TRUE) {
  echo "json removed successfully";
} else {
  echo "Error: " . $sql . "<br>" . $conn->error;
}
$conn->close();
?>

In the app we call removeJson.php with these blocks:

and return a success message on completion:

File Storage


upload will upload a file from the device to the online storage

upload.php
<?php
//upload.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$tbl = $_REQUEST['tbl'];
$url = $_REQUEST['url'];
$fileName = $_REQUEST['fname'];
$imgDir = "cella/";
$data = file_get_contents('php://input');
$store = file_put_contents($imgDir.$fileName, $data);
if  ($store !== false ) {
$sql = "INSERT INTO cella(filename,url) VALUES('" . $fileName . "','" . $url.$fileName . "')";
if ($conn->query($sql) === TRUE) {
  echo "File $fileName saved to server, with url $url.$fileName, and new record created successfully for uploaded file";
} else {
  echo "Error: " . $sql . "<br>" . $conn->error;
}
} else {
echo "File $fileName not saved\n";
}
$conn->close();
?>

In the app we call upload.php with these blocks:

and if the upload is successful it will return the filename and the url for the file in a json Array:


listFiles returns a listing of all the files in the online file storage

listFiles.php
<?php
//listFiles.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$id = $_REQUEST['id'];
$tbl = $_REQUEST['tbl'];
$sql = "SELECT * FROM $tbl WHERE id = $id;";
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
    while($row = $result->fetch_assoc()) {
        $rows[] = [$row["id"],$row["filename"],$row["url"]];
    }
    echo json_encode($rows,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} else {
 echo "0 results";
}
$conn->close();
?>

In the app we call listFiles.php with these blocks:

and it returns a listing of all the files and their urls in a json Array:

.
.
Here is the Web1.GotText block that handles all the returns from the database. If a jsonArray then it is encoded to an AI2 list, if a string then it is kept as such. I am displaying all returns in a label for convenience and demonstration purposes.


Caveats

  • I have chosen to use separate php files for each function, for demonstration purposes. My php method is not the only way to do this, one could build queries in the app, for example, or everything could be in one php file. (update - see 2nd post)

  • there is much that can be done to beef up security; on the server, with php, and with interaction between AppInventor and the infernus framework. I will look at this later, but have kept things simple for now. (update - see 2nd post)

  • you might want user signups, user rules etc. These are all possible, in one way or another, and I will cover some of the approaches in the future.


Resources - well worth watching / looking at!

https://www.youtube.com/watch?v=sLE7jPETp8g
(just about everything you need to understand CRUD with json on mariadb)

https://www.youtube.com/watch?v=mL7xnMZNYXM
(note the -> and ->> shortcuts shown don't work [for me] with mariadb, so you may have to use the longhand methods)

https://database.guide/?s=mariadb+json
(examples on most json functions)

https://mariadb.com/docs/server/reference/sql-functions/special-functions/json-functions
(the descriptions and examples can be a bit terse)

https://www.w3schools.com/php/php_mysql_intro.asp
(a general background to php with mySql)


Files (for the lazy...)

All the php files, and the products.json. Rename with zip extension once downloaded
infernusFiles.aia (7.1 KB)

A "blank" aia project as used here
infernus_blank.aia (201.5 KB)

2 Likes

Improving Security


  1. Prevent directory listing with browser

Ensure you have an .htaccess file in your main folder (e.g. infernus) with the following: Options -Indexes

This will prevent the viewing of a directory listing / downloading of files.

You should get this message: 403.png


  1. You could also move your config.php file outside of the publicly available directories, and change the relative path to it in each of the php files that require it.
from
require_once 'config.php';

to
require_once '../../config.php';

  1. Add a secret to your php config and php files

In your config.php and the following line:

define('secret','your secret word or phrase');

Then, in all your php files (not config.php), after the connection code, wrap the rest of your code with this if statement:

if ($_REQUEST['secret'] == secret) {

//your query code here 

} else {
echo 'access denied';
}
$conn->close();
?>

You then need to provide the php file with the set secret as a parameter, when calling a query. Of course, the secret should only be shared with trusted users.


  1. Prepared Statements

Using this approach in your php files helps to improve performance and prevent SQL injection.

Read all about it, with examples, here: https://www.w3schools.com/php/php_mysql_prepared_statements.asp

This can get complicated when you do not "know" the input and outputs for the query.


  1. Password Protect Directories

Again using your htaccess file, or by setting password protection in your hosting provider's panel, you can require a user/password in order to access the files within.

AuthType Basic
AuthName "Restricted Area - Login Required"
AuthUserFile /../../.htpasswd
Require valid-user

Use htpasswd to setup users

Also, see here for how you might set this up with the web component:

https://puravidaapps.com/filedownload.php#restricted

"Simplified" Queries...

I have created another php file called query.php, which allows you to enter the full query/sql at app level. This works for most of the original queries above.

List of queries (which match the original ones)
getJson
SELECT json_data FROM ignis WHERE id = 1;

getDbData
SELECT id,name FROM ignis WHERE id = id;

getJsonKeys
SELECT JSON_KEYS(json_data,'$.Products') AS 'keys' FROM ignis WHERE id = 1;

getJsonValues
SELECT JSON_EXTRACT(json_data,'$.Products.*.model') AS 'values' FROM ignis WHERE id = 1;

createJson
INSERT INTO ignis (name, json_data) VALUES ($name,$json_data);

insertJson
UPDATE ignis SET json_data = JSON_INSERT(json_data, '$.Products.Samsung.location', 'France') WHERE id = 1;

replaceJson
UPDATE ignis SET json_data = JSON_REPLACE(json_data, '$.Products.Samsung.location', 'Germany') WHERE id = 1;

removeJson
UPDATE ignis SET json_data = JSON_REMOVE(json_data, '$.Products.Samsung.location') WHERE id = 1;

listFiles
SELECT * FROM ignis WHERE id = id;

Example Blocks (two varieties):

You will see that I have included a "secret"

and with some procedures:

query.php
<?php
//query.php

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

if ($_REQUEST['secret'] == secret) {

$sql = $_REQUEST['qry'];
$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
 while($row = $result->fetch_assoc()) {
    $rows[] = [$row];
 }
 echo json_encode($rows,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

} else {
   if (str_contains($sql,'UPDATE')) {
     echo "success";
   } else{
     echo "0 results";
   }
 }

} else {
echo 'access denied';
}

$conn->close();
?>

User Management

Rather than provide multiple scenarios for each type of user management, I feel it may be easier if I show how I have used php to generate/confirm the various user values required to replicate the kind of behaviour seen in Firebase Authentication.

To begin with, we need to create a table for users. (I do it this way, because if your database is on shared hosting, it is unlikely that you will be able to create additional users for the database.

I have a table called sodales (members in Latin), with the following fields:

  • id
  • email
  • password(hashed)
  • uid
  • access_token
  • refresh_token
  • timestamp
  • verified

id

This is generated automatically by the database when a new record is created, and is the primary key (unique) for the table. If a record is deleted, the ids of other records remain the same. I just use the default incrementing integer.

email

This is provided by the user. I use php to check that the email address provided is actally an email address, and does not already exist in the table.

//email validation

if (!filter_var($_REQUEST['email'], FILTER_VALIDATE_EMAIL)) {
  die('invalid email address format');
}

$email = $_REQUEST['email'];
$escaped_email = mysqli_real_escape_string($conn, $email);
$sql = "SELECT * FROM sodales WHERE email = '$escaped_email'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
  die("email address already exists");
}

The email address can also be verified - see verified further down.

password (hashed)

The password is also provided by the user. We can then hash the password with a php function, so that the password is not stored in plain text, and there is another php function used to check the password provided against the hash

//hash the password

$password = $_REQUEST['password'];
$h_password = password_hash($password, PASSWORD_DEFAULT);

//check the hash against password when signing in

$email = $_REQUEST['email'];
$password = $_REQUEST['password'];
$sql = "SELECT id, password FROM sodales WHERE email = '$email'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    if (password_verify($password, $row['password'])) {
        echo "signin successful";
    } else {
        echo "invalid password";
    }
} else {
    echo "user not found";
}

uid

We can generate a unique uid for the user, which they can user to carry out validated actions on the database. This also can allow for users to change their email/password, without losing their uid.

//generate uid

$uid = str_replace(array("+","/"), array("0","0"),strtoupper(base64_encode(random_bytes(intval(ceil(20 * 3 / 4))))));

access_token

An access_token, which is used in conjunction with the uid, can be generated using php, a length of 400 characters

//generate access_token

$access_token = str_replace(array("+","/"), array("0","0"),base64_encode(random_bytes(intval(ceil(400 * 3 / 4)))));

refresh_token

A refresh_token, which can be used to create a new access_token, can be generated using php, a length of 200 characters

//generate refresh_token

$refresh_token = str_replace(array("+","/"), array("0","0"),base64_encode(random_bytes(intval(ceil(200 * 3 / 4)))));

timestamp

A timestamp can be created (usually 1 hour ahead of current time) to create an expiry for the access_token, using php. We could just use this instead of an access_token...

//create timestamp

$timestamp = floor(microtime(true) * 1000) + 3600000;

insert all these values to the user record:

$sql = "INSERT INTO sodales (email, password, uid, access_token, refresh_token, timestamp) VALUES ('$email','$h_password', '$uid', '$access_token', '$refresh_token', '$timestamp');";

if ($conn->query($sql) === TRUE) {
  echo "You are signed up successfully";
} else {
  echo "Error: " . $sql . "<br>" . $conn->error;
}

verified

This will be a boolean 0 (not verified), 1 (verified). What we are doing here is verifying the email address. On receipt of an email address from the propsective user, we send an email (using the phpMailer library) to the email address supplied, with a link (which contains a token) to a verfication page. The use must click the link to the page, then click the verfication button/link on the page, which will then set the verified field to 1 (verified). Future access to the database can then be controlled by checking this field.

(Note the email and verification page are very basic, they can be prettified with more html and css!)

//send the verification email (requires phpMailer in place)

<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\SMTP;

// Load PHPMailer classes
require __DIR__ . '/../phpmailer/Exception.php';
require __DIR__ . '/../phpmailer/PHPMailer.php';
require __DIR__ . '/../phpmailer/SMTP.php';

$mail = new PHPMailer(true);


try {
    
 // parameters
 $email = $_REQUEST['email'];
 $token = $_REQUEST['token'];
 
 
    
 // Server settings
 $mail->isSMTP();
 $mail->Host = 'smtp.provider.com'; // For provider Email
 $mail->SMTPAuth = true;
 $mail->Username = 'verify@domain.co.uk'; // full email address (sending)
 $mail->Password = '123abc';
 $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // or 'tls'
 $mail->Port = 587;

 // Sender & recipient
 $mail->setFrom('verify@domain.co.uk', 'Infernus');
 $mail->addAddress($email);

        $mail->isHTML(false);
        $mail->Subject = 'Verify Your Email Address for Infernus';
        $mail->Body = "Click here to verify your email for Infernus: https://domain.co.uk/infernus/verify.php?token=" .$token;

        $mail->send();
        return true;
    } catch (Exception $e) {
        return false;
    }

?>


// the verification page

<?php
$token = $_REQUEST['token'];

require_once 'config.php';
$conn = new mysqli(servername, username, password, dbname);
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

$sql = "SELECT id,uid,access_token FROM sodales WHERE token = $token;";

$result = $conn->query($sql);
$rows = array();
if ($result->num_rows > 0) {
  while($row = $result->fetch_assoc()) {
     $rows[] = [$row["id"],$row["uid"],$row["access_token"]];
}
  $vid = $rows[0][0];
  $vsql = "UPDATE sodales SET verified = 1 WHERE id = $vid;";
  if ($conn->query($vsql) === TRUE) {
    echo '{"verified":"true","uid":' . $rows[0][1] . ',"access_token":' . $rows[0][2] . '}';
  }
}
else {
  echo "Invalid verification link or token";
}
$conn->close();
?>   

I have also created an OTP process (for phone number verfication) using a phone to send SMS. SEE HERE.

It is also possible to create anonymous users, just like Firebase. We simply create a user without email or password, and the user is provided with a uid, access_token and refresh_token to use in order to access the database. These users will definitely need to hang on to their refresh_token for continued access! (this would, of course, be managed by your app.)

Summary

Using a combination (one, some or all) of all the methods above, it is possible to replicate Firebase Authentication methods, controlling user access to the database, and how a user might use the database once they have access.

You might want to refer back to my guide on Firebase with the Web component for reference