lab 05
This commit is contained in:
16 files changed
+1171
No files matched your search
@@ -0,0 +1,275 @@
|
||||
import { STRINGS } from "../lang/messages/en/user.js";
|
||||
|
||||
// server 2 lives on its own origin, change this one line when it moves
|
||||
const API_BASE_URL = "http://localhost:8080";
|
||||
const SQL_ROUTE = "/lab5/api/v1/sql";
|
||||
|
||||
const SELECT_PATTERN = /^select\b/i;
|
||||
const INSERT_PATTERN = /^insert\b/i;
|
||||
|
||||
class SqlStatement {
|
||||
constructor(text) {
|
||||
this.text = text.trim().replace(/;+$/, "").trim();
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this.text.length === 0;
|
||||
}
|
||||
|
||||
isSelect() {
|
||||
return SELECT_PATTERN.test(this.text);
|
||||
}
|
||||
|
||||
isInsert() {
|
||||
return INSERT_PATTERN.test(this.text);
|
||||
}
|
||||
|
||||
// a semicolon inside a quoted literal is data, not a second statement
|
||||
isSingleStatement() {
|
||||
const withoutLiterals = this.text
|
||||
.replace(/'(?:[^']|'')*'/g, "''")
|
||||
.replace(/"(?:[^"]|"")*"/g, '""');
|
||||
|
||||
return !withoutLiterals.includes(";");
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
constructor(baseUrl, route) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.route = route;
|
||||
}
|
||||
|
||||
getRouteUrl() {
|
||||
return `${this.baseUrl}${this.route}`;
|
||||
}
|
||||
|
||||
buildQueryUrl(statement) {
|
||||
return `${this.getRouteUrl()}/${encodeURIComponent(statement.toString())}`;
|
||||
}
|
||||
|
||||
select(statement) {
|
||||
return this.send(this.buildQueryUrl(statement), {
|
||||
method: "GET",
|
||||
});
|
||||
}
|
||||
|
||||
insert(statement) {
|
||||
return this.send(this.getRouteUrl(), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: statement.toString() }),
|
||||
});
|
||||
}
|
||||
|
||||
// an empty body tells server 2 to insert the four sample patients
|
||||
insertSamplePatients() {
|
||||
return this.send(this.getRouteUrl(), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
async send(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const body = await response.text();
|
||||
|
||||
let payload;
|
||||
|
||||
try {
|
||||
payload = JSON.parse(body);
|
||||
} catch {
|
||||
payload = { success: false, error: body };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
payload: payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ResponseView {
|
||||
constructor(responseElement, statusElement) {
|
||||
this.responseElement = responseElement;
|
||||
this.statusElement = statusElement;
|
||||
}
|
||||
|
||||
showStatus(message) {
|
||||
this.statusElement.textContent = message;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.statusElement.textContent = "";
|
||||
this.responseElement.replaceChildren();
|
||||
}
|
||||
|
||||
showMessage(message, className) {
|
||||
const paragraph = document.createElement("p");
|
||||
paragraph.classList.add(className);
|
||||
paragraph.textContent = message;
|
||||
|
||||
this.responseElement.replaceChildren(paragraph);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.showMessage(message, "error-message");
|
||||
}
|
||||
|
||||
showRows(rows) {
|
||||
if (rows.length === 0) {
|
||||
this.showMessage(STRINGS.NO_ROWS, "empty-message");
|
||||
return;
|
||||
}
|
||||
|
||||
this.responseElement.replaceChildren(this.buildTable(rows));
|
||||
}
|
||||
|
||||
buildTable(rows) {
|
||||
const table = document.createElement("table");
|
||||
const head = document.createElement("thead");
|
||||
const body = document.createElement("tbody");
|
||||
const columns = Object.keys(rows[0]);
|
||||
|
||||
table.classList.add("result-table");
|
||||
head.appendChild(this.buildRow(columns, "th", (column) => column));
|
||||
|
||||
for (const row of rows) {
|
||||
body.appendChild(this.buildRow(columns, "td", (column) => row[column]));
|
||||
}
|
||||
|
||||
table.append(head, body);
|
||||
return table;
|
||||
}
|
||||
|
||||
buildRow(columns, cellTag, getValue) {
|
||||
const tableRow = document.createElement("tr");
|
||||
|
||||
for (const column of columns) {
|
||||
const cell = document.createElement(cellTag);
|
||||
cell.textContent = this.formatValue(getValue(column));
|
||||
tableRow.appendChild(cell);
|
||||
}
|
||||
|
||||
return tableRow;
|
||||
}
|
||||
|
||||
formatValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return STRINGS.NULL_VALUE;
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
class LabPage {
|
||||
constructor(api) {
|
||||
this.api = api;
|
||||
|
||||
this.insertButton = document.getElementById("insert-button");
|
||||
this.queryButton = document.getElementById("query-button");
|
||||
this.queryInput = document.getElementById("query-input");
|
||||
|
||||
this.view = new ResponseView(
|
||||
document.getElementById("response"),
|
||||
document.getElementById("status")
|
||||
);
|
||||
}
|
||||
|
||||
init() {
|
||||
document.title = STRINGS.PAGE_TITLE;
|
||||
|
||||
document.getElementById("page-title").textContent = STRINGS.PAGE_TITLE;
|
||||
document.getElementById("student-name").textContent = STRINGS.STUDENT_NAME;
|
||||
document.getElementById("insert-heading").textContent = STRINGS.INSERT_HEADING;
|
||||
document.getElementById("insert-description").textContent = STRINGS.INSERT_DESCRIPTION;
|
||||
document.getElementById("query-heading").textContent = STRINGS.QUERY_HEADING;
|
||||
document.getElementById("query-description").textContent = STRINGS.QUERY_DESCRIPTION;
|
||||
document.getElementById("query-label").textContent = STRINGS.QUERY_LABEL;
|
||||
document.getElementById("response-heading").textContent = STRINGS.RESPONSE_HEADING;
|
||||
|
||||
this.insertButton.textContent = STRINGS.INSERT_BUTTON;
|
||||
this.queryButton.textContent = STRINGS.QUERY_BUTTON;
|
||||
this.queryInput.placeholder = STRINGS.QUERY_PLACEHOLDER;
|
||||
|
||||
this.view.showMessage(STRINGS.RESPONSE_EMPTY, "empty-message");
|
||||
|
||||
this.insertButton.addEventListener("click", () => this.handleInsert());
|
||||
this.queryButton.addEventListener("click", () => this.handleQuery());
|
||||
}
|
||||
|
||||
handleInsert() {
|
||||
this.sendRequest(() => this.api.insertSamplePatients());
|
||||
}
|
||||
|
||||
handleQuery() {
|
||||
const statement = new SqlStatement(this.queryInput.value);
|
||||
|
||||
if (statement.isEmpty()) {
|
||||
this.view.clear();
|
||||
this.view.showError(STRINGS.EMPTY_QUERY);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!statement.isSingleStatement()) {
|
||||
this.view.clear();
|
||||
this.view.showError(STRINGS.SINGLE_STATEMENT_ONLY);
|
||||
return;
|
||||
}
|
||||
|
||||
// reads go out as GET, inserts as POST. anything else still goes out
|
||||
// as GET so the read only database user is the one that rejects it.
|
||||
if (statement.isInsert()) {
|
||||
this.sendRequest(() => this.api.insert(statement));
|
||||
} else {
|
||||
this.sendRequest(() => this.api.select(statement));
|
||||
}
|
||||
}
|
||||
|
||||
async sendRequest(request) {
|
||||
this.view.clear();
|
||||
this.view.showStatus(STRINGS.SENDING);
|
||||
this.setBusy(true);
|
||||
|
||||
try {
|
||||
this.showResult(await request());
|
||||
} catch (error) {
|
||||
this.view.showStatus("");
|
||||
this.view.showError(STRINGS.NETWORK_ERROR + error.message);
|
||||
} finally {
|
||||
this.setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
showResult(result) {
|
||||
const payload = result.payload;
|
||||
|
||||
if (!result.ok || payload.success === false) {
|
||||
this.view.showStatus("");
|
||||
this.view.showError(STRINGS.SERVER_ERROR + (payload.error || result.status));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(payload.data)) {
|
||||
this.view.showStatus(STRINGS.ROWS_RETURNED + payload.data.length);
|
||||
this.view.showRows(payload.data);
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.showStatus(STRINGS.ROWS_INSERTED + (payload.rowCount ?? 0));
|
||||
this.view.showMessage(payload.message, "success-message");
|
||||
}
|
||||
|
||||
setBusy(isBusy) {
|
||||
this.insertButton.disabled = isBusy;
|
||||
this.queryButton.disabled = isBusy;
|
||||
}
|
||||
}
|
||||
|
||||
const api = new ApiClient(API_BASE_URL, SQL_ROUTE);
|
||||
new LabPage(api).init();
|
||||
Reference in new issue
Block a user