You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
frontend/src/app/service/api.service.ts

897 lines
27 KiB

import {Injectable} from '@angular/core';
import {environment} from '../../environments/environment';
import {HttpClient} from '@angular/common/http';
import {BehaviorSubject, Observable} from 'rxjs';
import ApiResponse from '../structures/ApiResponse';
import {MenuItem} from './db/MenuItem';
import {DatabaseService} from './db/database.service';
import {ConnectionService} from 'ng-connection-service';
import {Codium} from './db/Codium';
import {Database} from './db/Database';
import {DatabaseColumn} from './db/DatabaseColumn';
import {DatabaseEntry} from './db/DatabaseEntry';
import {FileGroup} from './db/FileGroup';
export class ResourceNotAvailableOfflineError extends Error {
constructor(msg = 'This resource is not yet available offline on this device.') {
super(msg);
}
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
protected baseEndpoint: string = environment.backendBase;
protected statUrl: string = environment.statUrl;
protected versionUrl: string = environment.versionUrl;
protected offline = false;
public readonly offline$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
get isOffline() {
return this.offline;
}
constructor(
protected http: HttpClient,
protected db: DatabaseService,
protected connection: ConnectionService,
) {
connection.monitor().subscribe(isConnected => {
if ( !isConnected ) {
this.makeOffline();
} else {
this.checkOnline().then(isOnline => {
if ( isOnline ) {
this.makeOnline();
} else {
this.makeOffline();
}
});
}
});
this.checkOnline().then(isConnected => {
if ( !isConnected ) {
this.makeOffline();
} else {
this.makeOnline();
}
});
}
public checkOnline(): Promise<boolean> {
return new Promise(res => {
fetch(this.statUrl).then(resp => {
res(resp && (resp.ok || resp.type === 'opaque'));
}).catch(e => {
console.error('Check Online Error', e);
res(false);
});
});
}
public makeOffline() {
this.offline = true;
this.offline$.next(true);
}
public makeOnline() {
this.offline = false;
this.offline$.next(false);
}
public get(endpoint, params = {}): Observable<ApiResponse> {
return this.request(endpoint, params, 'get');
}
public post(endpoint, body = {}): Observable<ApiResponse> {
return this.request(endpoint, body, 'post');
}
public request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable<ApiResponse> {
return this._request(this._build_url(endpoint), params, method);
}
public stat(): Observable<ApiResponse> {
return new Observable<ApiResponse>(sub => {
(async () => {
const statKV = await this.db.getKeyValue('host_stat');
// If offline, look up the last stored stat for information
if ( this.isOffline ) {
if ( typeof statKV !== 'object' ) {
throw new Error('No locally stored host stat found.');
}
sub.next(new ApiResponse(statKV.data));
sub.complete();
}
// Otherwise, fetch the stat and cache it locally
this._request(this.statUrl).subscribe(apiResponse => {
statKV.data = {status: apiResponse.status, message: apiResponse.message, data: apiResponse.data};
statKV.save().then(() => {
sub.next(statKV.data);
sub.complete();
});
});
})();
});
}
public version(): Promise<string> {
return new Promise(async (res, rej) => {
const versionKV = await this.db.getKeyValue('app_version');
// If offline, look up the local app version.
if ( this.isOffline ) {
if ( versionKV ) {
return res(versionKV.data);
} else {
return rej(new Error('No local app version found.'));
}
}
// Otherwise, look up the app version and store it locally
this._request(this.versionUrl).subscribe({
next: async result => {
const version = result.data.text.trim();
versionKV.data = version;
await versionKV.save();
res(version);
},
error: rej,
});
});
}
protected _request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable<ApiResponse> {
return new Observable<ApiResponse>(sub => {
let data: any = {};
if ( method === 'get' ) {
data.params = params;
} else {
data = params;
}
this.http[method](endpoint, data).subscribe({
next: (response: any) => {
sub.next(new ApiResponse(response));
},
error: (err) => {
const response = {
status: err.status,
message: err.message,
data: err.error,
};
sub.next(new ApiResponse(response));
},
});
});
}
public _build_url(endpoint) {
if ( !endpoint.startsWith('/') ) {
endpoint = `/${endpoint}`;
}
return `${this.baseEndpoint.endsWith('/') ? this.baseEndpoint.slice(0, -1) : this.baseEndpoint}${endpoint}`;
}
public getDeviceToken(): Promise<string> {
return new Promise(async (res, rej) => {
const tokenKV = await this.db.getKeyValue('device_token');
if ( !tokenKV.data && this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
if ( tokenKV.data ) {
const expDate = new Date(tokenKV.data.expiration_date);
if (expDate > new Date()) {
return res(tokenKV.data.token);
}
}
this.get(`/session/device-token`).subscribe({
next: async result => {
tokenKV.data = result.data;
await tokenKV.save();
res(result.data.token);
},
error: rej,
});
});
}
public resumeSession(): Promise<void> {
return new Promise(async (res, rej) => {
if ( !this.isOffline ) {
this.getDeviceToken().then(token => {
this.post(`/session/resume/${token}`).subscribe({
next: result => {
res();
},
error: rej,
});
}).catch(rej);
}
});
}
public getMenuItems(): Promise<any[]> {
return new Promise(async (res, rej) => {
await this.db.createSchemata();
// If offline, fetch the menu from the database
if ( this.isOffline ) {
const items = await this.db.menuItems.toArray();
const nodes = MenuItem.inflateTree(items as MenuItem[]);
return res(nodes);
}
// Download the latest menu items
const tree: any[] = await new Promise(res2 => {
this.get('/menu/items').subscribe({
next: async result => {
const nodes = result.data as any[];
const items = MenuItem.deflateTree(nodes);
// Update the locally stored nodes
await this.db.menuItems.clear();
await Promise.all(items.map(item => item.save()));
res2(nodes);
},
error: rej,
});
});
res(tree);
});
}
public getSessionData(): Promise<any> {
return new Promise(async (res, rej) => {
const sessionKV = await this.db.getKeyValue('session_data');
const authenticatedUserKV = await this.db.getKeyValue('authenticated_user');
// If offline, just return the locally cached session data
if ( this.isOffline ) {
if ( typeof sessionKV.data !== 'object' ) {
return rej(new Error('No locally cached session data found.'));
}
return res(sessionKV.data);
}
// Otherwise, fetch the session data from the server and cache it locally
this.get('/session').subscribe(async result => {
// If the locally logged in user is not the one in the server session, purge everything and reset
if (
authenticatedUserKV.data
&& authenticatedUserKV.data?.user?.id
&& authenticatedUserKV.data.user.id !== result.data.user.id
) {
await this.cleanSession(result.data);
return res(result.data);
}
sessionKV.data = result.data;
await sessionKV.save();
res(sessionKV.data);
});
});
}
protected async cleanSession(data: any) {
await this.db.purge();
await new Promise((res, rej) => {
this.stat().subscribe({
next: res,
error: rej,
});
});
const sessionKV = await this.db.getKeyValue('session_data');
sessionKV.data = data;
await sessionKV.save();
await this.getDeviceToken();
}
public saveSessionData(data: any): Promise<void> {
return new Promise(async (res, rej) => {
// Update the local session data
const sessionKV = await this.db.getKeyValue('session_data');
sessionKV.data = data;
await sessionKV.save();
// If we're not offline, then update the data on the server
if ( !this.isOffline ) {
await new Promise(res2 => {
this.post('/session', data || {}).subscribe({
next: res2,
error: rej,
});
});
}
res();
});
}
public deleteCodium(PageId: string, NodeId: string, CodiumId: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray();
const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined;
if ( this.isOffline ) {
if ( existingLocalCodium ) {
existingLocalCodium.deleted = true;
existingLocalCodium.needsServerUpdate = true;
await existingLocalCodium.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/code/${PageId}/${NodeId}/delete/${CodiumId}`).subscribe({
next: async result => {
if ( existingLocalCodium ) {
await this.db.codiums.delete(existingLocalCodium.id);
}
res();
},
error: rej,
});
});
}
public saveCodium(PageId: string, NodeId: string, CodiumId: string, data: any): Promise<any> {
return new Promise(async (res, rej) => {
const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray();
const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined;
// If we're offline, update or create the local record
if ( this.isOffline ) {
if ( existingLocalCodium ) {
existingLocalCodium.fillFromRecord(data);
existingLocalCodium.needsServerUpdate = true;
await existingLocalCodium.save();
return res(existingLocalCodium.getSaveRecord());
} else {
const newLocalCodium = new Codium(
data.Language,
NodeId,
PageId,
data.code,
data.UUID || Codium.getUUID(),
true,
);
await newLocalCodium.save();
return res(newLocalCodium.getSaveRecord());
}
}
// If we're online, save the data and update our local records
this.post(`/code/${PageId}/${NodeId}/set/${CodiumId}`, data).subscribe({
next: async result => {
if ( existingLocalCodium ) {
existingLocalCodium.fillFromRecord(result.data);
existingLocalCodium.needsServerUpdate = false;
await existingLocalCodium.save();
return res(result.data);
} else {
const newLocalCodium = new Codium(
result.data.Language,
result.data.NodeId,
result.data.PageId,
result.data.code,
result.data.UUID,
);
await newLocalCodium.save();
return res(result.data);
}
},
error: rej,
});
});
}
public createCodium(PageId: string, NodeId: string): Promise<any> {
return new Promise(async (res, rej) => {
// If offline, create a new local DB record
if ( this.isOffline ) {
const newLocalCodium = new Codium(
'javascript',
NodeId,
PageId,
'',
Codium.getUUID(),
true
);
await newLocalCodium.save();
return res(newLocalCodium.getSaveRecord());
}
// If online, create a new record on the server and sync it to the local db
this.post(`/code/${PageId}/${NodeId}/create`).subscribe({
next: async result => {
const newLocalCodium = new Codium(
result.data.Language,
result.data.NodeId,
result.data.PageId,
result.data.code,
result.data.UUID,
);
await newLocalCodium.save();
res(result.data);
},
error: rej,
});
});
}
public getCodium(PageId: string, NodeId: string, CodiumId: string): Promise<any> {
return new Promise(async (res, rej) => {
const existingLocalCodiums = await this.db.codiums.where({ UUID: CodiumId }).toArray();
const existingLocalCodium = existingLocalCodiums.length > 0 ? existingLocalCodiums[0] as Codium : undefined;
// If offline, try to load it from the local DB
if ( this.isOffline ) {
if ( existingLocalCodium ) {
return res(existingLocalCodium.getSaveRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
// If online, fetch the codium and store/update it locally
this.get(`/code/${PageId}/${NodeId}/get/${CodiumId}`).subscribe({
next: async result => {
if ( existingLocalCodium ) {
existingLocalCodium.fillFromRecord(result.data);
await existingLocalCodium.save();
return res(result.data);
} else {
const newLocalCodium = new Codium(
result.data.Language,
result.data.NodeId,
result.data.PageId,
result.data.code,
result.data.UUID
);
await newLocalCodium.save();
return res(result.data);
}
},
error: rej,
});
});
}
public deleteDatabase(PageId: string, NodeId: string, DatabaseId: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingLocalDatabase = await this.db.databases.where({ UUID: DatabaseId }).first() as Database;
if ( this.isOffline ) {
if ( existingLocalDatabase ) {
existingLocalDatabase.needsServerUpdate = true;
existingLocalDatabase.deleted = true;
await existingLocalDatabase.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/db/${PageId}/${NodeId}/drop/${DatabaseId}`).subscribe({
next: async result => {
if ( existingLocalDatabase ) {
await this.db.databases.delete(existingLocalDatabase.id);
res();
}
},
error: rej,
});
});
}
public getDatabaseEntries(PageId: string, NodeId: string, DatabaseId: string): Promise<any[]> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
const rows = (await this.db.databaseEntries.where({ DatabaseId }).toArray()) as DatabaseEntry[];
return res(rows.filter(x => !x.deleted).map(x => x.inflateToRecord()));
}
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}/data`).subscribe({
next: async result => {
for ( const row of result.data ) {
const existingDatabaseEntry = await this.db.databaseEntries.where({
DatabaseId, UUID: row.UUID,
}).first() as DatabaseEntry;
if ( existingDatabaseEntry ) {
existingDatabaseEntry.fillFromRecord(row);
await existingDatabaseEntry.save();
} else {
const newDatabaseEntry = new DatabaseEntry(
row.DatabaseId,
JSON.stringify(row.RowData),
row.UUID
);
await newDatabaseEntry.save();
}
}
return res(result.data);
},
error: rej,
});
});
}
public getDatabaseColumns(PageId: string, NodeId: string, DatabaseId: string): Promise<any[]> {
return new Promise(async (res, rej) => {
// If offline, fetch the columns from the local database
if ( this.isOffline ) {
const columns = (await this.db.databaseColumns.where({ DatabaseId }).toArray()) as DatabaseColumn[];
return res(columns.filter(x => !x.deleted).map(x => x.getSaveRecord()));
}
// If online, fetch the columns and sync the local database
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}/columns`).subscribe({
next: async results => {
for ( const def of results.data ) {
const existingColumnDef = await this.db.databaseColumns.where({
DatabaseId, UUID: def.UUID,
}).first() as DatabaseColumn;
if ( existingColumnDef ) {
existingColumnDef.fillFromRecord(def);
await existingColumnDef.save();
} else {
const newColumnDef = new DatabaseColumn(
def.headerName,
def.field,
def.DatabaseId,
def.UUID,
def.Type,
def.additionalData,
);
await newColumnDef.save();
}
}
return res(results.data);
},
error: rej,
});
});
}
public createDatabase(PageId: string, NodeId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
const newLocalDatabase = new Database(
'New Database',
NodeId,
PageId,
[],
Database.getUUID(),
true,
true
);
await newLocalDatabase.save();
return res(newLocalDatabase.getSaveRecord());
}
this.post(`/db/${PageId}/${NodeId}/create`).subscribe({
next: async result => {
const newLocalDatabase = new Database(
result.data.Name,
result.data.NodeId,
result.data.PageId,
result.data.ColumnIds,
result.data.UUID,
result.data.Active
);
await newLocalDatabase.save();
return res(result.data);
},
error: rej,
});
});
}
public getDatabase(PageId: string, NodeId: string, DatabaseId: string): Promise<any> {
return new Promise(async (res, rej) => {
const existingLocalDatabases = await this.db.databases.where({ UUID: DatabaseId }).toArray();
const existingLocalDatabase = existingLocalDatabases.length > 0 ? existingLocalDatabases[0] as Database : undefined;
if ( this.isOffline ) {
if ( existingLocalDatabase ) {
return res(existingLocalDatabase.getSaveRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}`).subscribe({
next: async result => {
if ( existingLocalDatabase ) {
existingLocalDatabase.fillFromRecord(result.data);
await existingLocalDatabase.save();
return res(result.data);
} else {
const newLocalDatabase = new Database(
result.data.Name,
result.data.NodeId,
result.data.PageId,
result.data.ColumnIds,
result.data.UUID,
result.data.Active
);
await newLocalDatabase.save();
return res(result.data);
}
},
error: rej,
});
});
}
public saveDatabaseEntries(PageId: string, NodeId: string, DatabaseId: string, rowData: any[]): Promise<any[]> {
return new Promise(async (res, rej) => {
const existingDatabaseEntries = await this.db.databaseEntries.where({ DatabaseId }).toArray() as DatabaseEntry[];
if ( this.isOffline ) {
const returnData = [];
for ( const entry of existingDatabaseEntries ) {
entry.deleted = true;
entry.needsServerUpdate = true;
await entry.save();
}
for ( const row of rowData ) {
const newDatabaseEntry = new DatabaseEntry(
row.DatabaseId,
JSON.stringify(row.RowData),
row.UUID || DatabaseEntry.getUUID()
);
await newDatabaseEntry.save();
returnData.push(newDatabaseEntry.inflateToRecord());
}
return res(returnData);
}
this.post(`/db/${PageId}/${NodeId}/set/${DatabaseId}/data`, rowData).subscribe({
next: async result => {
await this.db.databaseEntries.bulkDelete(existingDatabaseEntries.map(x => x.id));
for ( const row of result.data ) {
const newDatabaseEntry = new DatabaseEntry(
row.DatabaseId,
JSON.stringify(row.RowData),
row.UUID
);
await newDatabaseEntry.save();
}
return res(result.data);
},
error: rej,
});
});
}
public saveDatabaseColumns(PageId: string, NodeId: string, DatabaseId: string, columns: any[]): Promise<void> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
for ( const def of columns ) {
const existingColumnDef = await this.db.databaseColumns.where({
DatabaseId, ...(def.UUID ? { UUID: def.UUID } : { headerName: def.headerName }),
}).first() as DatabaseColumn;
if ( existingColumnDef ) {
existingColumnDef.fillFromRecord(def);
existingColumnDef.needsServerUpdate = true;
await existingColumnDef.save();
} else {
const newColumnDef = new DatabaseColumn(
def.headerName,
def.field,
def.DatabaseId,
def.UUID || DatabaseColumn.getUUID(),
def.Type,
def.additionalData,
);
await newColumnDef.save();
}
}
return res();
}
this.post(`/db/${PageId}/${NodeId}/set/${DatabaseId}/columns`, { columns }).subscribe({
next: async results => {
for ( const def of results.data ) {
const existingColumnDef = await this.db.databaseColumns.where({
DatabaseId, UUID: def.UUID,
}).first() as DatabaseColumn;
if ( existingColumnDef ) {
existingColumnDef.fillFromRecord(def);
existingColumnDef.needsServerUpdate = false;
await existingColumnDef.save();
} else {
const newColumnDef = new DatabaseColumn(
def.headerName,
def.field,
def.DatabaseId,
def.UUID,
def.Type,
def.additionalData,
);
await newColumnDef.save();
}
}
res();
},
error: rej,
});
});
}
public saveDatabaseName(PageId: string, NodeId: string, DatabaseId: string, name: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingDatabase = await this.db.databases.where({ UUID: DatabaseId }).first() as Database;
if ( this.isOffline ) {
if ( existingDatabase ) {
existingDatabase.Name = name;
existingDatabase.needsServerUpdate = true;
await existingDatabase.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/db/${PageId}/${NodeId}/set/${DatabaseId}/name`, { Name: name }).subscribe({
next: async result => {
if ( existingDatabase ) {
existingDatabase.Name = name;
await existingDatabase.save();
res();
}
},
error: rej,
});
});
}
public deleteFileGroup(PageId: string, NodeId: string, FileGroupId: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingFileGroup = await this.db.fileGroups.where({ UUID: FileGroupId }).first() as FileGroup;
if ( this.isOffline ) {
if ( existingFileGroup ) {
existingFileGroup.deleted = true;
existingFileGroup.needsServerUpdate = true;
await existingFileGroup.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/files/${PageId}/${NodeId}/delete/${FileGroupId}`).subscribe({
next: async result => {
if ( existingFileGroup ) {
await this.db.fileGroups.delete(existingFileGroup.id);
res();
}
},
error: rej,
});
});
}
public createFileGroup(PageId: string, NodeId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
const newFileGroup = new FileGroup(
NodeId,
PageId,
[],
JSON.stringify([]),
FileGroup.getUUID(),
true
);
await newFileGroup.save();
return res(newFileGroup.inflateToRecord());
}
this.post(`/files/${PageId}/${NodeId}/create`).subscribe({
next: async result => {
const newFileGroup = new FileGroup(
result.data.NodeId,
result.data.PageId,
result.data.FileIds,
JSON.stringify(result.data.files),
result.data.UUID
);
await newFileGroup.save();
res(result.data);
},
error: rej,
});
});
}
public getFileGroup(PageId: string, NodeId: string, FileGroupId: string): Promise<any> {
return new Promise(async (res, rej) => {
const existingFileGroup = await this.db.fileGroups.where({ UUID: FileGroupId }).first() as FileGroup;
if ( this.isOffline ) {
if ( existingFileGroup ) {
return res(existingFileGroup.inflateToRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.get(`/files/${PageId}/${NodeId}/get/${FileGroupId}`).subscribe({
next: async result => {
if ( existingFileGroup ) {
existingFileGroup.fillFromRecord(result.data);
existingFileGroup.needsServerUpdate = false;
await existingFileGroup.save();
} else {
const newFileGroup = new FileGroup(
result.data.NodeId,
result.data.PageId,
result.data.FileIds,
JSON.stringify(result.data.files),
result.data.UUID
);
await newFileGroup.save();
}
res(result.data);
},
error: rej,
});
});
}
}