import { StorageAdapter } from "../../api/StorageAdapter" ; import { createLogger } from "../../../utils/logger" ; import { ModelSchema } from "../../../ModelSchema" ; import { Filter } from "../../../filters" ; import { prepareStatement , flattenResultSet , getType , serializeData , deserializeData } from "./utils" ; import { filterToSQL } from "./filterToSQL" ; const logger = createLogger ( "sqlite" ) ; /**
 * Web Storage Implementation for DataStore using IndexedDB
 */ export abstract class BaseSQLAdapter implements StorageAdapter { private sqlite : Database ; private stores : ModelSchema [ ] = [ ] ; constructor ( sqlite ) { this . sqlite = sqlite ; } public addStore ( config ) { this . stores . push ( config ) ; } public async createStores ( ) { const existingStoreNames = await this . getStoreNames ( ) ; for await ( const storeName of existingStoreNames ) { const existingModelStoreName = this . stores . find ( ( ( store ) => ( storeName === store . getName ( ) ) ) ) ; if ( existingModelStoreName ) { return ; } await this . transaction ( `DROP TABLE IF EXISTS ${ existingModelStoreName }` , [ ] ) ; } for await ( const store of this . stores ) { if ( existingStoreNames . includes ( store . getName ( ) ) ) { return ; } const query = this . getCreateStatement ( store ) ; await this . transaction ( query , [ ] ) ; } } public async save ( storeName , input ) { const serialized = serializeData ( input ) ; const [ cols , vals ] = prepareStatement ( serialized , "insert" ) ; const query = `INSERT INTO ${ storeName } ${ cols }` ; await this . transaction ( query , vals ) ; return serialized ; } public async query ( storeName , filter ? ) { if ( ! filter ) { const data = await this . fetchAll ( storeName ) ; return deserializeData ( data ) ; } const condition = filterToSQL ( filter ) ; const query = `SELECT * FROM ${ storeName } ${ condition }` ; const res = await this . readTransaction ( query , [ ] ) ; return deserializeData ( res ) ; } public async queryById ( storeName , idField , id ) { const escapedId = ( typeof id === "string" ) ? `"${ id }"` : id ; const query = `SELECT * FROM ${ storeName } WHERE ${ idField } = ${ escapedId }` ; const res = await this . readTransaction ( query , [ ] ) ; return deserializeData ( res ) ; } public async update ( storeName , input , filter ? ) { const condition = filterToSQL ( filter ) ; const [ cols , vals ] = prepareStatement ( serializeData ( input ) , "update" ) ; const query = `UPDATE ${ storeName } SET ${ cols } ${ condition }` ; return this . transaction ( query , [ ... vals ] ) ; } public async updateById ( storeName , idField , input ) { const [ cols , vals ] = prepareStatement ( serializeData ( input ) , "update" ) ; const id = input [ idField ] ; const escapedId = ( typeof id === "string" ) ? `"${ id }"` : id ; const query = `UPDATE ${ storeName } SET ${ cols } ${ idField } = ${ escapedId }` ; return this . transaction ( query , [ ... vals ] ) ; } public async saveOrUpdate ( storeName , idField , input ) { const serializedInput = serializeData ( input ) ; const [ cols , vals ] = prepareStatement ( serializedInput , "insert" ) ; const [ updateCols ] = prepareStatement ( serializedInput , "update" ) ; const query = `INSERT INTO ${ storeName } ${ cols }` + ` ON CONFLICT(${ idField }) DO UPDATE SET ${ updateCols }` ; return this . transaction ( query , [ ... vals ] ) ; } public async remove ( storeName , idField , filter ? ) { const condition = filterToSQL ( filter ) ; const query = `DELETE FROM ${ storeName } ${ condition }` ; return this . transaction ( query , [ ] ) ; } public async deleteStore ( storeName ) { const query = `DELETE FROM ${ storeName }` ; return this . transaction ( query , [ ] ) ; } public async removeById ( storeName , idField , id ) { const escapedId = ( typeof id === "string" ) ? `"${ id }"` : id ; const query = `DELETE FROM ${ storeName } ${ idField } ${ escapedId }` ; return this . transaction ( query , [ ] ) ; } public getSQLiteInstance ( ) { return this . sqlite ; } createTransaction ( ) { return new Promise ( ( resolve ) => { resolve ( this ) ; } ) ; } commit ( ) { logger ( "[Commit] Method not implemented." ) ; return Promise . resolve ( ) ; } rollback ( ) { logger ( "[Rollback] Method not implemented." ) ; return Promise . resolve ( ) ; } isTransactionOpen ( ) { throw new Error ( "[isOpen] Method not implemented." ) ; } private async fetchAll ( storeName ) { return this . readTransaction ( `SELECT * FROM ${ storeName }` , [ ] ) ; } private async getStoreNames ( ) { const res = await this . readTransaction ( "SELECT name FROM sqlite_master WHERE name NOT LIKE 'android_%' AND name NOT LIKE 'sqlite_%'" , [ ] ) ; return res . map ( ( r ) => r . name ) ; } private async transaction ( query , args ) { return new Promise ( ( resolve , reject ) => { const successCb = ( tx , res ) => { logger ( "Executed query with results" , res . rows ) ; resolve ( flattenResultSet ( res . rows ) ) ; } ; const errorCb = ( tx , error ) => { reject ( error ) ; return false ; } ; this . sqlite . transaction ( ( tx ) => { tx . executeSql ( query , [ ... args ] , successCb , errorCb ) ; } ) ; } ) ; } private async readTransaction ( query , args ) { return new Promise ( ( resolve , reject ) => { const successCb = ( tx , res ) => { logger ( "Executed query with results" , res . rows ) ; resolve ( flattenResultSet ( res . rows ) ) ; } ; const errorCb = ( tx , error ) => { reject ( error ) ; return false ; } ; this . sqlite . readTransaction ( ( tx ) => { tx . executeSql ( query , [ ... args ] , successCb , errorCb ) ; } ) ; } ) ; } private getCreateStatement ( store ) { const fields = store . getFields ( ) ; const query = Object . keys ( fields ) . map ( ( name ) => { const field = fields [ name ] ; const type = getType ( field . type as string ) ; if ( field . primary ) { return `${ name } TEXT PRIMARY KEY` ; } ; return `${ name } ${ type }` ; } ) . join ( "," ) ; return `CREATE TABLE IF NOT EXISTS ${ store . getStoreName ( ) } (${ query })` ; } private convertToPromise < T > ( statement , args ) { return new Promise < T > ( ( resolve , reject ) => { const successCb = ( tx , res ) => resolve ( res . rows as unknown as T ) ; const errorCb = ( tx , err ) => { reject ( err ) ; return false ; } ; this . sqlite . transaction ( ( tx ) => { tx . executeSql ( statement , args , successCb , errorCb ) ; } ) ; } ) ; } }