Skip to content

Commit

Permalink
Adds task model
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvltn committed Feb 10, 2018
1 parent 342d197 commit 749fcc8
Show file tree
Hide file tree
Showing 2 changed files with 319 additions and 0 deletions.
174 changes: 174 additions & 0 deletions src/helpers/Database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const SQLite = (window as any).sqlitePlugin;
let db;

export class Database {
db;

/**
* Start the database and create an instance when necessary
*/
static async getInstance(): Promise<Database> {
if (db) return db
db = new Database()
await db.connect()
return db
}

/**
* Start database connection
* Only used in getInstance() method
*/
async connect() {
if (this.db) return this.db;
this.db = await new Promise((resolve, reject) => {
const db = SQLite.openDatabase({ name: 'tasklist5.db' }, () => resolve(db), err => reject(err))
})
return this.db
}

/**
* Executes a query
* @param sql Query to be executed
* @param params Params that substitutes '?' in the query
*/
async query(sql: string, params: any[] = []): Promise<SQLResult> {
params = this.treatParams(params)
return new Promise<SQLResult>(resolve => {
this.db.transaction(async tx => {
const [newTx, results] = await (new Promise((resolve, reject) => {
tx.executeSql(sql, params, (a, b) => resolve([a, b]), err => reject(err))
})) as any
resolve(results)
})
})
}

/**
* Execute a query and get returned rows as an array
* Used in SELECT queries
* @param sql Query to be executed
* @param params Params that substitutes '?' in the query
*/
async queryAndGetRows(sql: string, params: any[] = []): Promise<any[]> {
const result = await this.query(sql, params)
const data = []

for (let i = 0; i < result.rows.length; i++) {
data.push(result.rows.item(i))
}

return data;
}

/**
* Executes a query and get the inserted id
* Used in INSERT queries
* @param sql Query to be executed
* @param params Params that substitutes '?' in the query
*/
async queryAndGetInsertId(sql: string, params: any[] = []): Promise<number> {
const result = await this.query(sql, params)
return result.insertId
}

/**
* Insert data in the database
* @param table Table to be inserted
* @param keys Keys to be inserted
* @param values Values to be inserted
*/
async insert(table: string, keys: string[], values: any[]): Promise<number> {
const gaps = keys.map(k => '?')
const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${gaps.join(', ')})`
const insertId = this.queryAndGetInsertId(sql, values)
return insertId
}

/**
* Update data in the database
* @param table Table to be inserted
* @param keys Keys to be inserted
* @param values Values to be inserted
* @param where Where statement to know which lines will be updated
*/
async update(table: string, keys: string[], values: any[], where?: { [key: string]: any }) {
const keysString = keys.map(k => k + ' = ?').join(', ')
const { sql, params } = this.buildWhere(where)
const query = `UPDATE ${table} SET ${keysString} ` + sql
return this.query(query, values.concat(params))
}

/**
* Delete one or more rows in a table
* @param table Table name
* @param statement Where statement to know which lines will be deleted
*/
async delete<T>(table: string, statement?: WhereStatement<T>): Promise<any> {
const { sql, params } = this.buildWhere(statement)
const query = `DELETE ${table} ` + sql
return this.query(query, params)
}

/**
* Treats an where object and returns the query with its parameters
* @param where Where statement
*/
private buildWhere<T>(where: WhereStatement<T>): WhereStatementResult {
const values = []
let sql = ''
if (where) {
Object.keys(where).forEach(k => values.push(where[k]))
sql = 'WHERE ' + Object.keys(where).map(k => k + ' = ?').join(' AND ')
}
return {
params: values,
sql: sql,
}
}

/**
* Treat all parameters to be used in queries
* @param params Parameters
*/
private treatParams(params: any[]): any[] {
const newParams = []
params.forEach(p => {
switch (typeof p) {
case 'string':
newParams.push(p.trim()); break;
case 'boolean':
newParams.push(p ? 1 : 0); break;
case 'object':
if (p instanceof Date) {
newParams.push(p.toISOString())
} else if (p !== null) {
try { newParams.push(JSON.stringify(p)) }
catch (e) { newParams.push(p) }
} else {
newParams.push(p)
}
break;
default:
newParams.push(p)
}
})
return newParams
}
}

type WhereStatement<T> = {
[K in keyof T]?: T[K]
}

type WhereStatementResult = {
sql: string
params: any[]
}

type SQLResult = {
rows: {
length: number
item: (index: number) => any
}
insertId: number | undefined
}
145 changes: 145 additions & 0 deletions src/models/Task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Database } from "../helpers/Database";
import * as moment from 'moment'

export class Task {

static TABLE_NAME = 'task'
static KEYS: ObjectKeyDefinition<Task>[] = [
{ name: 'id', type: Number, primary: true },
{ name: 'title', type: String },
{ name: 'description', type: String },
{ name: 'done', type: Boolean },
{ name: 'create_date', type: Date },
]

id: number
title: string
description: string
done: boolean
create_date: Date

/**
* Convert an object to a Task instance
* @param data Task params
*/
static parse(data?: ObjectKeys<Task>): Task;
static parse(data?: ObjectKeys<Task>[]): Task[];
static parse(data?: (ObjectKeys<Task> | ObjectKeys<Task>[])): (Task | Task[]) {
data = data || {}
if (Array.isArray(data)) {
return data.map(i => Task.parse(i))
}

const task = new Task()
Task.KEYS.forEach(key => {
if (typeof data[key.name] == 'undefined') return;
if (key.type == Date && data[key.name]) {
try {
task[key.name] = moment(data[key.name] as any).toDate()
} catch (e) { console.warn('Not possible to convert date type') }
} else if (data[key.name] === null) {
task[key.name] = null
} else {
task[key.name] = key.type(data[key.name]) as any
}
})

return task
}

/**
* Prepare Task table in the database
*/
static async prepare(): Promise<any> {
const db = await Database.getInstance()

const keys = Task.KEYS.map(k => {
const name = k.name
const primary = k.primary ? 'PRIMARY KEY AUTOINCREMENT' : ''
let type = 'TEXT'
switch (k.type) {
case Number:
type = 'INTEGER'; break;
case String:
type = 'VARCHAR(255)'; break;
case Date:
type = 'TEXT'; break;
case Boolean:
type = 'INTEGER'; break;
}
return [name, type, primary].join(' ')
})

const sql = `
CREATE TABLE IF NOT EXISTS ${Task.TABLE_NAME} (
${keys.join(',\n')}
)
`.trim()

return db.query(sql)
}

/**
* Find all stored tasks
*/
static async findAll(): Promise<Task[]> {
const db = await Database.getInstance()
const rows = await db.queryAndGetRows(`SELECT * FROM ${Task.TABLE_NAME}`)
return Task.parse(rows)
}

/**
* Find a task
* @param id Task id
*/
static async findById(id: number): Promise<Task> {
const db = await Database.getInstance()
const primaryKey = Task.KEYS.find(k => k.primary).name
const rows = await db.queryAndGetRows(`SELECT * FROM ${Task.TABLE_NAME} WHERE ${primaryKey} = ?`, [id])
if (!rows.length) return null
return Task.parse(rows[0])
}

/**
* Save (insert or update) a task in the database
*/
async save(): Promise<Task> {
const db = await Database.getInstance()
const primaryKey = Task.KEYS.find(k => k.primary).name
const update = !!(this[primaryKey] && (await Task.findById(this[primaryKey] as any)))
const keys = Task.KEYS.filter(k => !k.primary).map(k => k.name)
const values = keys.map(k => this[k])

const updateWhere = {}
updateWhere[primaryKey] = this[primaryKey]

const insertId = !update ?
await db.insert(Task.TABLE_NAME, keys, values) :
await db.update(Task.TABLE_NAME, keys, values, updateWhere)

if (typeof insertId == 'number') this[primaryKey] = insertId
return this
}

/**
* Delete a task in the database
*/
async delete(): Promise<Task> {
const db = await Database.getInstance()
const primaryKey = Task.KEYS.find(k => k.primary).name
const value = this[primaryKey]
const where = {}
where[primaryKey] = value
await db.delete(Task.TABLE_NAME, where)
this[primaryKey] = null
return this
}
}

type Creator<T> = (v) => T
type ObjectKeys<T> = {[K in keyof T]?: T[K]}
type ObjectKeyDefinition<T> = {
name: keyof T
type: Creator<Number | String | Boolean | Date>
primary?: boolean
}

0 comments on commit 749fcc8

Please sign in to comment.