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 = new BehaviorSubject(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 { 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 { return this.request(endpoint, params, 'get'); } public post(endpoint, body = {}): Observable { return this.request(endpoint, body, 'post'); } public delete(endpoint, body = {}): Observable { return this.request(endpoint, body, 'delete'); } public request(endpoint, params = {}, method: 'get'|'post'|'delete' = 'get'): Observable { return this._request(this._build_url(endpoint), params, method); } public stat(): Observable { return new Observable(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 { 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 { return new Observable(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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, }); }); } }