Add offline caching for databases, database columns, and database entries
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2020-10-21 22:40:20 -05:00
parent 8de9db08a6
commit 02d8505b05
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
7 changed files with 635 additions and 85 deletions

View File

@ -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) {

View File

@ -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<void> {
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<void> {
await this.api.deleteDatabase(this.page.UUID, this.node.UUID, this.node.Value.Value);
}
public async performSave(): Promise<void> {
// 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;
}

View File

@ -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<ApiResponse> {
return new Observable<ApiResponse>(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<void> {
return new Promise(async (res, rej) => {
const existingLocalDatabase = await this.db.databases.where({ UUID: DatabaseId }).first() as Database;
if ( this.isOffline ) {
if ( existingLocalDatabase ) {
existingLocalDatabase.needsServerUpdate = true;
existingLocalDatabase.deleted = true;
await existingLocalDatabase.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/db/${PageId}/${NodeId}/drop/${DatabaseId}`).subscribe({
next: async result => {
if ( existingLocalDatabase ) {
await this.db.databases.delete(existingLocalDatabase.id);
res();
}
},
error: rej,
});
});
}
public getDatabaseEntries(PageId: string, NodeId: string, DatabaseId: string): Promise<any[]> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
const rows = (await this.db.databaseEntries.where({ DatabaseId }).toArray()) as DatabaseEntry[];
return res(rows.filter(x => !x.deleted).map(x => x.inflateToRecord()));
}
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}/data`).subscribe({
next: async result => {
for ( const row of result.data ) {
const existingDatabaseEntry = await this.db.databaseEntries.where({
DatabaseId, UUID: row.UUID,
}).first() as DatabaseEntry;
if ( existingDatabaseEntry ) {
existingDatabaseEntry.fillFromRecord(row);
await existingDatabaseEntry.save();
} else {
const newDatabaseEntry = new DatabaseEntry(
row.DatabaseId,
JSON.stringify(row.RowData),
row.UUID
);
await newDatabaseEntry.save();
}
}
return res(result.data);
},
error: rej,
});
});
}
public getDatabaseColumns(PageId: string, NodeId: string, DatabaseId: string): Promise<any[]> {
return new Promise(async (res, rej) => {
// If offline, fetch the columns from the local database
if ( this.isOffline ) {
const columns = (await this.db.databaseColumns.where({ DatabaseId }).toArray()) as DatabaseColumn[];
return res(columns.filter(x => !x.deleted).map(x => x.getSaveRecord()));
}
// If online, fetch the columns and sync the local database
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}/columns`).subscribe({
next: async results => {
for ( const def of results.data ) {
const existingColumnDef = await this.db.databaseColumns.where({
DatabaseId, UUID: def.UUID,
}).first() as DatabaseColumn;
if ( existingColumnDef ) {
existingColumnDef.fillFromRecord(def);
await existingColumnDef.save();
} else {
const newColumnDef = new DatabaseColumn(
def.headerName,
def.field,
def.DatabaseId,
def.UUID,
def.Type,
def.additionalData,
);
await newColumnDef.save();
}
}
return res(results.data);
},
error: rej,
});
});
}
public createDatabase(PageId: string, NodeId: string): Promise<any> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
const newLocalDatabase = new Database(
'New Database',
NodeId,
PageId,
[],
Database.getUUID(),
true,
true
);
await newLocalDatabase.save();
return res(newLocalDatabase.getSaveRecord());
}
this.post(`/db/${PageId}/${NodeId}/create`).subscribe({
next: async result => {
const newLocalDatabase = new Database(
result.data.Name,
result.data.NodeId,
result.data.PageId,
result.data.ColumnIds,
result.data.UUID,
result.data.Active
);
await newLocalDatabase.save();
return res(result.data);
},
error: rej,
});
});
}
public getDatabase(PageId: string, NodeId: string, DatabaseId: string): Promise<any> {
return new Promise(async (res, rej) => {
const existingLocalDatabases = await this.db.databases.where({ UUID: DatabaseId }).toArray();
const existingLocalDatabase = existingLocalDatabases.length > 0 ? existingLocalDatabases[0] as Database : undefined;
if ( this.isOffline ) {
if ( existingLocalDatabase ) {
return res(existingLocalDatabase.getSaveRecord());
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.get(`/db/${PageId}/${NodeId}/get/${DatabaseId}`).subscribe({
next: async result => {
if ( existingLocalDatabase ) {
existingLocalDatabase.fillFromRecord(result.data);
await existingLocalDatabase.save();
return res(result.data);
} else {
const newLocalDatabase = new Database(
result.data.Name,
result.data.NodeId,
result.data.PageId,
result.data.ColumnIds,
result.data.UUID,
result.data.Active
);
await newLocalDatabase.save();
return res(result.data);
}
},
error: rej,
});
});
}
public saveDatabaseEntries(PageId: string, NodeId: string, DatabaseId: string, rowData: any[]): Promise<any[]> {
return new Promise(async (res, rej) => {
const existingDatabaseEntries = await this.db.databaseEntries.where({ DatabaseId }).toArray() as DatabaseEntry[];
if ( this.isOffline ) {
const returnData = [];
for ( const entry of existingDatabaseEntries ) {
entry.deleted = true;
entry.needsServerUpdate = true;
await entry.save();
}
for ( const row of rowData ) {
const newDatabaseEntry = new DatabaseEntry(
row.DatabaseId,
JSON.stringify(row.RowData),
row.UUID || DatabaseEntry.getUUID()
);
await newDatabaseEntry.save();
returnData.push(newDatabaseEntry.inflateToRecord());
}
return res(returnData);
}
this.post(`/db/${PageId}/${NodeId}/set/${DatabaseId}/data`, rowData).subscribe({
next: async result => {
await this.db.databaseEntries.bulkDelete(existingDatabaseEntries.map(x => x.id));
for ( const row of result.data ) {
const newDatabaseEntry = new DatabaseEntry(
row.DatabaseId,
JSON.stringify(row.RowData),
row.UUID
);
await newDatabaseEntry.save();
}
return res(result.data);
},
error: rej,
});
});
}
public saveDatabaseColumns(PageId: string, NodeId: string, DatabaseId: string, columns: any[]): Promise<void> {
return new Promise(async (res, rej) => {
if ( this.isOffline ) {
for ( const def of columns ) {
const existingColumnDef = await this.db.databaseColumns.where({
DatabaseId, ...(def.UUID ? { UUID: def.UUID } : { headerName: def.headerName }),
}).first() as DatabaseColumn;
if ( existingColumnDef ) {
existingColumnDef.fillFromRecord(def);
existingColumnDef.needsServerUpdate = true;
await existingColumnDef.save();
} else {
const newColumnDef = new DatabaseColumn(
def.headerName,
def.field,
def.DatabaseId,
def.UUID || DatabaseColumn.getUUID(),
def.Type,
def.additionalData,
);
await newColumnDef.save();
}
}
return res();
}
this.post(`/db/${PageId}/${NodeId}/set/${DatabaseId}/columns`, { columns }).subscribe({
next: async results => {
for ( const def of results.data ) {
const existingColumnDef = await this.db.databaseColumns.where({
DatabaseId, UUID: def.UUID,
}).first() as DatabaseColumn;
if ( existingColumnDef ) {
existingColumnDef.fillFromRecord(def);
existingColumnDef.needsServerUpdate = false;
await existingColumnDef.save();
} else {
const newColumnDef = new DatabaseColumn(
def.headerName,
def.field,
def.DatabaseId,
def.UUID,
def.Type,
def.additionalData,
);
await newColumnDef.save();
}
}
res();
},
error: rej,
});
});
}
public saveDatabaseName(PageId: string, NodeId: string, DatabaseId: string, name: string): Promise<void> {
return new Promise(async (res, rej) => {
const existingDatabase = await this.db.databases.where({ UUID: DatabaseId }).first() as Database;
if ( this.isOffline ) {
if ( existingDatabase ) {
existingDatabase.Name = name;
existingDatabase.needsServerUpdate = true;
await existingDatabase.save();
return res();
} else {
return rej(new ResourceNotAvailableOfflineError());
}
}
this.post(`/db/${PageId}/${NodeId}/set/${DatabaseId}/name`, { Name: name }).subscribe({
next: async result => {
if ( existingDatabase ) {
existingDatabase.Name = name;
await existingDatabase.save();
res();
}
},
error: rej,
});
});
}
}

View File

@ -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<IDatabase> 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<IDatabase, number> {
return this.staticClass().dbService.table('databases') as Dexie.Table<IDatabase, number>;
}
}

View File

@ -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<IDatabaseColumn> 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<IDatabaseColumn, number> {
return this.staticClass().dbService.table('databaseColumns') as Dexie.Table<IDatabaseColumn, number>;
}
}

View File

@ -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<IDatabaseEntry> 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<IDatabaseEntry, number> {
return this.staticClass().dbService.table('databaseEntries') as Dexie.Table<IDatabaseEntry, number>;
}
}

View File

@ -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<IMigration, number>;
menuItems!: Dexie.Table<IMenuItem, number>;
keyValues!: Dexie.Table<IKeyValue, number>;
codiums!: Dexie.Table<ICodium, number>;
databases!: Dexie.Table<IDatabase, number>;
databaseColumns!: Dexie.Table<IDatabaseColumn, number>;
databaseEntries!: Dexie.Table<IDatabaseEntry, number>;
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);
}
}