lukujarjestaja/tietokanta.js

276 lines
6.8 KiB
JavaScript

'use strict';
const taulut = {
luokat: 'luokat',
opettajat: 'opettajat',
tilat: 'tilat',
tunnit: 'tunnit',
};
class Transaktio {
peruttu = false;
muutokset = [];
constructor(tietokanta) {
this.tietokanta = tietokanta;
this.seuraavaId = tietokanta.seuraavaId;
this.taulut = new Map;
for (const taulu of tietokanta.taulut.keys()) {
this.taulut.set(taulu, new Map);
}
}
peru() {
this.peruttu = true;
}
hae(taulu, id) {
if (!this.taulut.has(taulu)) {
throw new Error(`ei taulua ${taulu}`);
}
if (this.taulut.get(taulu).has(id)) {
return this.taulut.get(taulu).get(id);
} else {
return this.tietokanta.taulut.get(taulu).get(id);
}
}
lisää(taulu, sisältö) {
if (this.peruttu) {
throw new Error(`yritys lisätä rivi perutussa transaktiossa`);
}
if (!this.taulut.has(taulu)) {
throw new Error(`ei taulua ${taulu}`);
}
const id = this.seuraavaId++;
this.muutokset.push({
taulu,
id,
uusi: sisältö,
});
this.taulut.get(taulu).set(id, sisältö);
return id;
}
poista(taulu, id) {
if (this.peruttu) {
throw new Error(`yritys poistaa rivi perutussa transaktiossa`);
}
const vanha = this.hae(taulu, id);
if (vanha === undefined) {
throw new Error(`ei riviä ${id} taulussa ${taulu}`);
}
this.muutokset.push({
taulu,
id,
vanha,
});
this.taulut.get(taulu).set(id, undefined);
}
suodata(taulu, suodatin) {
if (!this.taulut.has(taulu)) {
throw new Error(`ei taulua ${taulu}`);
}
const suodatetut = [];
for (const [id, sisältö] of this.taulut.get(taulu)) {
// Jos sisältö on undefined, rivi on poistettu, eikä sitä tule ottaa
// huomioon suodatettaessa
if (sisältö !== undefined && suodatin(sisältö)) {
suodatetut.push(id);
}
}
for (const [id, sisältö] of this.tietokanta.taulut.get(taulu)) {
// Älä huomio rivejä, jotka löytyvät transaktion tauluista. Ne on
// joko käsitelty jo edellisessä silmukassa (jos ne on päivitetty)
// tai niitä ei tulisi käsitellä ollenkaan (jos ne on poistettu).
if (!this.taulut.get(taulu).has(id) && suodatin(sisältö)) {
suodatetut.push(id);
}
}
return suodatetut;
}
}
class Tietokanta {
seuraavaId = 0;
taulut = new Map;
historia = [];
static serialisoidusta(serialisoitu) {
const parsittu = JSON.parse(serialisoitu);
const tietokanta = new this;
tietokanta.seuraavaId = parsittu.seuraavaId;
const muutokset = [];
for (const taulu in parsittu.taulut) {
for (let id in parsittu.taulut[taulu]) {
id = Number.parseInt(id);
const sisältö = parsittu.taulut[taulu][id];
tietokanta.taulut.get(taulu).set(id, sisältö);
muutokset.push({taulu, id, uusi: sisältö});
}
}
return [tietokanta, muutokset];
}
constructor() {
for (let taulu in taulut) {
this.taulut.set(taulu, new Map);
}
}
transaktio(funktio) {
const transaktio = new Transaktio(this);
funktio(transaktio);
return [this, this.suorita(transaktio)];
}
suorita(transaktio) {
if (transaktio.peruttu || transaktio.muutokset.length === 0) {
return [];
}
// Varmista, että invariantit ovat yhä totta
for (const {taulu, id, vanha, uusi} of transaktio.muutokset) {
if (uusi === undefined && taulu !== taulut.tunnit) {
// Poistettu luokka, opettaja tai tila ei ole tunnin käytössä
const roikkuvat = transaktio.suodata(taulut.tunnit, (tunti) => {
if (taulu === taulut.luokat) {
return tunti.luokat.includes(id);
} else if (taulu === taulut.opettajat) {
return tunti.opettajat.includes(id);
} else if (taulu === taulut.tilat) {
return tunti.tilat.includes(id);
} else {
throw new Error(`Ei-tunnettu taulu ${taulu}`);
}
});
if (roikkuvat.length !== 0) {
throw new Error(`Yritetty poistaa ${taulu}:${id}, joka on ${roikkuvat} käytössä`);
}
} else if (taulu === taulut.tunnit && uusi !== undefined) {
// Uusi tunti käyttää vain olemassaolevia luokkia, opettajia ja
// tiloja
for (const luokka of uusi.luokat) {
if (transaktio.hae(taulut.luokat, luokka) === undefined) {
throw new Error(`Yritetty luoda tunti ${id} olemattomalla luokalla ${luokka}`);
}
}
for (const opettaja of uusi.opettajat) {
if (transaktio.hae(taulut.opettajat, opettaja) === undefined) {
throw new Error(`Yritetty luoda tunti ${id} olemattomalla opettajalla ${opettaja}`);
}
}
for (const tila of uusi.tilat) {
if (transaktio.hae(taulut.tilat, tila) === undefined) {
throw new Error(`Yritetty luoda tunti ${id} olemattomalla tilalla ${tila}`);
}
}
}
}
// Suorita muutokset
for (const {taulu, id, uusi} of transaktio.muutokset) {
if (uusi !== undefined) {
this.taulut.get(taulu).set(id, uusi);
} else {
this.taulut.get(taulu).delete(id);
}
}
this.historia.push({
muutokset: transaktio.muutokset,
idMuutos: transaktio.seuraavaId - this.seuraavaId,
});
this.seuraavaId = transaktio.seuraavaId;
return transaktio.muutokset;
}
kumoa() {
if (this.historia.length === 0) {
return [this, []];
}
const {muutokset, idMuutos} = this.historia.pop();
this.seuraavaId -= idMuutos;
const kumotut = [];
for (const {taulu, id, vanha, uusi} of muutokset) {
if (vanha !== undefined) {
this.taulut.get(taulu).set(id, vanha);
} else {
this.taulut.get(taulu).delete(id);
}
kumotut.push({
taulu,
id,
vanha: uusi,
uusi: vanha,
});
}
return [this, kumotut];
}
hae(taulu, id) {
if (!this.taulut.has(taulu)) {
throw new Error(`ei taulua ${taulu}`);
}
return this.taulut.get(taulu).get(id);
}
järjestyksessä(taulu, järjestys) {
if (!this.taulut.has(taulu)) {
throw new Error(`ei taulua ${taulu}`);
}
const taulukko = Array.from(this.taulut.get(taulu).entries());
taulukko.sort(([xId, x], [yId, y]) => {
const vertaus = järjestys(x, y);
if (vertaus < 0 || vertaus > 0) {
return vertaus;
} else {
return xId - yId;
}
});
return taulukko.map(([id, _]) => id);
}
suodata(taulu, suodatin) {
if (!this.taulut.has(taulu)) {
throw new Error(`ei taulua ${taulu}`);
}
const suodatetut = [];
for (const [id, sisältö] of this.taulut.get(taulu)) {
if (suodatin(sisältö)) {
suodatetut.push(id);
}
}
return suodatetut;
}
serialisoi() {
return JSON.stringify(this, (avain, arvo) => {
if (avain === 'historia') {
return undefined;
}
if (arvo instanceof Map) {
return Object.fromEntries(arvo.entries());
}
return arvo;
});
}
}
function tallennaTietokanta(tietokanta) {
window.localStorage.setItem('tietokanta', tietokanta.serialisoi());
}
function lataaTietokanta() {
const serialisoitu = window.localStorage.getItem('tietokanta');
if (serialisoitu === null) {
return;
}
let [tietokanta, muutokset] = Tietokanta.serialisoidusta(serialisoitu);
_tietokanta = tietokanta;
suorita([tietokanta, muutokset]);
}