From 749fcc82d931608d8964f304607956776fc309a8 Mon Sep 17 00:00:00 2001 From: chrisvltn Date: Sat, 10 Feb 2018 12:48:19 -0200 Subject: [PATCH] Adds task model --- src/helpers/Database.ts | 174 ++++++++++++++++++++++++++++++++++++++++ src/models/Task.ts | 145 +++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 src/helpers/Database.ts create mode 100644 src/models/Task.ts diff --git a/src/helpers/Database.ts b/src/helpers/Database.ts new file mode 100644 index 0000000..43710f4 --- /dev/null +++ b/src/helpers/Database.ts @@ -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 { + 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 { + params = this.treatParams(params) + return new Promise(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 { + 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 { + 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 { + 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(table: string, statement?: WhereStatement): Promise { + 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(where: WhereStatement): 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 = { + [K in keyof T]?: T[K] +} + +type WhereStatementResult = { + sql: string + params: any[] +} + +type SQLResult = { + rows: { + length: number + item: (index: number) => any + } + insertId: number | undefined +} \ No newline at end of file diff --git a/src/models/Task.ts b/src/models/Task.ts new file mode 100644 index 0000000..93b8dbc --- /dev/null +++ b/src/models/Task.ts @@ -0,0 +1,145 @@ +import { Database } from "../helpers/Database"; +import * as moment from 'moment' + +export class Task { + + static TABLE_NAME = 'task' + static KEYS: ObjectKeyDefinition[] = [ + { 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; + static parse(data?: ObjectKeys[]): Task[]; + static parse(data?: (ObjectKeys | ObjectKeys[])): (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 { + 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 { + 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 { + 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 { + 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 { + 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 = (v) => T +type ObjectKeys = {[K in keyof T]?: T[K]} +type ObjectKeyDefinition = { + name: keyof T + type: Creator + primary?: boolean +}