Self hosted Firestore-like database with API endpoints based on micro bulk operations
Installation
npm install --save firestorm-db
JavaScript Part
The JavaScript index.js file is just an Axios wrapper of the library.
How to use it
First, you need to configure your API address, and your token if needed:
require("dotenv").config();
const firestorm = require("firestorm-db");
firestorm.address(process.env.FIRESTORM_URL);
firestorm.token(process.env.FIRESTORM_TOKEN);
Now you can use Firestorm to its full potential:
const firestorm = require("firestorm-db");
const userCollection = firestorm.collection("users");
userCollection
.readRaw()
.then((res) => console.log(res))
.catch((err) => console.error(err));
Collection constructor
A collection takes one required argument and one optional argument:
- The name of the collection as a
String
. - The method adder, which allows to inject methods to the get methods results. This would be a
Function
taking the element as an argument.
const firestorm = require("firestorm-db");
const userCollection = firestorm.collection("users", (el) => {
el.hello = function () {
console.log(`${el.name} says hello!`);
};
});
const johnDoe = await userCollection.get(123456789);
johnDoe.hello();
Available methods for a collection:
Read operations
Name | Parameters | Description |
---|
sha1() | none | Get the sha1 hash of the file. Can be used to see if same file content without downloading the file. |
readRaw() | none | Returns the whole content of the JSON. ID values are injected for easier iteration, so this may be different to sha1(). |
get(id) | id: string | number | Get an element from the collection. |
search(searchOptions, random) | searchOptions: SearchOption[] random?:boolean | number | Search through the collection You can randomize the output order with random as true or a given seed. |
searchKeys(keys) | keys: string[] | number[] | Search specific keys through the collection. |
select(selectOption) | selectOption: { field: string[] } | Get only selected fields from the collection Essentially an upgraded version of readRaw. |
random(max, seed, offset) | max?: integer >= -1 seed?: integer offset?:integer >= 0 | Reads random entries of collection. |
The search method can take one or more options to filter entries in a collection. A search option takes a field
with a criteria
and compares it to a value
. You can also use the boolean ignoreCase
option for string values.
Not all criteria are available depending the field type. There are more options available than the firestore where
command, allowing you to get better and faster search results.
All search options available
Criteria | Types allowed | Description |
---|
'!=' | boolean , number , string | Searches if the entry field's value is different from yours |
'==' | boolean , number , string | Searches if the entry field's value is equal to yours |
'>=' | number , string | Searches if the entry field's value is greater or equal than yours |
'<=' | number , string | Searches if the entry field's value is equal to than yours |
'>' | number , string | Searches if the entry field's value is greater than yours |
'<' | number , string | Searches if the entry field's value is lower than yours |
'in' | number , string | Searches if the entry field's value is in the array of values you gave |
'includes' | string | Searches if the entry field's value includes your substring |
'startsWith' | string | Searches if the entry field's value starts with your substring |
'endsWith' | string | Searches if the entry field's value ends with your substring |
'array-contains' | Array | Searches if the entry field's array contains your value |
'array-contains-any' | Array | Searches if the entry field's array ends contains your one value of more inside your values array |
'array-length-eq' | number | Searches if the entry field's array size is equal to your value |
'array-length-df' | number | Searches if the entry field's array size is different from your value |
'array-length-lt' | number | Searches if the entry field's array size is lower than your value |
'array-length-gt' | number | Searches if the entry field's array size is lower greater than your value |
'array-length-le' | number | Searches if the entry field's array size is lower or equal to your value |
'array-length-ge' | number | Searches if the entry field's array size is greater or equal to your value |
Write operations
Name | Parameters | Description |
---|
writeRaw() | none | Set the entire JSON file contents /!\ Very dangerous /!\ |
add(value) | value: Object | Adds one element with autoKey into the collection |
addBulk(values) | value: Object[] | Adds multiple elements with autoKey into the collection |
remove(key) | key: string | number | Remove one element from the collection with the corresponding key |
removeBulk(keys) | keys: string[] | number[] | Remove multiple elements from the collection with the corresponding keys |
set(key, value) | key: string | number , value: Object | Sets one element with its key and value into the collection |
setBulk(keys, values) | keys: string[] | number[] , values: Object[] | Sets multiple elements with their corresponding keys and values into the collection |
editField(obj) | obj: EditObject | Changes one field of a given element in a collection |
editFieldBulk(objArray) | objArray: EditObject[] | Changes one field per element in a collection |
Edit field operations
Edit objects have an id
to get the wanted element, a field
they want to edit, an operation
with what to do to this field, and a possible value
. Here is a list of operations:
Operation | Value required | Types allowed | Description |
---|
set | Yes | any | Sets a field to a given value |
remove | No | any | Removes a field from the element |
append | Yes | string | Appends string at the end of the string field |
invert | No | any | Inverts tate of boolean field |
increment | No | number | Adds a number to the field, default is 1 |
decrement | No | number | Retrieves a number to the field, default is -1 |
array-push | Yes | any | Push an element to the end of an array field |
array-delete | Yes | integer | Removes and element at a certain index in an array field, check array_splice documentation offset for more infos |
array-splice | Yes | [integer, integer] | Removes certain elements, check array_splice documentation offset and length for more infos |
PHP files
The PHP files are the ones handling files, read and writes. They also handle GET and POST requests to manipulate the database.
PHP setup
The developer has to create two main files at the root of their Firestorm setup: tokens.php
and config.php
.
tokens.php
will contain the tokens inside a $db_tokens
value array with the tokens to use. You will use these tokens to write data or read private tables.
config.php
stores all of your collections config. You will create a $database_list
variable with an array of JSONDatabase
instances
<?php
require_once('./classes/JSONDatabase.php');
$database_list = array();
$tmp = new JSONDatabase;
$tmp->folderPath = './files/';
$tmp->fileName = 'users';
$tmp->autoKey = false;
$database_list[$tmp->fileName] = $tmp;
$tmp = new JSONDatabase;
$tmp->folderPath = './files/';
$tmp->fileName = 'paths';
$tmp->autoKey = true;
$database_list[$tmp->fileName] = $tmp;
?>
The database will be stored in <folderPath>/<filename>.json
and autoKey
allows or forbids some write operations.
Files feature
File API functions are detailed in the files.php
PHP script. If you do not want to include this functionality, then just delete this file.
You have to add 2 new configuration variables to your config.php
file:
$authorized_file_extension = array('.txt', '.png');
$STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/';
You can use the wrapper functions in order to upload, get and delete a file.
If the folder is accessible from server url, you can directly type its address.
File rights
The PHP scripts create folders and files, so the script will fail if the PHP user doesn't have write permissions.
You can give rights to a folder with the following command:
sudo chown -R www-data "/path/to/uploads/"
Upload a file
In order to upload a file, you have to give the function a FormData
object. This class is generated from forms and is native in modern browsers but in Node.js can be imported with the form-data package.
The uploaded file content can be a String, a Blob, a Buffer or an ArrayBuffer.
There is additionally an overwrite option in order to avoid big mistakes and allow unique file names.
const firestorm = require("firestorm-db");
firestorm.address("ADDRESS_VALUE");
firestorm.token("TOKEN_VALUE");
const form = new FormData();
form.append("path", "/quote.txt");
form.append("file", "but your kids are gonna love it.", "quote.txt");
form.append("overwrite", "true");
const uploadPromise = firestorm.files.upload(form);
uploadPromise
.then(() => {
console.log("Upload successful");
})
.catch((err) => {
consoler.error(err);
});
Get a file
You can get a file via its direct file URL location or its content with a request.
const firestorm = require("firestorm-db");
firestorm.address("ADDRESS_VALUE");
const getPromise = firestorm.files.get("/quote.txt");
getPromise
.then((fileContent) => {
console.log(fileContent);
})
.catch((err) => {
console.error(err);
});
Delete a file
Because I am a nice guy, I thought about deletion too. So I figured I would put a method to delete the files too.
const firestorm = require("firestorm-db");
firestorm.address("ADDRESS_VALUE");
firestorm.token("TOKEN_VALUE");
const deletePromise = firestorm.files.delete("/quote.txt");
deletePromise
.then(() => {
console.log("File successfully deleted");
})
.catch((err) => {
console.error(err);
});
Memory warning
Handling big collections can cause memory allocation issues like:
Fatal error:
Allowed memory size of 134217728 bytes exhausted (tried to allocate 32360168 bytes)
If you encounter a memory allocation issue, you have to allow more memory through this file /etc/php/7.4/apache2/php.ini
with a bigger value here:
memory_limit = 256M
API endpoints
All Firestorm methods correspond to an equivalent Axios request to the relevant PHP file. Read requests are GET requests and write requests are POST requests with provided JSON data.
You always have the same first keys and the one key per method:
{
"collection": "<collectionName>",
"token": "<writeTokenIfNecessary>",
"command": "<methodName>",
...
}