|
|
|
/**
|
|
|
|
* Command for working with databases.
|
|
|
|
* Copyright 2021-2022 Anton Tarasenko
|
|
|
|
*------------------------------------------------------------------------------
|
|
|
|
* This file is part of Acedia.
|
|
|
|
*
|
|
|
|
* Acedia is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Acedia is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
class ACommandDB extends Command
|
|
|
|
dependson(Database);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This command provides a text user interface to databases.
|
|
|
|
* It can perform two types of tasks:
|
|
|
|
* 1. Tasks that have to do with managing set of databases as a whole:
|
|
|
|
* listing them, their creation and deletion. For currently implemented
|
|
|
|
* local databases they work synchronously and are just simple bindings
|
|
|
|
* to the Acedia's API.
|
|
|
|
* 2. Tasks that edit a particular database: these require us to send
|
|
|
|
* database a query and then wait for the response. With these we
|
|
|
|
* cannot give an immediate reply, so we have to remember player that
|
|
|
|
* requested these queries to then relay him database's reply.
|
|
|
|
* Main problem with remembering a player for 2nd-type tasks is that,
|
|
|
|
* while highly unlikely, several different players may make their own requests
|
|
|
|
* while we are still waiting for the reply to the previous query.
|
|
|
|
* We could simply remember a queue of several players and then return them in
|
|
|
|
* a FIFO order (since databases guarantee orderly replies to their queries),
|
|
|
|
* but requests can also be towards different databases and, therefore,
|
|
|
|
* completed in a random order.
|
|
|
|
* To solve this we fill-in the waiting queue of pairs player-database that
|
|
|
|
* link player that made a request and database he made this request to.
|
|
|
|
* Once reply from any database arrives - we simply search and return
|
|
|
|
* the first player in our queue that made a request to this database.
|
|
|
|
* Thanks to the fact that databases reply to their queries in order of
|
|
|
|
* arrival - this will let us fetch precisely the players that were responsible
|
|
|
|
* for these requests. This logic is implemented in `PushPlayer()` and
|
|
|
|
* `PopPlayer()` methods.
|
|
|
|
* The rest of the methods are mostly straightforward callbacks that
|
|
|
|
* transform `Database`'s reply into text message for the player.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Array of pairs is represented by two arrays of single values.
|
|
|
|
// Arrays should be kept same length, elements with the same index
|
|
|
|
// correspond to the same pair.
|
|
|
|
var protected array<Database> queueWaitingListDatabases;
|
|
|
|
var protected array<EPlayer> queueWaitingListPlayers;
|
|
|
|
|
|
|
|
// Auxiliary structure that corresponds to database + JSON path from resolved
|
|
|
|
// database link.
|
|
|
|
struct DBPointerPair
|
|
|
|
{
|
|
|
|
var public Database database;
|
|
|
|
var public JSONPointer pointer;
|
|
|
|
};
|
|
|
|
|
|
|
|
var protected const int TCREATE, TDELETE, TLIST, TREAD, TSIZE, TKEYS, TREMOVE;
|
|
|
|
var protected const int TWRITE, TINCREMENT, TDATABASE_NAME, TDATABASE_LINK;
|
|
|
|
var protected const int TJSON_VALUE, TQUERY_INVALID_POINTER, TQUERY_INVALID_DB;
|
|
|
|
var protected const int TOBJECT_KEYS_ARE, TOBJECT_SIZE_IS, TQUERY_COMPLETED;
|
|
|
|
var protected const int TQUERY_INVALID_DATA, TAVAILABLE_DATABASES, TDA_DELETED;
|
|
|
|
var protected const int TDB_DOESNT_EXIST, TDB_ALREADY_EXISTS, TDB_CREATED;
|
|
|
|
var protected const int TDB_CANNOT_BE_CREATED, TNO_DEFAULT_COMMAND, TBAD_DBLINK;
|
|
|
|
|
|
|
|
protected function BuildData(CommandDataBuilder builder)
|
|
|
|
{
|
|
|
|
builder.Name(P("db")).Group(P("admin"))
|
|
|
|
.Summary(P("Read and edit data in your databases."
|
|
|
|
@ "Databases' values are addressed with links:"
|
|
|
|
@ "\"<db_name>:<json_path>\""));
|
|
|
|
builder.SubCommand(T(TCREATE))
|
|
|
|
.ParamText(T(TDATABASE_NAME))
|
|
|
|
.Describe(P("Creates new database with a specified name."));
|
|
|
|
builder.SubCommand(T(TDELETE))
|
|
|
|
.ParamText(T(TDATABASE_NAME))
|
|
|
|
.Describe(P("Completely deletes specified database."));
|
|
|
|
builder.SubCommand(T(TLIST))
|
|
|
|
.Describe(P("Lists available databases."));
|
|
|
|
builder.SubCommand(T(TREAD))
|
|
|
|
.ParamText(T(TDATABASE_LINK))
|
|
|
|
.Describe(P("Reads data from location given by the `databaseLink`."));
|
|
|
|
builder.SubCommand(T(TSIZE))
|
|
|
|
.ParamText(T(TDATABASE_LINK))
|
|
|
|
.Describe(P("Gets amount of elements inside JSON array or object at"
|
|
|
|
@ "location given by the `databaseLink`."));
|
|
|
|
builder.SubCommand(T(TKEYS))
|
|
|
|
.ParamText(T(TDATABASE_LINK))
|
|
|
|
.Describe(P("Lists keys of JSON object at location given by"
|
|
|
|
@ "the `databaseLink`."));
|
|
|
|
builder.SubCommand(T(TREMOVE))
|
|
|
|
.ParamText(T(TDATABASE_LINK))
|
|
|
|
.Describe(P("Removes data from location given by the `databaseLink`."));
|
|
|
|
builder.SubCommand(T(TWRITE))
|
|
|
|
.ParamText(T(TDATABASE_LINK))
|
|
|
|
.ParamJSON(T(TJSON_VALUE))
|
|
|
|
.Describe(P("Writes specified JSON value into location given by"
|
|
|
|
@ "the `databaseLink`."));
|
|
|
|
builder.Option(T(TINCREMENT))
|
|
|
|
.Describe(F("Specifying this option for any of the"
|
|
|
|
@ "{$TextEmphasis 'write'} subcommands will cause them to append"
|
|
|
|
@ "data to the old one, instead of rewriting it."));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function PushPlayer(EPlayer nextPlayer, Database callDatabase)
|
|
|
|
{
|
|
|
|
queueWaitingListPlayers[queueWaitingListPlayers.length] =
|
|
|
|
EPlayer(nextPlayer.Copy());
|
|
|
|
queueWaitingListDatabases[queueWaitingListDatabases.length] = callDatabase;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function EPlayer PopPlayer(Database relevantDatabase)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local EPlayer result;
|
|
|
|
|
|
|
|
if (queueWaitingListPlayers.length <= 0) return none;
|
|
|
|
if (queueWaitingListDatabases.length <= 0) return none;
|
|
|
|
|
|
|
|
while (i < queueWaitingListDatabases.length)
|
|
|
|
{
|
|
|
|
if (queueWaitingListDatabases[i] != relevantDatabase)
|
|
|
|
{
|
|
|
|
result = queueWaitingListPlayers[i];
|
|
|
|
queueWaitingListPlayers.Remove(i, 1);
|
|
|
|
queueWaitingListDatabases.Remove(i, 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
if (result != none && result.IsExistent()) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
_.memory.Free(result);
|
|
|
|
return none;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function Executed(CallData arguments, EPlayer instigator)
|
|
|
|
{
|
|
|
|
local AcediaObject valueToWrite;
|
|
|
|
local DBPointerPair pair;
|
|
|
|
local Text subCommand;
|
|
|
|
|
|
|
|
subCommand = arguments.subCommandName;
|
|
|
|
// Try executing on of the operation that manage multiple databases
|
|
|
|
if (TryAPICallCommands(subCommand, instigator, arguments.parameters)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// If we have failed - it has got to be one of the operations on
|
|
|
|
// a single database
|
|
|
|
pair = TryLoadingDB(arguments.parameters.GetText(T(TDATABASE_LINK)));
|
|
|
|
if (pair.database == none)
|
|
|
|
{
|
|
|
|
callerConsole.WriteLine(T(TBAD_DBLINK));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Remember the last player we are making a query to and make that query
|
|
|
|
PushPlayer(instigator, pair.database);
|
|
|
|
if (subCommand.Compare(T(TWRITE)))
|
|
|
|
{
|
|
|
|
valueToWrite = arguments.parameters.GetItem(T(TJSON_VALUE));
|
|
|
|
if (arguments.options.HasKey(T(TINCREMENT)))
|
|
|
|
{
|
|
|
|
pair.database.IncrementData(pair.pointer, valueToWrite)
|
|
|
|
.connect = DisplayResponse;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
pair.database.WriteData(pair.pointer, valueToWrite)
|
|
|
|
.connect = DisplayResponse;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TREAD))) {
|
|
|
|
pair.database.ReadData(pair.pointer).connect = DisplayData;
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TSIZE))) {
|
|
|
|
pair.database.GetDataSize(pair.pointer).connect = DisplaySize;
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TKEYS))) {
|
|
|
|
pair.database.GetDataKeys(pair.pointer).connect = DisplayKeys;
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TREMOVE))) {
|
|
|
|
pair.database.RemoveData(pair.pointer).connect = DisplayResponse;
|
|
|
|
}
|
|
|
|
_.memory.Free(pair.pointer);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Simple API calls
|
|
|
|
private function bool TryAPICallCommands(
|
|
|
|
BaseText subCommand,
|
|
|
|
EPlayer instigator,
|
|
|
|
HashTable commandParameters)
|
|
|
|
{
|
|
|
|
local Text databaseName;
|
|
|
|
if (subCommand.IsEmpty())
|
|
|
|
{
|
|
|
|
callerConsole.WriteLine(T(TNO_DEFAULT_COMMAND));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TLIST)))
|
|
|
|
{
|
|
|
|
ListDatabases(instigator);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TCREATE)))
|
|
|
|
{
|
|
|
|
databaseName = commandParameters.GetText(T(TDATABASE_NAME));
|
|
|
|
CreateDatabase(instigator, databaseName);
|
|
|
|
_.memory.Free(databaseName);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (subCommand.Compare(T(TDELETE)))
|
|
|
|
{
|
|
|
|
databaseName = commandParameters.GetText(T(TDATABASE_NAME));
|
|
|
|
DeleteDatabase(instigator, databaseName);
|
|
|
|
_.memory.Free(databaseName);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// json pointer as `Text` -> `DBPointerPair` representation converter method
|
|
|
|
private function DBPointerPair TryLoadingDB(BaseText databaseLink)
|
|
|
|
{
|
|
|
|
local DBPointerPair result;
|
|
|
|
if (databaseLink == none) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
result.database = _.db.Load(databaseLink);
|
|
|
|
if (result.database == none) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
result.pointer = _.db.GetPointer(databaseLink);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function CreateDatabase(EPlayer instigator, Text databaseName)
|
|
|
|
{
|
|
|
|
if (instigator == none) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (_.db.ExistsLocal(databaseName))
|
|
|
|
{
|
|
|
|
callerConsole.WriteLine(T(TDB_ALREADY_EXISTS));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (_.db.NewLocal(databaseName) != none) {
|
|
|
|
callerConsole.WriteLine(T(TDB_CREATED));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
callerConsole.WriteLine(T(TDB_CANNOT_BE_CREATED));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function DeleteDatabase(EPlayer instigator, Text databaseName)
|
|
|
|
{
|
|
|
|
if (instigator == none) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (_.db.DeleteLocal(databaseName)) {
|
|
|
|
callerConsole.WriteLine(T(TDA_DELETED));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
callerConsole.WriteLine(T(TDB_DOESNT_EXIST));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function ListDatabases(EPlayer instigator)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local array<Text> availableDatabases;
|
|
|
|
local ConsoleWriter console;
|
|
|
|
|
|
|
|
if (instigator == none) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
availableDatabases = _.db.ListLocal();
|
|
|
|
console = callerConsole;
|
|
|
|
console.Write(T(TAVAILABLE_DATABASES));
|
|
|
|
for (i = 0; i < availableDatabases.length; i += 1)
|
|
|
|
{
|
|
|
|
if (i > 0) {
|
|
|
|
console.ResetColor().Write(P(", "));
|
|
|
|
}
|
|
|
|
console.UseColor(_.color.TextSubtle).Write(availableDatabases[i]);
|
|
|
|
}
|
|
|
|
console.ResetColor().Flush();
|
|
|
|
_.memory.FreeMany(availableDatabases);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function OutputStatus(
|
|
|
|
EPlayer instigator,
|
|
|
|
Database.DBQueryResult error)
|
|
|
|
{
|
|
|
|
if (instigator == none) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (error == DBR_Success) {
|
|
|
|
instigator.BorrowConsole().WriteLine(T(TQUERY_COMPLETED));
|
|
|
|
}
|
|
|
|
if (error == DBR_InvalidPointer) {
|
|
|
|
instigator.BorrowConsole().WriteLine(T(TQUERY_INVALID_POINTER));
|
|
|
|
}
|
|
|
|
if (error == DBR_InvalidDatabase) {
|
|
|
|
instigator.BorrowConsole().WriteLine(T(TQUERY_INVALID_DB));
|
|
|
|
}
|
|
|
|
if (error == DBR_InvalidData) {
|
|
|
|
instigator.BorrowConsole().WriteLine(T(TQUERY_INVALID_DATA));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function DisplayData(
|
|
|
|
Database.DBQueryResult result,
|
|
|
|
AcediaObject data,
|
|
|
|
Database source)
|
|
|
|
{
|
|
|
|
local Text printedJSON;
|
|
|
|
local EPlayer instigator;
|
|
|
|
|
|
|
|
instigator = PopPlayer(source);
|
|
|
|
OutputStatus(instigator, result);
|
|
|
|
if (instigator != none && result == DBR_Success)
|
|
|
|
{
|
|
|
|
printedJSON = _.json.PrettyPrint(data).IntoText();
|
|
|
|
instigator.BorrowConsole().Write(printedJSON).Flush();
|
|
|
|
_.memory.Free(printedJSON);
|
|
|
|
_.memory.Free(instigator);
|
|
|
|
instigator = none;
|
|
|
|
}
|
|
|
|
_.memory.Free(data);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function DisplaySize(
|
|
|
|
Database.DBQueryResult result,
|
|
|
|
int size,
|
|
|
|
Database source)
|
|
|
|
{
|
|
|
|
local Text sizeAsText;
|
|
|
|
local EPlayer instigator;
|
|
|
|
|
|
|
|
instigator = PopPlayer(source);
|
|
|
|
OutputStatus(instigator, result);
|
|
|
|
if (instigator != none && result == DBR_Success)
|
|
|
|
{
|
|
|
|
sizeAsText = _.text.FromInt(size);
|
|
|
|
instigator.BorrowConsole()
|
|
|
|
.Write(T(TOBJECT_SIZE_IS))
|
|
|
|
.Write(sizeAsText)
|
|
|
|
.Flush();
|
|
|
|
_.memory.Free(sizeAsText);
|
|
|
|
_.memory.Free(instigator);
|
|
|
|
instigator = none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function DisplayKeys(
|
|
|
|
Database.DBQueryResult result,
|
|
|
|
ArrayList keys,
|
|
|
|
Database source)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local Text nextKey;
|
|
|
|
local EPlayer instigator;
|
|
|
|
local ConsoleWriter console;
|
|
|
|
|
|
|
|
instigator = PopPlayer(source);
|
|
|
|
OutputStatus(instigator, result);
|
|
|
|
if (keys == none) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (instigator != none && result == DBR_Success)
|
|
|
|
{
|
|
|
|
console = instigator.BorrowConsole();
|
|
|
|
console.Write(T(TOBJECT_KEYS_ARE));
|
|
|
|
for (i = 0; i < keys.GetLength(); i += 1)
|
|
|
|
{
|
|
|
|
if (i > 0) {
|
|
|
|
console.ResetColor().Write(P(", "));
|
|
|
|
}
|
|
|
|
nextKey = keys.GetText(i);
|
|
|
|
console.UseColor(_.color.jPropertyName).Write(nextKey);
|
|
|
|
_.memory.Free(nextKey);
|
|
|
|
}
|
|
|
|
console.Flush();
|
|
|
|
_.memory.Free(instigator);
|
|
|
|
instigator = none;
|
|
|
|
}
|
|
|
|
_.memory.Free(keys);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function DisplayResponse(
|
|
|
|
Database.DBQueryResult result,
|
|
|
|
Database source)
|
|
|
|
{
|
|
|
|
local EPlayer instigator;
|
|
|
|
|
|
|
|
instigator = PopPlayer(source);
|
|
|
|
OutputStatus(instigator, result);
|
|
|
|
_.memory.Free(instigator);
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultproperties
|
|
|
|
{
|
|
|
|
TCREATE = 0
|
|
|
|
stringConstants(0) = "create"
|
|
|
|
TDELETE = 1
|
|
|
|
stringConstants(1) = "delete"
|
|
|
|
TLIST = 2
|
|
|
|
stringConstants(2) = "list"
|
|
|
|
TREAD = 3
|
|
|
|
stringConstants(3) = "read"
|
|
|
|
TSIZE = 4
|
|
|
|
stringConstants(4) = "size"
|
|
|
|
TKEYS = 5
|
|
|
|
stringConstants(5) = "keys"
|
|
|
|
TREMOVE = 6
|
|
|
|
stringConstants(6) = "remove"
|
|
|
|
TWRITE = 7
|
|
|
|
stringConstants(7) = "write"
|
|
|
|
TINCREMENT = 8
|
|
|
|
stringConstants(8) = "increment"
|
|
|
|
TDATABASE_NAME = 9
|
|
|
|
stringConstants(9) = "databaseName"
|
|
|
|
TDATABASE_LINK = 10
|
|
|
|
stringConstants(10) = "databaseLink"
|
|
|
|
TJSON_VALUE = 11
|
|
|
|
stringConstants(11) = "jsonValue"
|
|
|
|
TOBJECT_KEYS_ARE = 12
|
|
|
|
stringConstants(12) = "{$TextEmphasis Object keys are:} "
|
|
|
|
TOBJECT_SIZE_IS = 13
|
|
|
|
stringConstants(13) = "{$TextEmphasis Object size is:} "
|
|
|
|
TQUERY_COMPLETED = 14
|
|
|
|
stringConstants(14) = "{$TextPositive Database query was completed!}"
|
|
|
|
TQUERY_INVALID_POINTER = 15
|
|
|
|
stringConstants(15) = "{$TextNegative Query was provided with an invalid JSON pointer.}"
|
|
|
|
TQUERY_INVALID_DB = 16
|
|
|
|
stringConstants(16) = "{$TextNegative Operation could not finish because database is damaged and unusable.}"
|
|
|
|
TQUERY_INVALID_DATA = 17
|
|
|
|
stringConstants(17) = "{$TextNegative Query data is invalid.}"
|
|
|
|
TAVAILABLE_DATABASES = 18
|
|
|
|
stringConstants(18) = "{$TextEmphasis Available databases:} "
|
|
|
|
TDA_DELETED = 19
|
|
|
|
stringConstants(19) = "{$TextPositive Database was deleted.}"
|
|
|
|
TDB_DOESNT_EXIST = 20
|
|
|
|
stringConstants(20) = "{$TextNegative Database with specified name does not exist.}"
|
|
|
|
TDB_ALREADY_EXISTS = 21
|
|
|
|
stringConstants(21) = "{$TextNegative Database with specified name already exists.}"
|
|
|
|
TDB_CREATED = 22
|
|
|
|
stringConstants(22) = "{$TextPositive Database was created.}"
|
|
|
|
TDB_CANNOT_BE_CREATED = 23
|
|
|
|
stringConstants(23) = "{$TextNegative Database cannot be created.}"
|
|
|
|
TNO_DEFAULT_COMMAND = 24
|
|
|
|
stringConstants(24) = "{$TextNegative Default command does nothing. Use on of the sub-commands.}"
|
|
|
|
TBAD_DBLINK = 25
|
|
|
|
stringConstants(25) = "{$TextNegative Database could not be read for the specified link.}"
|
|
|
|
}
|