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

1575 lines
46 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';
import {Page} from './db/Page';
import {PageNode} from './db/PageNode';
import {debug} from '../utility';
import HostRecord from '../structures/HostRecord';
import {NodeTypeIcons} from '../structures/node-types';
export class ResourceNotAvailableOfflineError extends Error {
constructor(msg = 'This resource is not yet available offline on this device.') {
super(msg);
}
}
export class OfflinePrefetchWouldOverwriteLocalDataError extends Error {
constructor(msg = 'Cannot run offline prefetch. There is un-synced local data that would be overwritten.') {
super(msg);
}
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
public isAuthenticated = false;
public isPublicUser = false;
public systemBase?: string;
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,
) {
this.startOfflineObserver();
}
protected stopOfflineObserverClosure?: any;
public stopOfflineObserver() {
if ( this.stopOfflineObserverClosure ) {
this.stopOfflineObserverClosure();
}
}
protected startOfflineObserver() {
const passiveCheckTime = 120;
const checkTimes = [5, 5, 10, 10, 15, 15, 20, 20, 30, 30, 30, 60, 60, 500];
let currentCheckTimeIndex = 0;
let hasNetConnection = true;
let hasServerConnection = true;
let online = true;
let passiveCheckInterval;
let activeCheckInterval;
let stopped = false;
this.stopOfflineObserverClosure = () => {
stopped = true;
clearInterval(passiveCheckInterval);
clearInterval(activeCheckInterval);
};
const checkServerConnection = async () => {
if ( !hasNetConnection ) {
return false;
}
return this.checkOnline();
};
const startPassiveCheck = () => {
passiveCheckInterval = setInterval(async () => {
if ( hasNetConnection ) {
const server = await checkServerConnection();
if ( server !== hasServerConnection ) {
hasServerConnection = server;
await handleNetConnectionEvent();
}
}
}, passiveCheckTime * 1000);
};
const doActiveCheck = async () => {
if ( activeCheckInterval ) {
clearInterval(activeCheckInterval);
if ( currentCheckTimeIndex < (checkTimes.length - 1) ) {
currentCheckTimeIndex += 1;
}
}
if ( hasNetConnection ) {
const server = await checkServerConnection();
if ( server !== hasServerConnection ) {
hasServerConnection = server;
await handleNetConnectionEvent();
} else {
if ( activeCheckInterval ) {
activeCheckInterval = setInterval(doActiveCheck, checkTimes[currentCheckTimeIndex] * 1000);
}
}
}
};
const handleNetConnectionEvent = async () => {
if ( stopped ) {
return;
}
if ( online && (!hasNetConnection || !hasServerConnection) ) {
online = false;
await this.makeOffline();
clearInterval(passiveCheckInterval);
activeCheckInterval = setInterval(doActiveCheck, checkTimes[currentCheckTimeIndex] * 1000);
} else if ( !online && (hasNetConnection && hasServerConnection) ) {
if ( activeCheckInterval ) {
clearInterval(activeCheckInterval);
}
online = true;
currentCheckTimeIndex = 0;
await this.makeOnline();
startPassiveCheck();
}
};
this.connection.monitor().subscribe(isConnected => {
hasNetConnection = isConnected;
handleNetConnectionEvent();
});
startPassiveCheck();
this.checkOnline().then(server => {
hasServerConnection = server;
handleNetConnectionEvent();
});
}
public forceRestart() {
window.location.href = `${this.systemBase || '/'}start`;
}
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 async makeOffline() {
const offlineKV = await this.db.getKeyValue('needs_online_sync');
offlineKV.data = true;
await offlineKV.save();
this.offline = true;
this.offline$.next(true);
}
public async needsSync() {
if ( !this.isAuthenticated || this.isPublicUser ) {
return false;
}
const offlineKV = await this.db.getKeyValue('needs_online_sync');
return Boolean(offlineKV.data);
}
public async 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 delete(endpoint, body = {}): Observable<ApiResponse> {
return this.request(endpoint, body, 'delete');
}
public request(endpoint, params = {}, method: 'get'|'post'|'delete' = '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'|'delete' = '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, base = this.baseEndpoint) {
if ( !endpoint.startsWith('/') ) {
endpoint = `/${endpoint}`;
}
return `${base.endsWith('/') ? base.slice(0, -1) : base}${endpoint}`;
}
public async getToken(): Promise<string> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get('token').subscribe({
next: response => {
return res(response.data);
},
error: rej,
});
});
}
public async syncOfflineData() {
const dirtyRecords = await this.db.getDirtyRecords();
const uuidMap = await new Promise(async (res, rej) => {
this.post('/offline/sync', { dirtyRecords }).subscribe({
next: async result => {
res(result.data);
},
error: rej,
});
});
// For now, we're not going to handle this great.
// Rather, we're going to wait until the server supports proper versioning and date-based offline updates.
// In the meantime, just purge everything and reload.
await this.prefetchOfflineData(true);
const offlineKV = await this.db.getKeyValue('needs_online_sync');
offlineKV.data = false;
await offlineKV.save();
// Pre-fetch menu items
await this.getMenuItems();
}
public async prefetchOfflineData(overwriteLocalData = false) {
if ( this.isOffline ) {
throw new ResourceNotAvailableOfflineError();
}
if ( !overwriteLocalData && await this.needsSync() ) {
throw new OfflinePrefetchWouldOverwriteLocalDataError();
}
const data: any = await new Promise((res, rej) => {
this.get('/offline/prefetch').subscribe({
next: result => {
if ( !result.data ) {
return rej(new TypeError('Unable to parse data from API response.'));
}
res(result.data);
},
error: rej,
});
});
if ( Array.isArray(data.pages) ) {
await this.db.pages.clear();
for ( const rec of data.pages ) {
const page = new Page(
rec.UUID,
rec.Name,
rec.OrgUserId,
rec.IsPublic,
rec.IsVisibleInMenu,
rec.ParentId,
rec.NodeIds,
rec.CreatedAt,
rec.UpdatedAt,
rec.Active,
rec.CreatedUserId,
rec.UpdateUserId,
rec.ChildPageIds,
rec.noDelete,
rec.virtual
);
await page.save();
}
}
if ( Array.isArray(data.pageNodes) ) {
await this.db.pageNodes.clear();
for ( const rec of data.pageNodes ) {
const node = new PageNode(
rec.UUID,
rec.Type,
rec.Value ? JSON.stringify(rec.Value) : JSON.stringify({}),
rec.PageId,
rec.CreatedAt,
rec.UpdatedAt,
rec.CreatedUserId,
rec.UpdateUserId
);
await node.save();
}
}
if ( Array.isArray(data.codiums) ) {
await this.db.codiums.clear();
for ( const rec of data.codiums ) {
const code = new Codium(
rec.Language,
rec.NodeId,
rec.PageId,
rec.code,
rec.UUID
);
await code.save();
}
}
if ( Array.isArray(data.databases) ) {
await this.db.databases.clear();
for ( const rec of data.databases ) {
const db = new Database(
rec.Name,
rec.NodeId,
rec.PageId,
rec.ColumnIds,
rec.UUID,
rec.Active
);
await db.save();
}
}
if ( Array.isArray(data.databaseColumns) ) {
await this.db.databaseColumns.clear();
for ( const rec of data.databaseColumns ) {
const col = new DatabaseColumn(
rec.headerName,
rec.field,
rec.DatabaseId,
rec.UUID,
rec.Type,
rec.additionalData
);
await col.save();
}
}
if ( Array.isArray(data.databaseEntries) ) {
await this.db.databaseEntries.clear();
for ( const rec of data.databaseEntries ) {
const entry = new DatabaseEntry(
rec.DatabaseId,
rec.UUID,
JSON.stringify(rec.RowData || {})
);
await entry.save();
}
}
if ( Array.isArray(data.fileGroups) ) {
await this.db.fileGroups.clear();
for ( const rec of data.fileGroups ) {
const group = new FileGroup(
rec.NodeId,
rec.PageId,
rec.FileIds,
JSON.stringify(rec.files || []),
rec.UUID
);
await group.save();
}
}
}
public getDeviceToken(): Promise<string> {
return new Promise(async (res, rej) => {
if ( this.isPublicUser ) {
return res();
}
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 => {
debug('Got device token:', token);
if ( !token ) {
return res();
}
this.post(`/session/resume/${token}`).subscribe({
next: result => {
res();
},
error: rej,
});
}).catch(rej);
}
});
}
public getMenuItems(pageOnly: boolean = false, virtualRootPageId?: string): 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 = pageOnly ? await this.db.menuItems.where({ type: 'page' }).toArray() : await this.db.menuItems.toArray();
const nodes = MenuItem.inflateTree(items as MenuItem[]);
return res(nodes);
}
let loadUrl = '/menu/items';
if ( virtualRootPageId ) {
loadUrl += `?virtualRootPageId=${virtualRootPageId}`;
}
if ( pageOnly ) {
loadUrl += (virtualRootPageId ? '&' : '?') + 'type=page';
}
// Download the latest menu items
const tree: any[] = await new Promise(res2 => {
this.get(loadUrl).subscribe({
next: async result => {
const nodes = this.setMenuItemIconKeys(result.data as any[]);
const items = MenuItem.deflateTree(nodes);
// Update the locally stored nodes
if ( !pageOnly ) {
await this.db.menuItems.clear();
await Promise.all(items.map(item => item.save()));
}
res2(nodes);
},
error: rej,
});
});
res(tree);
});
}
protected setMenuItemIconKeys(nodes: any[]): any[] {
return nodes.map(node => {
node.faIconClass = NodeTypeIcons[node.type];
if ( !node.faIconClass ) {
debug('Unable to map type icon class for menu item type:', node.faIconClass, node);
}
if ( Array.isArray(node.children) ) {
node.children = this.setMenuItemIconKeys(node.children);
}
return node;
});
}
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 async loadNodes(pageId: string, version?: number): Promise<HostRecord[]> {
return new Promise(async (res, rej) => {
const existingNodes = await this.db.pageNodes.where({ PageId: pageId }).toArray() as PageNode[];
const inflateRecords = (records) => {
return records.map(rec => {
const host = new HostRecord(rec.Value.Value);
host.load(rec);
return host;
});
};
// If we're offline, just resolve the offline nodes
if ( this.isOffline ) {
const parsedRecords = existingNodes.map(x => x.inflateToRecord());
return res(inflateRecords(parsedRecords));
}
this.get(`/page/${pageId}/nodes${version ? '?version=' + version : ''}`).subscribe({
next: async result => {
// If we got resolved records, delete the local ones to replace them
await this.db.pageNodes.where({ PageId: pageId }).delete();
for ( const rawRec of result.data ) {
const newLocalNode = new PageNode(
rawRec.UUID,
rawRec.Type,
JSON.stringify(rawRec.Value),
rawRec.PageId,
rawRec.CreatedAt,
rawRec.UpdatedAt,
rawRec.CreatedUserId,
rawRec.UpdateUserId,
);
await newLocalNode.save();
}
res(inflateRecords(result.data));
},
error: rej,
});
});
}
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, version?: number): 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 && !version ) {
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}${version ? '?version=' + version : ''}`).subscribe({
next: async result => {
if ( version ) {
return res(result.data);
}
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 => {
// Resolve first so the GUI doesn't need to wait for the DB to sync to render
res(result.data);
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,
row.UUID,
JSON.stringify(row.RowData)
);
await newDatabaseEntry.save();
}
}
},
error: rej,
});
});
}
public getDatabaseColumns(PageId: string, NodeId: string, DatabaseId: string, databaseVersion?: number): Promise<any[]> {
return new Promise(async (res, rej) => {
// If offline, fetch the columns from the local database
if ( this.isOffline ) {
if ( databaseVersion ) {
return rej(new ResourceNotAvailableOfflineError());
}
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${databaseVersion ? '?database_version=' + databaseVersion : ''}`)
.subscribe({
next: async results => {
// Resolve this first, so the UI doesn't need to wait for the db sync to render
res(results.data);
if ( databaseVersion ) {
return;
}
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();
}
}
},
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, version?: number): 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 && !version ) {
return res(existingLocalDatabase.getSaveRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}${version ? '?version=' + version : ''}`).subscribe({
next: async result => {
if ( version ) {
return res(result.data);
}
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(
DatabaseId,
row.UUID || DatabaseEntry.getUUID(),
JSON.stringify(row),
true
);
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,
row.UUID,
JSON.stringify(row.RowData)
);
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,
true
);
await newColumnDef.save();
// Add the column to the database's column IDs
const parentDatabase = await this.db.databases.where({ UUID: DatabaseId }).first() as Database;
if ( parentDatabase ) {
parentDatabase.ColumnIds.push(newColumnDef.UUID);
await parentDatabase.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,
});
});
}
public createFileBox(PageId: string, NodeId: string, name: string, rootUUID?: string, parentUUID?: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post(`/file-box/${PageId}/${NodeId}/create`, { name, rootUUID, parentUUID }).subscribe({
next: async result => {
res(result.data);
},
error: rej,
});
});
}
public getFileBox(PageId: string, NodeId: string, FileBoxId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get(`/file-box/${PageId}/${NodeId}/${FileBoxId}`).subscribe({
next: async result => {
res(result.data);
},
error: rej,
});
});
}
public getFileBoxHistory(PageId: string, NodeId: string, FileBoxId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get(`/file-box/${PageId}/${NodeId}/${FileBoxId}/history`).subscribe({
next: async result => {
res(result.data);
},
error: rej,
});
});
}
public getFileBoxFiles(PageId: string, NodeId: string, FileBoxId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get(`/file-box/${PageId}/${NodeId}/${FileBoxId}/files`).subscribe({
next: async result => {
res(result.data);
},
error: rej,
});
});
}
public getFileBoxChildren(PageId: string, NodeId: string, FileBoxId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get(`/file-box/${PageId}/${NodeId}/${FileBoxId}/children`).subscribe({
next: async result => {
res(result.data);
},
error: rej,
});
});
}
public updateFileBox(PageId: string, NodeId: string, FileBoxId: string, data: any): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post(`/file-box/${PageId}/${NodeId}/${FileBoxId}`, data).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public updateFileBoxFile(PageId: string, NodeId: string, FileBoxId: string, FileBoxFileId: string, data: any): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post(`/file-box/${PageId}/${NodeId}/${FileBoxId}/files/${FileBoxFileId}`, data).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public deleteFileBoxFile(PageId: string, NodeId: string, FileBoxId: string, FileBoxFileId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.delete(`/file-box/${PageId}/${NodeId}/${FileBoxId}/files/${FileBoxFileId}`).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public deleteFileBox(PageId: string, NodeId: string, FileBoxId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.delete(`/file-box/${PageId}/${NodeId}/${FileBoxId}`).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public uploadFileBoxFiles(PageId: string, NodeId: string, FileBoxId: string, formData: FormData): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post(`/file-box/${PageId}/${NodeId}/${FileBoxId}/files`, formData).subscribe({
next: async result => {
return res(result.data);
},
error: rej,
});
});
}
public getFileBoxFileDownloadUrl(PageId: string, NodeId: string, FileBoxId: string, FileBoxFileId: string): string {
return this._build_url(`/file-box/${PageId}/${NodeId}/${FileBoxId}/files/${FileBoxFileId}`);
}
public moveMenuNode(MovedPageId: string, ParentPageId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return;
}
this.post('/menu/move-node', { MovedPageId, ParentPageId }).subscribe({
next: async result => {
if ( result.data.status !== 200 ) {
return rej(new Error (result.data.message || 'An unknown error has occurred.'));
}
res();
},
error: err => {
rej(err);
},
});
});
}
public getUserInfo(uid: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.get('/auth/user-info', { uid }).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public checkPermission(permission: string): Promise<boolean> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/share/check', { permission }).subscribe({
next: result => {
return res(result.data.check);
},
error: rej,
});
});
}
public checkPagePermission(PageId: string, level: string = 'view'): Promise<boolean> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post(`/share/check-page/${PageId}/${level}`, {}).subscribe({
next: result => {
return res(result.data.check);
},
error: rej,
});
});
}
public attemptLogin(uid: string, password: string): Promise<{ success: boolean, message?: string }> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/auth/attempt', { uid, password }).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public attemptRegistration(uid: string, password: string, passwordConfirmation: string, fullName: string):
Promise<{ success: boolean, message?: string }> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/auth/register', { uid, password, passwordConfirmation, fullName }).subscribe({
next: result => {
res(result.data);
},
error: rej,
});
});
}
public endSession(): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
return rej(new ResourceNotAvailableOfflineError());
}
this.post('/auth/end-session').subscribe({
next: result => {
return res(result.data);
},
error: rej,
});
});
}
}