feat: Add routing to web interface and database

This commit is contained in:
2024-06-11 10:39:13 +02:00
parent 06660a2fa1
commit 9100a6e619
20 changed files with 270 additions and 66 deletions

View File

@@ -4,6 +4,7 @@
<data-source name="roommapper@localhost" uuid="469f6bec-f113-4467-9d39-7ac38558045a">
<database-info product="Cassandra" version="3.0.8" jdbc-version="4.2" driver-name="Cassandra JDBC Driver" driver-version="1.4" dbms="CASSANDRA" exact-version="3.0.8" exact-driver-version="1.4">
<identifier-quote-string>&quot;</identifier-quote-string>
<jdbc-catalog-is-schema>true</jdbc-catalog-is-schema>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="exact" />
<secret-storage>master_key</secret-storage>

View File

@@ -8,33 +8,33 @@
<CanLogin>1</CanLogin>
<SuperRole>1</SuperRole>
</role>
<schema id="3" parent="1" name="system_auth">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.SimpleStrategy&apos;, &apos;replication_factor&apos;: &apos;1&apos;}
</Properties>
</schema>
<schema id="4" parent="1" name="system_schema">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.LocalStrategy&apos;}
</Properties>
</schema>
<schema id="5" parent="1" name="eve">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.SimpleStrategy&apos;, &apos;replication_factor&apos;: &apos;1&apos;}
</Properties>
</schema>
<schema id="6" parent="1" name="roommapper">
<schema id="3" parent="1" name="roommapper">
<Current>1</Current>
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.NetworkTopologyStrategy&apos;, &apos;datacenter1&apos;: &apos;1&apos;}
</Properties>
</schema>
<schema id="7" parent="1" name="system_distributed">
<schema id="4" parent="1" name="system">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.LocalStrategy&apos;}
</Properties>
</schema>
<schema id="5" parent="1" name="system_auth">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.SimpleStrategy&apos;, &apos;replication_factor&apos;: &apos;1&apos;}
</Properties>
</schema>
<schema id="6" parent="1" name="system_distributed">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.SimpleStrategy&apos;, &apos;replication_factor&apos;: &apos;3&apos;}
</Properties>
</schema>
<schema id="8" parent="1" name="system">
<schema id="7" parent="1" name="system_distributed_everywhere">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.EverywhereStrategy&apos;}
</Properties>
</schema>
<schema id="8" parent="1" name="system_schema">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.LocalStrategy&apos;}
</Properties>
@@ -44,17 +44,7 @@ replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.LocalStrategy
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.SimpleStrategy&apos;, &apos;replication_factor&apos;: &apos;2&apos;}
</Properties>
</schema>
<schema id="10" parent="1" name="system_distributed_everywhere">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.EverywhereStrategy&apos;}
</Properties>
</schema>
<schema id="11" parent="1" name="discordlogger">
<Properties>durable_writes:true
replication:{&apos;class&apos;: &apos;org.apache.cassandra.locator.SimpleStrategy&apos;, &apos;replication_factor&apos;: &apos;1&apos;}
</Properties>
</schema>
<table id="12" parent="6" name="maps">
<table id="10" parent="3" name="maps">
<Properties>caching:{&apos;keys&apos;: &apos;ALL&apos;, &apos;rows_per_partition&apos;: &apos;ALL&apos;}
compression:{&apos;sstable_compression&apos;: &apos;org.apache.cassandra.io.compress.LZ4Compressor&apos;}
bloom_filter_fp_chance:0.01
@@ -70,26 +60,54 @@ dclocal_read_repair_chance:0
compaction:{&apos;class&apos;: &apos;SizeTieredCompactionStrategy&apos;}
</Properties>
</table>
<column id="13" parent="12" name="id">
<table id="11" parent="3" name="routes">
<Properties>caching:{&apos;keys&apos;: &apos;ALL&apos;, &apos;rows_per_partition&apos;: &apos;ALL&apos;}
compression:{&apos;sstable_compression&apos;: &apos;org.apache.cassandra.io.compress.LZ4Compressor&apos;}
bloom_filter_fp_chance:0.01
default_time_to_live:0
speculative_retry:99.0PERCENTILE
gc_grace_seconds:864000
max_index_interval:2048
memtable_flush_period_in_ms:0
min_index_interval:128
read_repair_chance:0
crc_check_chance:1
dclocal_read_repair_chance:0
compaction:{&apos;class&apos;: &apos;SizeTieredCompactionStrategy&apos;}
</Properties>
</table>
<column id="12" parent="10" name="id">
<DasType>uuid|0s</DasType>
<Position>1</Position>
</column>
<column id="14" parent="12" name="version">
<column id="13" parent="10" name="version">
<DasType>int|0s</DasType>
<Position>2</Position>
</column>
<column id="15" parent="12" name="date">
<column id="14" parent="10" name="date">
<DasType>timestamp|0s</DasType>
<Position>3</Position>
</column>
<column id="16" parent="12" name="objects">
<column id="15" parent="10" name="objects">
<DasType>text|0s</DasType>
<Position>4</Position>
</column>
<key id="17" parent="12" name="primary key">
<key id="16" parent="10" name="primary key">
<Columns>id
version|ASC
date|ASC
</Columns>
</key>
<column id="17" parent="11" name="id">
<DasType>uuid|0s</DasType>
<Position>1</Position>
</column>
<column id="18" parent="11" name="path">
<DasType>text|0s</DasType>
<Position>2</Position>
</column>
<key id="19" parent="11" name="primary key">
<Columns>id
</Columns>
</key>
</database-model>

View File

@@ -9,12 +9,22 @@
<component name="ChangeListManager">
<list default="true" id="79f184c3-e88e-45be-9116-5fa813562754" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/.idea.REST API/.idea/dataSources.local.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.REST API/.idea/dataSources.local.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.REST API/.idea/dataSources/469f6bec-f113-4467-9d39-7ac38558045a.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.REST API/.idea/dataSources/469f6bec-f113-4467-9d39-7ac38558045a.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.idea.REST API/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.REST API/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../Web/public/favicon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/../Web/public/favicon.ico" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../Web/public/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/../Web/public/index.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../Web/src/components/controlField.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/../Web/src/components/controlField.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../Web/src/components/layout.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/../Web/src/components/layout.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../Web/src/components/mapCanvas.jsx" beforeDir="false" afterPath="$PROJECT_DIR$/../Web/src/components/mapCanvas.jsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Console/Program.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console.dll" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console.pdb" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console.pdb" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibParse.dll" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibParse.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibParse.pdb" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibParse.pdb" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibServer.dll" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibServer.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibServer.pdb" beforeDir="false" afterPath="$PROJECT_DIR$/Console/bin/Debug/net8.0/LibServer.pdb" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LibParse/bin/Debug/net8.0/LibParse.dll" beforeDir="false" afterPath="$PROJECT_DIR$/LibParse/bin/Debug/net8.0/LibParse.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LibParse/bin/Debug/net8.0/LibParse.pdb" beforeDir="false" afterPath="$PROJECT_DIR$/LibParse/bin/Debug/net8.0/LibParse.pdb" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LibServer/Http/Request.cs" beforeDir="false" afterPath="$PROJECT_DIR$/LibServer/Http/Request.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LibServer/bin/Debug/net8.0/LibServer.dll" beforeDir="false" afterPath="$PROJECT_DIR$/LibServer/bin/Debug/net8.0/LibServer.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LibServer/bin/Debug/net8.0/LibServer.pdb" beforeDir="false" afterPath="$PROJECT_DIR$/LibServer/bin/Debug/net8.0/LibServer.pdb" afterDir="false" />
<change beforePath="$PROJECT_DIR$/REST API.sln.DotSettings.user" beforeDir="false" afterPath="$PROJECT_DIR$/REST API.sln.DotSettings.user" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -42,34 +52,34 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
".NET Project.Console.executor": "Run",
"ASKED_ADD_EXTERNAL_FILES": "true",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"d452b4e4-aa15-4fa9-847f-33d4d7fab23d.executor": "Run",
"git-widget-placeholder": "master",
"ignore.virus.scanning.warn.message": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "HotReloadSettingsPage",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;.NET Project.Console.executor&quot;: &quot;Run&quot;,
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;d452b4e4-aa15-4fa9-847f-33d4d7fab23d.executor&quot;: &quot;Run&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;HotReloadSettingsPage&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
"keyToStringList": {
"DatabaseDriversLRU": [
"cassandra"
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;cassandra&quot;
],
"rider.external.source.directories": [
"/home/wessel/.config/JetBrains/Rider2024.1/resharper-host/DecompilerCache",
"/home/wessel/.config/JetBrains/Rider2024.1/resharper-host/SourcesCache",
"/home/wessel/.local/share/Symbols/src"
&quot;rider.external.source.directories&quot;: [
&quot;/home/wessel/.config/JetBrains/Rider2024.1/resharper-host/DecompilerCache&quot;,
&quot;/home/wessel/.config/JetBrains/Rider2024.1/resharper-host/SourcesCache&quot;,
&quot;/home/wessel/.local/share/Symbols/src&quot;
]
}
}]]></component>
}</component>
<component name="RunManager">
<configuration name="Console" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Console/bin/Debug/net8.0/Console.exe" />
@@ -118,6 +128,10 @@
<workItem from="1717492595765" duration="6218000" />
<workItem from="1717584416884" duration="5757000" />
<workItem from="1717602912720" duration="1420000" />
<workItem from="1717668473610" duration="971000" />
<workItem from="1717670832494" duration="901000" />
<workItem from="1717673464254" duration="1428000" />
<workItem from="1718093417281" duration="1709000" />
</task>
<servers />
</component>

View File

@@ -19,6 +19,7 @@ internal static class Program {
{ "", new Root() },
{ "database", new RouteDatabase(cassandraSession) },
{ "database/metadata", new RouteMetadata(cassandraSession) },
{ "database/route", new RouteMap(cassandraSession) },
{ "roomba/control", new RouteControl() }
};
@@ -44,5 +45,12 @@ internal static class Program {
Date timestamp,
PRIMARY KEY (Id,Version,Date)
)");
session.Execute(@"
CREATE TABLE IF NOT EXISTS Roommapper.Routes(
Id uuid,
Path text,
PRIMARY KEY (Id)
)");
}
}

View File

@@ -0,0 +1,126 @@
using Cassandra;
using LibParse.Json;
using LibServer.Http;
using LibServer.Router;
using Console.JsonClasses;
namespace Console.Routes;
public class RouteMap(ISession cassandraSession): IRoute {
/// <summary>
/// GET request for the database/mapped route, return a map based on given criteria.
/// </summary>
public HttpResponse Get(HttpRequest request) {
try {
// Prepare the select statement based on the given criteria
// If no criteria is given, throw an exception.
// Using a prepare due to the potential of SQL injection when accepting
// any form of user data.
var queries = new Dictionary<string, Func<string, BoundStatement>> {
["id"] = id => cassandraSession.Prepare(@"SELECT * FROM Roommapper.Routes WHERE Id = ?;").Bind(Guid.Parse(id)),
};
var selectStatement = queries
.Where(query => request.QueryString.ContainsKey(query.Key))
.Select(query => query.Value(request.QueryString[query.Key]))
.FirstOrDefault();
if (selectStatement == null) throw new Exception("No valid search criteria given, give one of (id).");
// Execute the query and return all rows in an array
var rowSet = cassandraSession.Execute(selectStatement);
// Create a list to hold the row data, convert it to a JSON string
var rows = RowsToString(rowSet);
// Return the JSON string to the user
return new HttpResponse(rows);
} catch (Exception ex) {
// Return error message if failed for any reason
return new HttpResponse($"{{\"message\": \"{ex.Message.Replace("\"", "\\\"")}\"}}", 400);
}
}
/// <summary>
/// POST request for the database route, insert a new map containing
/// object data into the database.
/// </summary>
public HttpResponse Post(HttpRequest request) {
try {
// Parse request body to Data object, give error if non-nullable fields
// are null.
var parsedBody = request.Body?.FromJson<Data>();
if (parsedBody?.objects == null) {
throw new Exception("objectData is null");
}
// Prepare the insert statement, bind the values, and execute the query
// Using a prepare due to the potential of SQL injection when accepting
// any form of user data.
var uuid = Guid.Parse(parsedBody.id);
var insertStatement = cassandraSession.Prepare(@"
INSERT INTO Roommapper.Routes(Id, path)
VALUES (?, ?);
").Bind(uuid, parsedBody.objects);
cassandraSession.Execute(insertStatement);
// Return success message with the UUID of the inserted row
return new HttpResponse($"{{\"message\":\"success\",\"id\":\"{uuid}\"}}");
}
catch (Exception ex) {
// Return error message if failed for any reason
return new HttpResponse($"{{\"message\": \"{ex.Message.Replace("\"", "\\\"")}\"}}", 400);
}
}
/// <summary>
/// Deletes a given row
/// </summary>
public HttpResponse Delete(HttpRequest request) {
try {
var parsedBody = request.Body?.FromJson<Data>();
// Prepare the select statement based on the given criteria
// If no criteria is given, throw an exception.
// Using a prepare due to the potential of SQL injection when accepting
// any form of user data.
var selectStatement =
parsedBody?.id != null ? cassandraSession.Prepare(@"SELECT * FROM Roommapper.Routes WHERE Id = ?;")
.Bind(Guid.Parse(parsedBody.id)) :
throw new Exception("No valid search criteria given, give one of (id).");
// Execute the query and return all rows in an array
var rowSet = cassandraSession.Execute(selectStatement);
if (rowSet.IsExhausted()) {
throw new Exception("No rows found with the given criteria.");
}
// Prepare and execute delete statement
var deleteStatement = cassandraSession.Prepare(@"DELETE FROM Roommapper.Routes WHERE Id = ?;")
.Bind(Guid.Parse(parsedBody.id));
cassandraSession.Execute(deleteStatement);
return new HttpResponse($"{{\"message\":\"success\",\"entry\":{RowsToString(rowSet)}}}");
} catch (Exception ex) {
return new HttpResponse($"{{\"message\": \"{ex.Message.Replace("\"", "\\\"")}\"}}", 400);
}
}
/// <summary>
/// Casts a set of `RowSet` into a valid JSON string.
/// </summary>
/// <param name="rowSet">The `RowSet` to cast.</param>
/// <returns>A valid JSON string formed from `rowSet`.</returns>
private string RowsToString(RowSet rowSet) {
return rowSet.Select(row => new RowData {
Id = row.GetValue<Guid>("id").ToString(),
Objects = $"[{row.GetValue<string>("path")}]".FromJson<int[][]>()
}).ToList().ToJson();
}
}

View File

@@ -47,7 +47,6 @@ public class HttpRequest {
catch { return -1; }
}
var querySplit = Route.Split("?");
if (querySplit.Length == 1) return 0;

View File

@@ -1,5 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">INFO</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d452b4e4_002Daa15_002D4fa9_002D847f_002D33d4d7fab23d/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="ParseJsonToAnonymousObject" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Project Location="C:\Users\wesse\OneDrive - Hogeschool Inholland\roommapper\src\Server\Tests" Presentation="&amp;lt;Tests&amp;gt;" /&gt;&#xD;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d452b4e4_002Daa15_002D4fa9_002D847f_002D33d4d7fab23d/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="ParseJsonToAnonymousObject" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;Project Location="C:\Users\wesse\OneDrive - Hogeschool Inholland\roommapper\src\Server\Tests" Presentation="&amp;lt;Tests&amp;gt;" /&gt;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View File

@@ -51,11 +51,30 @@ export default class MapCanvas extends React.Component {
}
}
async getRoutePoints() {
try {
const { searchOption, inputValue } = this.state;
const url = `${API_ENDPOINT}/database/route?id=${inputValue}`;
const data = await (await fetch(url)).json();
// Combine all found sets into a singular array.
const map = [];
if (!Array.isArray(data) || data.length < 1) return map;
data.forEach((set) => set.Objects.forEach((coord) => map.push(coord)));
return map;
} catch (ex) {
return;
// this.props.onError(ex);
}
}
async draw() {
// Get the 2D context from the canvas element
const ctx = this.canvasRef.current.getContext('2d');
// Get the points to draw from the server
const points = await this.getLinePoints();
const route = await this.getRoutePoints();
// Set the font for the text we will draw on the canvas
ctx.font = "24px Comic Sans MS";
@@ -71,11 +90,19 @@ export default class MapCanvas extends React.Component {
// Loop trough all coordinates, draw a dot at each point to form a top-down
// view of the objects.
ctx.fillStyle = "#000";
points.forEach(point => {
ctx.beginPath();
ctx.arc(point[1], point[0], 1, 0, 2 * Math.PI);
ctx.fill();
});
ctx.fillStyle = "#800000";
route.forEach(point => {
ctx.beginPath();
ctx.arc(point[1], point[0], 1, 0, 2 * Math.PI);
ctx.fill();
});
}
render() {