diff --git a/src/app/components/editor/database/columns/columns.component.ts b/src/app/components/editor/database/columns/columns.component.ts index 2ba62a7..d7d4281 100644 --- a/src/app/components/editor/database/columns/columns.component.ts +++ b/src/app/components/editor/database/columns/columns.component.ts @@ -38,7 +38,8 @@ export class ColumnsComponent implements OnInit { this.columnSets = this.columnSets.map(x => { x.field = x.headerName; return x; - }) + }); + this.modals.dismiss(this.columnSets); } else { this.modals.dismiss(); @@ -46,10 +47,9 @@ export class ColumnsComponent implements OnInit { } onDeleteClick(i) { - const newSets = this.columnSets.filter((x, index) => { + this.columnSets = this.columnSets.filter((x, index) => { return index !== i; }); - this.columnSets = newSets; } onUpArrow(i) { diff --git a/src/app/components/editor/database/database.component.ts b/src/app/components/editor/database/database.component.ts index 5687be4..9e231ca 100644 --- a/src/app/components/editor/database/database.component.ts +++ b/src/app/components/editor/database/database.component.ts @@ -1,5 +1,5 @@ import {Component, Input, OnInit, ViewChild} from '@angular/core'; -import {ApiService} from '../../../service/api.service'; +import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service'; import {AlertController, LoadingController, ModalController} from '@ionic/angular'; import {ColumnsComponent} from './columns/columns.component'; import {AgGridAngular} from 'ag-grid-angular'; @@ -29,6 +29,7 @@ export class DatabaseComponent extends EditorNodeContract implements OnInit { public dirty = false; public lastClickRow = -1; public dbName = ''; + public notAvailableOffline = false; protected dbId!: string; public get readonly() { @@ -181,102 +182,52 @@ export class DatabaseComponent extends EditorNodeContract implements OnInit { // Load the database record itself if ( !this.node.Value.Value && this.editorService.canEdit() ) { - await new Promise((res, rej) => { - this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/create`).subscribe({ - next: result => { - this.dbRecord = result.data; - this.dbName = result.data.Name; - this.node.Value.Mode = 'database'; - this.node.Value.Value = result.data.UUID; - this.node.value = result.data.UUID; - res(); - }, - error: rej, - }); - }); + this.dbRecord = await this.api.createDatabase(this.page.UUID, this.node.UUID); + this.dbName = this.dbRecord.Name; + this.node.Value.Mode = 'database'; + this.node.Value.Value = this.dbRecord.UUID; + this.node.value = this.dbRecord.UUID; } else { - await new Promise((res, rej) => { - this.api.get(`/db/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}`).subscribe({ - next: result => { - this.dbRecord = result.data; - this.dbName = result.data.Name; - res(); - }, - error: rej, - }); - }); + try { + this.dbRecord = await this.api.getDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value); + this.dbName = this.dbRecord.Name; + this.notAvailableOffline = false; + } catch (e: unknown) { + if ( e instanceof ResourceNotAvailableOfflineError ) { + this.notAvailableOffline = true; + } else { + throw e; + } + } } // Load the columns - await new Promise((res, rej) => { - this.api.get(`/db/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}/columns`).subscribe({ - next: result => { - this.setColumns(result.data); - res(); - }, - error: rej, - }); - }); + const columns = await this.api.getDatabaseColumns(this.page.UUID, this.node.UUID, this.node.Value.Value); + this.setColumns(columns); - // Load the data - await new Promise((res, rej) => { - this.api.get(`/db/${this.page.UUID}/${this.node.UUID}/get/${this.node.Value.Value}/data`).subscribe({ - next: result => { - this.rowData = result.data.map(x => x.RowData); - this.agGridElement.api.setRowData(this.rowData); - res(); - }, - error: rej, - }); - }); + const rows = await this.api.getDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value); + this.rowData = rows.map(x => x.RowData); + this.agGridElement.api.setRowData(this.rowData); this.pendingSetup = false; this.dirty = false; } - public performDelete(): void | Promise { - return new Promise((res, rej) => { - this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/drop/${this.node.Value.Value}`).subscribe({ - next: result => { - res(); - }, - error: rej, - }); - }); + public async performDelete(): Promise { + await this.api.deleteDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value); } public async performSave(): Promise { // Save the columns first - await new Promise((res, rej) => { - this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}/columns`, {columns: this.columnDefs}).subscribe({ - next: result => { - res(); - }, - error: rej, - }); - }); + await this.api.saveDatabaseColumns(this.page.UUID, this.node.UUID, this.node.Value.Value, this.columnDefs); // Save the data - await new Promise((res, rej) => { - this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}/data`, this.rowData).subscribe({ - next: result => { - this.rowData = result.data.map(x => x.RowData); - this.agGridElement.api.setRowData(this.rowData); - res(); - }, - error: rej, - }); - }); + const rows = await this.api.saveDatabaseEntries(this.page.UUID, this.node.UUID, this.node.Value.Value, this.rowData); + this.rowData = rows.map(x => x.RowData); + this.agGridElement.api.setRowData(this.rowData); // Save the name - await new Promise((res, rej) => { - this.api.post(`/db/${this.page.UUID}/${this.node.UUID}/set/${this.node.Value.Value}/name`, { Name: this.dbName }).subscribe({ - next: result => { - res(); - }, - error: rej, - }); - }); + await this.api.saveDatabaseName(this.page.UUID, this.node.UUID, this.node.Value.Value, this.dbName); this.dirty = false; } diff --git a/src/app/service/api.service.ts b/src/app/service/api.service.ts index 8a9536a..2f89f4c 100644 --- a/src/app/service/api.service.ts +++ b/src/app/service/api.service.ts @@ -7,6 +7,9 @@ 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'; export class ResourceNotAvailableOfflineError extends Error { constructor(msg = 'This resource is not yet available offline on this device.') { @@ -122,7 +125,7 @@ export class ApiService { protected _request(endpoint, params = {}, method: 'get'|'post' = 'get'): Observable { return new Observable(sub => { - let data: any = {} + let data: any = {}; if ( method === 'get' ) { data.params = params; } else { @@ -387,4 +390,317 @@ export class ApiService { }); }); } + + 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 => { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + }); + }); + } } diff --git a/src/app/service/db/Database.ts b/src/app/service/db/Database.ts new file mode 100644 index 0000000..9066a98 --- /dev/null +++ b/src/app/service/db/Database.ts @@ -0,0 +1,93 @@ +import {Model} from './Model'; + +export interface IDatabase { + id?: number; + Name: string; + NodeId: string; + PageId: string; + ColumnIds: string[]; + UUID: string; + Active: boolean; + needsServerUpdate?: boolean; + deleted?: boolean; +} + +export class Database extends Model implements IDatabase { + id?: number; + Name: string; + NodeId: string; + PageId: string; + ColumnIds: string[]; + UUID: string; + Active: boolean; + needsServerUpdate?: boolean; + deleted?: boolean; + + public static getTableName() { + return 'databases'; + } + + public static getSchema() { + return '++id, Name, NodeId, PageId, ColumnIds, UUID, Active, needsServerUpdate, deleted'; + } + + constructor( + Name: string, + NodeId: string, + PageId: string, + ColumnIds: string[], + UUID: string, + Active: boolean, + needsServerUpdate?: boolean, + deleted?: boolean, + id?: number + ) { + super(); + + this.Name = Name; + this.NodeId = NodeId; + this.PageId = PageId; + this.ColumnIds = ColumnIds; + this.UUID = UUID; + this.Active = Active; + + if ( typeof needsServerUpdate !== 'undefined' ) { + this.needsServerUpdate = needsServerUpdate; + } + + if ( typeof deleted !== 'undefined' ) { + this.deleted = deleted; + } + + if ( id ) { + this.id = id; + } + } + + public fillFromRecord(record: any) { + this.Name = record.Name; + this.NodeId = record.NodeId; + this.PageId = record.PageId; + this.ColumnIds = record.ColumnIds; + this.UUID = record.UUID; + this.Active = record.Active; + } + + public getSaveRecord(): any { + return { + ...(this.id ? { id: this.id } : {}), + Name: this.Name, + NodeId: this.NodeId, + PageId: this.PageId, + ColumnIds: this.ColumnIds, + UUID: this.UUID, + Active: this.Active, + ...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }), + ...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }), + }; + } + + public getDatabase(): Dexie.Table { + return this.staticClass().dbService.table('databases') as Dexie.Table; + } +} diff --git a/src/app/service/db/DatabaseColumn.ts b/src/app/service/db/DatabaseColumn.ts new file mode 100644 index 0000000..222494c --- /dev/null +++ b/src/app/service/db/DatabaseColumn.ts @@ -0,0 +1,93 @@ +import {Model} from './Model'; + +export interface IDatabaseColumn { + id?: number; + headerName: string; + field: string; + DatabaseId: string; + UUID: string; + Type: string; + additionalData: string; + needsServerUpdate?: boolean; + deleted?: boolean; +} + +export class DatabaseColumn extends Model implements IDatabaseColumn { + id?: number; + headerName: string; + field: string; + DatabaseId: string; + UUID: string; + Type: string; + additionalData: string; + needsServerUpdate?: boolean; + deleted?: boolean; + + public static getTableName() { + return 'databaseColumns'; + } + + public static getSchema() { + return '++id, headerName, field, DatabaseId, UUID, Type, additionalData, needsServerUpdate, deleted'; + } + + constructor( + headerName: string, + field: string, + DatabaseId: string, + UUID: string, + Type: string, + additionalData: string, + needsServerUpdate?: boolean, + deleted?: boolean, + id?: number + ) { + super(); + + this.headerName = headerName; + this.field = field; + this.DatabaseId = DatabaseId; + this.UUID = UUID; + this.Type = Type; + this.additionalData = additionalData; + + if ( typeof needsServerUpdate !== 'undefined' ) { + this.needsServerUpdate = needsServerUpdate; + } + + if ( typeof deleted !== 'undefined' ) { + this.deleted = deleted; + } + + if ( id ) { + this.id = id; + } + } + + public fillFromRecord(record: any) { + this.headerName = record.headerName; + this.field = record.field; + this.DatabaseId = record.DatabaseId; + this.UUID = record.UUID; + this.Type = record.Type; + this.additionalData = record.additionalData; + } + + public getSaveRecord(): any { + return { + ...(this.id ? { id: this.id } : {}), + headerName: this.headerName, + field: this.field, + DatabaseId: this.DatabaseId, + UUID: this.UUID, + Type: this.Type, + additionalData: this.additionalData, + ...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }), + ...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }), + }; + } + + public getDatabase(): Dexie.Table { + return this.staticClass().dbService.table('databaseColumns') as Dexie.Table; + } +} diff --git a/src/app/service/db/DatabaseEntry.ts b/src/app/service/db/DatabaseEntry.ts new file mode 100644 index 0000000..e467ed9 --- /dev/null +++ b/src/app/service/db/DatabaseEntry.ts @@ -0,0 +1,82 @@ +import {Model} from './Model'; + +export interface IDatabaseEntry { + id?: number; + DatabaseId: string; + RowDataJSON: string; + UUID: string; + needsServerUpdate?: boolean; + deleted?: boolean; +} + +export class DatabaseEntry extends Model implements IDatabaseEntry { + id?: number; + DatabaseId: string; + RowDataJSON: string; + UUID: string; + needsServerUpdate?: boolean; + deleted?: boolean; + + public static getTableName() { + return 'databaseEntries'; + } + + public static getSchema() { + return '++id, DatabaseId, RowDataJSON, UUID, needsServerUpdate, deleted'; + } + + constructor( + DatabaseId: string, + RowDataJSON: string, + UUID: string, + needsServerUpdate?: boolean, + deleted?: boolean, + id?: number + ) { + super(); + + this.DatabaseId = DatabaseId; + this.RowDataJSON = RowDataJSON; + this.UUID = UUID; + + if ( typeof needsServerUpdate !== 'undefined' ) { + this.needsServerUpdate = needsServerUpdate; + } + + if ( typeof deleted !== 'undefined' ) { + this.deleted = deleted; + } + + if ( id ) { + this.id = id; + } + } + + public fillFromRecord(record: any) { + this.DatabaseId = record.DatabaseId; + this.RowDataJSON = JSON.stringify(record.RowData); + this.UUID = record.UUID; + } + + public inflateToRecord() { + const record = this.getSaveRecord(); + record.RowData = JSON.parse(record.RowDataJSON); + delete record.RowDataJSON; + return record; + } + + public getSaveRecord(): any { + return { + ...(this.id ? { id: this.id } : {}), + DatabaseId: this.DatabaseId, + RowDataJSON: this.RowDataJSON, + UUID: this.UUID, + ...(typeof this.needsServerUpdate === 'undefined' ? {} : { needsServerUpdate: this.needsServerUpdate }), + ...(typeof this.deleted === 'undefined' ? {} : { deleted: this.deleted }), + }; + } + + public getDatabase(): Dexie.Table { + return this.staticClass().dbService.table('databaseEntries') as Dexie.Table; + } +} diff --git a/src/app/service/db/database.service.ts b/src/app/service/db/database.service.ts index c26f3ca..ac3c32e 100644 --- a/src/app/service/db/database.service.ts +++ b/src/app/service/db/database.service.ts @@ -4,18 +4,24 @@ import {IMigration, Migration} from './Migration'; import {IMenuItem, MenuItem} from './MenuItem'; import {KeyValue, IKeyValue} from './KeyValue'; import {Codium, ICodium} from './Codium'; +import {Database, IDatabase} from './Database'; +import {DatabaseColumn, IDatabaseColumn} from './DatabaseColumn'; +import {DatabaseEntry, IDatabaseEntry} from './DatabaseEntry'; @Injectable({ providedIn: 'root' }) export class DatabaseService extends Dexie { - protected static registeredModels = [Migration, MenuItem, KeyValue, Codium]; + protected static registeredModels = [Migration, MenuItem, KeyValue, Codium, Database, DatabaseColumn, DatabaseEntry]; protected initialized = false; migrations!: Dexie.Table; menuItems!: Dexie.Table; keyValues!: Dexie.Table; codiums!: Dexie.Table; + databases!: Dexie.Table; + databaseColumns!: Dexie.Table; + databaseEntries!: Dexie.Table; constructor( ) { @@ -48,7 +54,7 @@ export class DatabaseService extends Dexie { schema[ModelClass.getTableName()] = ModelClass.getSchema(); } - await this.version(5).stores(schema); + await this.version(9).stores(schema); await this.open(); this.migrations = this.table('migrations'); @@ -62,5 +68,14 @@ export class DatabaseService extends Dexie { this.codiums = this.table('codiums'); this.codiums.mapToClass(Codium); + + this.databases = this.table('databases'); + this.databases.mapToClass(Database); + + this.databaseColumns = this.table('databaseColumns'); + this.databaseColumns.mapToClass(DatabaseColumn); + + this.databaseEntries = this.table('databaseEntries'); + this.databaseEntries.mapToClass(DatabaseEntry); } }