mirror of
https://github.com/antos-rde/antosdk-apps.git
synced 2024-12-25 03:38:21 +01:00
SQLiteDB: add basic VFS binding for SQLite database file
This commit is contained in:
parent
e3deffe907
commit
cd5b0f66cc
@ -1,25 +1,301 @@
|
|||||||
namespace OS {
|
namespace OS {
|
||||||
export namespace API
|
export namespace API
|
||||||
{
|
{
|
||||||
export class SQLiteDBBase {
|
/**
|
||||||
|
* Generate SQL expression from input object
|
||||||
|
*
|
||||||
|
* Example of input object
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* where: {
|
||||||
|
* id$gte: 10,
|
||||||
|
* user: "dany'",
|
||||||
|
* $or: {
|
||||||
|
* 'user.email': "test@mail.com",
|
||||||
|
* age$lte: 30,
|
||||||
|
* $and: {
|
||||||
|
* 'user.birth$ne': 1986,
|
||||||
|
* age$not_between: [20,30],
|
||||||
|
* name$not_like: "%LE"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* fields: ['name as n', 'id', 'email'],
|
||||||
|
* order: ['user.name$asc', "id$desc"],
|
||||||
|
* joins: {
|
||||||
|
* cid: 'Category.id',
|
||||||
|
* did: 'Country.id'
|
||||||
|
* }
|
||||||
|
*}
|
||||||
|
* ```
|
||||||
|
* This will generate the followings expressions:
|
||||||
|
* - `( self.name as n,self.id,self.email )` for fields
|
||||||
|
* - condition:
|
||||||
|
* ```
|
||||||
|
* (
|
||||||
|
* ( self.id >= 10 ) AND
|
||||||
|
* ( self.user = 'dany''' ) AND
|
||||||
|
* (
|
||||||
|
* ( user.email = 'test@mail.com' ) OR
|
||||||
|
* ( self.age <= 30 ) OR
|
||||||
|
* (
|
||||||
|
* ( user.birth != 1986 ) AND
|
||||||
|
* ( self.age NOT BETWEEN 20 AND 30 ) AND
|
||||||
|
* ( self.name NOT LIKE '%LE' )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* - order: `user.name ASC,self.id DESC`
|
||||||
|
* - joining:
|
||||||
|
* ```
|
||||||
|
* INNER JOIN Category ON self.cid = Category.id
|
||||||
|
* INNER JOIN Country ON self.did = Country.id
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class SQLiteQueryGenerator {
|
||||||
|
private _where: string;
|
||||||
|
private _fields: string;
|
||||||
|
private _order: string;
|
||||||
|
private _joins: string;
|
||||||
|
private _is_joining: boolean;
|
||||||
|
constructor(obj: GenericObject<any>)
|
||||||
|
{
|
||||||
|
this._where = undefined;
|
||||||
|
this._fields = undefined;
|
||||||
|
this._order = undefined;
|
||||||
|
this._joins = undefined;
|
||||||
|
this._is_joining = false;
|
||||||
|
if(obj.joins)
|
||||||
|
{
|
||||||
|
this._is_joining = true;
|
||||||
|
this._joins = this.joins(obj.joins);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(obj.where)
|
||||||
|
{
|
||||||
|
this._where = this.where("$and", obj.where);
|
||||||
|
}
|
||||||
|
if(obj.fields)
|
||||||
|
{
|
||||||
|
this._fields = `( ${obj.fields.map(v=>this.infer_field(v)).join(",")} )`;
|
||||||
|
}
|
||||||
|
if(obj.order)
|
||||||
|
{
|
||||||
|
this._order = this.order_by(obj.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private infer_field(k: string) : string
|
||||||
|
{
|
||||||
|
if(!this._is_joining || k.indexOf(".") > 0) return k;
|
||||||
|
return `self.${k}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private joins(data: GenericObject<any>): string
|
||||||
|
{
|
||||||
|
let joins_arr = [];
|
||||||
|
for(let k in data)
|
||||||
|
{
|
||||||
|
let v = data[k];
|
||||||
|
let arr = v.split('.')
|
||||||
|
if(arr.length != 2)
|
||||||
|
{
|
||||||
|
throw new Error(__("Other table name parsing error: {0}", v).__());
|
||||||
|
}
|
||||||
|
joins_arr.push(`INNER JOIN ${arr[0]} ON ${this.infer_field(k)} = ${v}`);
|
||||||
|
}
|
||||||
|
return joins_arr.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
print()
|
||||||
|
{
|
||||||
|
console.log(this._fields);
|
||||||
|
console.log(this._where);
|
||||||
|
console.log(this._order);
|
||||||
|
console.log(this._joins);
|
||||||
|
}
|
||||||
|
|
||||||
|
private order_by(order: string[]): string
|
||||||
|
{
|
||||||
|
if(! Array.isArray(order))
|
||||||
|
{
|
||||||
|
throw new Error(__("Invalid type: expect array get {0}", typeof(order)).__());
|
||||||
|
|
||||||
|
}
|
||||||
|
return order.map((v,_) => {
|
||||||
|
const arr = v.split('$');
|
||||||
|
if(arr.length != 2)
|
||||||
|
{
|
||||||
|
throw new Error(__("Invalid field order format {0}", v).__());
|
||||||
|
}
|
||||||
|
switch(arr[1])
|
||||||
|
{
|
||||||
|
case 'asc': return `${this.infer_field(arr[0])} ASC`;
|
||||||
|
case 'desc': return `${this.infer_field(arr[0])} DESC`;
|
||||||
|
default: throw new Error(__("Invalid field order type {0}", v).__());
|
||||||
|
}
|
||||||
|
}).join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
private escape_string(s: string)
|
||||||
|
{
|
||||||
|
let regex = /[']/g;
|
||||||
|
var chunkIndex = regex.lastIndex = 0;
|
||||||
|
var escapedVal = '';
|
||||||
|
var match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(s))) {
|
||||||
|
escapedVal += s.slice(chunkIndex, match.index) + {'\'': '\'\''}[match[0]];
|
||||||
|
chunkIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkIndex === 0) {
|
||||||
|
// Nothing was escaped
|
||||||
|
return "'" + s + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkIndex < s.length) {
|
||||||
|
return "'" + escapedVal + s.slice(chunkIndex) + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "'" + escapedVal + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private parse_value(v:any, t: string[]): string
|
||||||
|
{
|
||||||
|
if(! (t as any).includes(typeof(v)))
|
||||||
|
{
|
||||||
|
throw new Error(__("Invalid type: expect [{0}] get {1}", t.join(","), typeof(v)).__());
|
||||||
|
}
|
||||||
|
switch(typeof(v))
|
||||||
|
{
|
||||||
|
case 'number': return JSON.stringify(v);
|
||||||
|
case 'string': return this.escape_string(v);
|
||||||
|
default: throw new Error(__("Un supported value {0} of type {1}", v, typeof(v)).__());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private binary(k: string,v :any)
|
||||||
|
{
|
||||||
|
const arr = k.split("$");
|
||||||
|
if(arr.length > 2)
|
||||||
|
{
|
||||||
|
throw new Error(__("Invalid left hand side format: {0}", k).__());
|
||||||
|
}
|
||||||
|
if(arr.length == 2)
|
||||||
|
{
|
||||||
|
switch(arr[1])
|
||||||
|
{
|
||||||
|
case "gt":
|
||||||
|
return `( ${this.infer_field(arr[0])} > ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "gte":
|
||||||
|
return `( ${this.infer_field(arr[0])} >= ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "lt":
|
||||||
|
return `( ${this.infer_field(arr[0])} < ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "lte":
|
||||||
|
return `( ${this.infer_field(arr[0])} <= ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "ne":
|
||||||
|
return `( ${this.infer_field(arr[0])} != ${this.parse_value(v, ['number', 'string'])} )`;
|
||||||
|
case "between":
|
||||||
|
return `( ${this.infer_field(arr[0])} BETWEEN ${this.parse_value(v[0],['number'])} AND ${this.parse_value(v[1],['number'])} )`;
|
||||||
|
case "not_between":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT BETWEEN ${this.parse_value(v[0],['number'])} AND ${this.parse_value(v[1],['number'])} )`;
|
||||||
|
case "in":
|
||||||
|
return `( ${this.infer_field(arr[0])} IN [${this.parse_value(v[0],['number'])}, ${this.parse_value(v[1],['number'])}] )`;
|
||||||
|
case "not_in":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT IN [${this.parse_value(v[0],['number'])}, ${this.parse_value(v[1],['number'])}] )`;
|
||||||
|
case "like":
|
||||||
|
return `( ${this.infer_field(arr[0])} LIKE ${this.parse_value(v,['string'])} )`;
|
||||||
|
case "not_like":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT LIKE ${this.parse_value(v,['string'])} )`;
|
||||||
|
default: throw new Error(__("Unsupported operator `{0}`", arr[1]).__());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return `( ${this.infer_field(arr[0])} = ${this.parse_value(v, ['number', 'string'])} )`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private where(op:string, obj: GenericObject<any>): string
|
||||||
|
{
|
||||||
|
let join_op = undefined;
|
||||||
|
switch(op)
|
||||||
|
{
|
||||||
|
case "$and":
|
||||||
|
join_op = " AND ";
|
||||||
|
break;
|
||||||
|
case "$or":
|
||||||
|
join_op = " OR ";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(__("Invalid operator {0}", op).__());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof obj !== "object")
|
||||||
|
{
|
||||||
|
throw new Error(__("Invalid input data for operator {0}", op).__());
|
||||||
|
}
|
||||||
|
|
||||||
|
let arr = [];
|
||||||
|
for(let k in obj){
|
||||||
|
if(k == "$and" || k=="$or")
|
||||||
|
{
|
||||||
|
arr.push(this.where(k, obj[k]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arr.push(this.binary(k, obj[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `( ${arr.join(join_op)} )`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class SQLiteDBCore {
|
||||||
|
static REGISTY: GenericObject<VFS.BaseFileHandle>;
|
||||||
|
|
||||||
private db_file: VFS.BaseFileHandle;
|
private db_file: VFS.BaseFileHandle;
|
||||||
|
|
||||||
constructor(path: VFS.BaseFileHandle | string)
|
constructor(path: VFS.BaseFileHandle | string)
|
||||||
{
|
{
|
||||||
|
if(!SQLiteDBCore.REGISTY)
|
||||||
|
{
|
||||||
|
SQLiteDBCore.REGISTY = {};
|
||||||
|
}
|
||||||
this.db_file = path.asFileHandle();
|
this.db_file = path.asFileHandle();
|
||||||
|
if(SQLiteDBCore.REGISTY[this.db_file.path])
|
||||||
|
{
|
||||||
|
this.db_file = SQLiteDBCore.REGISTY[this.db_file.path];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SQLiteDBCore.REGISTY[this.db_file.path] = this.db_file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private pwd(): VFS.BaseFileHandle
|
private pwd(): VFS.BaseFileHandle
|
||||||
{
|
{
|
||||||
return "pkg://SQLiteDB/".asFileHandle();
|
return "pkg://SQLiteDB/".asFileHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileinfo(): FileInfoType
|
||||||
|
{
|
||||||
|
return this.db_file.info;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* init and create the db file if it does not exist
|
* init and create the db file if it does not exist
|
||||||
*/
|
*/
|
||||||
private init(): Promise<any>
|
init(): Promise<any>
|
||||||
{
|
{
|
||||||
return new Promise(async (ok, reject) => {
|
return new Promise(async (ok, reject) => {
|
||||||
try{
|
try{
|
||||||
|
if(this.db_file.ready)
|
||||||
|
{
|
||||||
|
return ok(true);
|
||||||
|
}
|
||||||
let request = {
|
let request = {
|
||||||
action: 'init',
|
action: 'init',
|
||||||
args: {
|
args: {
|
||||||
@ -77,43 +353,92 @@ namespace OS {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
query(sql: string): Promise<any>
|
|
||||||
{
|
|
||||||
let rq = {
|
|
||||||
action: 'query',
|
|
||||||
args: {
|
|
||||||
query: sql
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.request(rq);
|
|
||||||
}
|
|
||||||
|
|
||||||
select(table: string, fields: string[], condition: string): Promise<GenericObject<any>[]>
|
select(filter: GenericObject<any>): Promise<any>
|
||||||
{
|
{
|
||||||
let rq = {
|
let rq = {
|
||||||
action: 'select',
|
action: 'select',
|
||||||
args: {
|
args: {
|
||||||
table: table,
|
filter
|
||||||
fields: fields.join(","),
|
|
||||||
cond: condition
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
|
|
||||||
list_tables(): Promise<string[]>
|
delete_records(filter: GenericObject<any>): Promise<any>
|
||||||
{
|
{
|
||||||
return new Promise(async (ok, reject) => {
|
let rq = {
|
||||||
try {
|
action: 'delete_records',
|
||||||
let result = await this.select(
|
args: {
|
||||||
"sqlite_master", ["name"], "type ='table'");
|
filter
|
||||||
return ok(result.map((e) => e.name))
|
|
||||||
}
|
}
|
||||||
catch(e)
|
}
|
||||||
{
|
return this.request(rq);
|
||||||
reject(__e(e))
|
}
|
||||||
|
|
||||||
|
drop_table(table_name: string): Promise<any>
|
||||||
|
{
|
||||||
|
let rq = {
|
||||||
|
action: 'drop_table',
|
||||||
|
args:{table_name}
|
||||||
|
}
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
|
||||||
|
list_tables(): Promise<any>
|
||||||
|
{
|
||||||
|
let rq = {
|
||||||
|
action: 'list_table',
|
||||||
|
args: {}
|
||||||
|
}
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
|
||||||
|
create_table(table, scheme): Promise<any>
|
||||||
|
{
|
||||||
|
let rq = {
|
||||||
|
action: 'create_table',
|
||||||
|
args: {
|
||||||
|
table_name: table,
|
||||||
|
scheme
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_table_scheme(table_name:string): Promise<any>
|
||||||
|
{
|
||||||
|
let rq = {
|
||||||
|
action: 'table_scheme',
|
||||||
|
args: {
|
||||||
|
table_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(table_name:string, record: GenericObject<any>): Promise<any>
|
||||||
|
{
|
||||||
|
let rq = {
|
||||||
|
action: 'insert',
|
||||||
|
args: {
|
||||||
|
table_name,
|
||||||
|
record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(table_name:string, record: GenericObject<any>): Promise<any>
|
||||||
|
{
|
||||||
|
let rq = {
|
||||||
|
action: 'update',
|
||||||
|
args: {
|
||||||
|
table_name,
|
||||||
|
record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
|
|
||||||
last_insert_id(): Promise<number>
|
last_insert_id(): Promise<number>
|
||||||
@ -126,5 +451,260 @@ namespace OS {
|
|||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export namespace VFS {
|
||||||
|
/**
|
||||||
|
* SQLite VFS handle for database accessing
|
||||||
|
*
|
||||||
|
* A Sqlite file handle shall be in the following formats:
|
||||||
|
* * `sqlite://remote/path/to/file.db` refers to the entire databale (`remote/path/to/file.db` is relative to the home folder)
|
||||||
|
* - read operation, will list all available tables
|
||||||
|
* - write operations will create table
|
||||||
|
* - rm operation will delete table
|
||||||
|
* - meta operation will return file info
|
||||||
|
* - other operations are not supported
|
||||||
|
* * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database
|
||||||
|
* - meta operation will return fileinfo with table scheme information
|
||||||
|
* - read operation will read all records by filter defined by the filter operation
|
||||||
|
* - write operations will insert a new record
|
||||||
|
* - rm operation will delete records by filter defined by the filter operation
|
||||||
|
* - filter operation sets the filter for the table
|
||||||
|
* - other operations are not supported
|
||||||
|
* - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id`
|
||||||
|
* - read operation will read the current record
|
||||||
|
* - write operation will update current record
|
||||||
|
* - rm operation will delete current record
|
||||||
|
* - other operations are not supported
|
||||||
|
*
|
||||||
|
* Some example of filters:
|
||||||
|
* ```ts
|
||||||
|
* handle.filter = (filter) => {
|
||||||
|
* filter.fields()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @class SqliteFileHandle
|
||||||
|
* @extends {BaseFileHandle}
|
||||||
|
*/
|
||||||
|
class SqliteFileHandle extends BaseFileHandle
|
||||||
|
{
|
||||||
|
private _handle: SQLiteDBCore;
|
||||||
|
private _table_name: string;
|
||||||
|
private _id: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a file path to the current file handle
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {string} p
|
||||||
|
* @returns {void}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
setPath(p: string): void {
|
||||||
|
let arr = p.split("@");
|
||||||
|
super.setPath(arr[0]);
|
||||||
|
if(arr.length > 3)
|
||||||
|
{
|
||||||
|
throw new Error(__("Invalid file path").__());
|
||||||
|
}
|
||||||
|
this.path = p;
|
||||||
|
this._table_name = arr[1];
|
||||||
|
this._id = arr[2] ? parseInt(arr[2]) : undefined;
|
||||||
|
this._handle = new SQLiteDBCore(`home://${this.genealogy.join("/")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read database file meta-data
|
||||||
|
*
|
||||||
|
* Return file info on the target database file, if the table_name is specified
|
||||||
|
* return also the table scheme
|
||||||
|
*
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
meta(): Promise<RequestResult> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await this._handle.init();
|
||||||
|
|
||||||
|
const d = {result: this._handle.fileinfo(), error: false};
|
||||||
|
if(this._table_name)
|
||||||
|
{
|
||||||
|
const data = await this._handle.get_table_scheme(this._table_name);
|
||||||
|
if(data.length == 0)
|
||||||
|
{
|
||||||
|
d.result.scheme = undefined
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
d.result.scheme = {}
|
||||||
|
for(let v of data)
|
||||||
|
{
|
||||||
|
d.result.scheme[v.name] = v.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolve(d);
|
||||||
|
} catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the database based on the provided info
|
||||||
|
*
|
||||||
|
* If no table is provided, return list of tables in the
|
||||||
|
* data base.
|
||||||
|
* If the current table is specified:
|
||||||
|
* - if the record id is specfied return the record
|
||||||
|
* - otherwise, return the records in the table using the specified filter
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {any} t filter type
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
protected _rd(user_data: any): Promise<any> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try{
|
||||||
|
if(this._table_name && ! this.info.scheme)
|
||||||
|
{
|
||||||
|
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
|
||||||
|
}
|
||||||
|
if(!this._table_name)
|
||||||
|
{
|
||||||
|
// return list of tables in form of data base file handles in ready mode
|
||||||
|
let list = await this._handle.list_tables();
|
||||||
|
const map = {} as GenericObject<BaseFileHandle>;
|
||||||
|
for(let v of list)
|
||||||
|
{
|
||||||
|
map[v.name] = `${this.path}@${v.name}`.asFileHandle();
|
||||||
|
}
|
||||||
|
this.cache = map;
|
||||||
|
resolve(map);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// return all the data in the table set by the filter
|
||||||
|
// if this is a table, return the filtered records
|
||||||
|
// otherwise, it is a record, fetch only that record
|
||||||
|
let filter = user_data;
|
||||||
|
if(!filter || this._id)
|
||||||
|
{
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
filter.table_name = this._table_name;
|
||||||
|
if(this._id)
|
||||||
|
{
|
||||||
|
filter.where = { id: this._id};
|
||||||
|
}
|
||||||
|
let data = await this._handle.select(filter);
|
||||||
|
if(this._id)
|
||||||
|
{
|
||||||
|
this.cache = data[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.cache = data;
|
||||||
|
}
|
||||||
|
resolve(this.cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write commit file cache to the remote database
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {string} t is table name, used only when create table
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
protected _wr(t: string): Promise<RequestResult> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try{
|
||||||
|
if(!this.cache)
|
||||||
|
{
|
||||||
|
throw new Error(__("No data to submit to remote database, please check the `cache` field").__());
|
||||||
|
}
|
||||||
|
if(this._id && this._table_name)
|
||||||
|
{
|
||||||
|
this.cache.id = this._id;
|
||||||
|
const ret = await this._handle.update(this._table_name, this.cache);
|
||||||
|
resolve({result:ret, error: false});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this._table_name)
|
||||||
|
{
|
||||||
|
const ret = await this._handle.insert(this._table_name, this.cache);
|
||||||
|
resolve({result:ret, error: false});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// create a new table with the scheme provided in the cache
|
||||||
|
let r = await this._handle.create_table(t, this.cache);
|
||||||
|
resolve({result: r, error: false});
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete data from remote database
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {any} user_data is table name, for delete table, otherwise, filter object for deleting records
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
protected _rm(user_data: any): Promise<RequestResult> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if(this._table_name && ! this.info.scheme)
|
||||||
|
{
|
||||||
|
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
|
||||||
|
}
|
||||||
|
if(!this._table_name)
|
||||||
|
{
|
||||||
|
let table_name = user_data as string;
|
||||||
|
if (!table_name)
|
||||||
|
{
|
||||||
|
throw new Error(__("No table specified for dropping").__());
|
||||||
|
}
|
||||||
|
let ret = await this._handle.drop_table(table_name);
|
||||||
|
resolve({result: ret, error: false});
|
||||||
|
// delete the table
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let filter = user_data as GenericObject<any>;
|
||||||
|
// delete the records in the table using the filter
|
||||||
|
if(!filter || this._id)
|
||||||
|
{
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
filter.table_name = this._table_name;
|
||||||
|
if(this._id)
|
||||||
|
{
|
||||||
|
filter.where = { id: this._id};
|
||||||
|
}
|
||||||
|
let ret = await this._handle.delete_records(filter);
|
||||||
|
resolve({result: ret, error: false});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
register("^sqlite$", SqliteFileHandle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,3 @@
|
|||||||
# SQLiteDB
|
# SQLiteDB
|
||||||
This is an example project, generated by AntOS Development Kit
|
|
||||||
|
|
||||||
## Howto
|
"mimes":["application/vnd.sqlite3"],
|
||||||
Use the Antedit command palette to access to the SDK functionalities:
|
|
||||||
|
|
||||||
1. Create new project
|
|
||||||
2. Init the project from the current folder located in side bar
|
|
||||||
3. Build and run the project
|
|
||||||
4. Release the project in zip package
|
|
||||||
|
|
||||||
## Set up build target
|
|
||||||
|
|
||||||
Open the `build.json` file from the current project tree and add/remove
|
|
||||||
build target entries and jobs. Save the file
|
|
||||||
|
@ -3,15 +3,15 @@ local args=...
|
|||||||
|
|
||||||
-- require libs
|
-- require libs
|
||||||
local vfs = require("vfs")
|
local vfs = require("vfs")
|
||||||
local sqlite = modules.sqlite()
|
|
||||||
|
|
||||||
-- helper functions
|
-- helper functions
|
||||||
local result = function(data)
|
local result = function(data)
|
||||||
return { error = false, result = data }
|
return { error = false, result = data }
|
||||||
end
|
end
|
||||||
|
|
||||||
local error = function(msg)
|
local error = function(msg,...)
|
||||||
return {error = msg, result = false}
|
local err_msg = string.format(msg or "ERROR",...)
|
||||||
|
return {error = err_msg, result = false}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- handler object
|
-- handler object
|
||||||
@ -21,49 +21,204 @@ local handle = {}
|
|||||||
|
|
||||||
handle.init = function(data)
|
handle.init = function(data)
|
||||||
local os_path = vfs.ospath(data.db_source)
|
local os_path = vfs.ospath(data.db_source)
|
||||||
local db = sqlite._getdb(os_path)
|
local db = sqlite.db(os_path)
|
||||||
if not db then
|
if not db then
|
||||||
return error("Unable to open sqlite db file")
|
return error("init: Unable to open sqlite db file")
|
||||||
end
|
end
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
return result(true)
|
return result(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
handle.query = function(data)
|
handle.update = function(data)
|
||||||
|
if not data.table_name or not data.record or not data.db_source then
|
||||||
|
return error("update: Invalid request data")
|
||||||
|
end
|
||||||
|
if not data.record.id then
|
||||||
|
return error("update: unknown record id for record")
|
||||||
|
end
|
||||||
local os_path = vfs.ospath(data.db_source)
|
local os_path = vfs.ospath(data.db_source)
|
||||||
local db = sqlite._getdb(os_path)
|
local db = sqlite.db(os_path)
|
||||||
if not db then
|
if not db then
|
||||||
return error("Unable to open sqlite db file")
|
return error("update: Unable to open sqlite db file")
|
||||||
end
|
end
|
||||||
local ret = sqlite.query(db, data.query)
|
local tb = {}
|
||||||
|
local gen = SQLQueryGenerator:new({})
|
||||||
|
for k,v in pairs(data.record) do
|
||||||
|
if k ~= "id" then
|
||||||
|
table.insert(tb, string.format("%s=%s", k, gen:parse_value(v, {[type(v)] = true})))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sql = string.format("UPDATE %s SET %s WHERE id = %d", data.table_name, table.concat(tb,","), data.record.id)
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
if ret ~= 1 then
|
if not ret then
|
||||||
return error("error executing query")
|
return error("insert: Unable to insert to %s: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.drop_table = function(data)
|
||||||
|
if not data.table_name or not data.db_source then
|
||||||
|
return error("drop_table: Invalid request data")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("drop_table: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local sql = string.format("DROP TABLE IF EXISTS %s;", data.table_name)
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("drop_table: Unable to drop table %s: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.insert = function(data)
|
||||||
|
if not data.table_name or not data.record or not data.db_source then
|
||||||
|
return error("insert: Invalid request data")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("insert: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local keys = {}
|
||||||
|
local vals = {}
|
||||||
|
local gen = SQLQueryGenerator:new({})
|
||||||
|
for k,v in pairs(data.record) do
|
||||||
|
if k ~= "id" then
|
||||||
|
table.insert(keys,k)
|
||||||
|
table.insert(vals,gen:parse_value(v, {[type(v)] = true}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sql = string.format("INSERT INTO %s (%s) VALUES(%s)", data.table_name, table.concat(keys,","), table.concat(vals,","))
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("insert: Unable to insert to %s: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.create_table = function(data)
|
||||||
|
if not data.table_name or not data.scheme or not data.db_source then
|
||||||
|
return error("create_table: Invalid request data")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("create_table: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local tb = {}
|
||||||
|
for k,v in pairs(data.scheme) do
|
||||||
|
table.insert(tb, k.." "..v)
|
||||||
|
end
|
||||||
|
local sql = string.format("CREATE TABLE IF NOT EXISTS %s(id INTEGER PRIMARY KEY,%s)", data.table_name, table.concat(tb,","))
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret,err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("create_table: Unable to create table %s with the provided scheme: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
end
|
end
|
||||||
return result(true)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
handle.select = function(data)
|
handle.select = function(data)
|
||||||
local os_path = vfs.ospath(data.db_source)
|
if not data.filter then
|
||||||
local db = sqlite._getdb(os_path)
|
return error("select: No filter provided")
|
||||||
if not db then
|
|
||||||
return error("Unable to open sqlite db file")
|
|
||||||
end
|
end
|
||||||
local ret = sqlite.select(db, data.table, data.fields, data.cond);
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("select: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local generator = SQLQueryGenerator:new(data.filter)
|
||||||
|
local r,sql = generator:sql_select()
|
||||||
|
if not r then
|
||||||
|
return error(sql)
|
||||||
|
end
|
||||||
|
LOG_DEBUG("Execute query: %s", sql);
|
||||||
|
local ret, err = sqlite.query(db, sql);
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
if not ret then
|
if not ret then
|
||||||
return error("error executing select statement")
|
return error("select: error executing query statement: %s ", err)
|
||||||
|
end
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
handle.delete_records = function(data)
|
||||||
|
if not data.filter then
|
||||||
|
return error("delete_records: No filter provided")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("delete_records: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local generator = SQLQueryGenerator:new(data.filter)
|
||||||
|
local r,sql = generator:sql_delete()
|
||||||
|
if not r then
|
||||||
|
return error(sql)
|
||||||
|
end
|
||||||
|
LOG_DEBUG("Execute query: %s", sql);
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("delete_records: error executing query statement: %s ", err)
|
||||||
|
end
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
handle.table_scheme = function(data)
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("table_scheme: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local sql = string.format("SELECT p.name, p.type FROM sqlite_master AS m JOIN pragma_table_info(m.name) AS p WHERE m.type ='table' AND m.name = '%s'", data.table_name)
|
||||||
|
local ret, err = sqlite.query(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("table_scheme: error executing query statement: %s", err)
|
||||||
|
end
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
handle.list_table = function(data)
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("list_table: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local sql = "SELECT name FROM sqlite_master WHERE type ='table'"
|
||||||
|
local ret, err = sqlite.query(db, sql)
|
||||||
|
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("table_scheme: error executing query statement: %s", err)
|
||||||
end
|
end
|
||||||
return result(ret)
|
return result(ret)
|
||||||
end
|
end
|
||||||
|
|
||||||
handle.last_insert_id = function(data)
|
handle.last_insert_id = function(data)
|
||||||
local os_path = vfs.ospath(data.db_source)
|
local os_path = vfs.ospath(data.db_source)
|
||||||
local db = sqlite._getdb(os_path)
|
local db = sqlite.db(os_path)
|
||||||
if not db then
|
if not db then
|
||||||
return error("Unable to open sqlite db file")
|
return error("last_insert_id: Unable to open sqlite db file")
|
||||||
end
|
end
|
||||||
local ret = sqlite.lastInsertID(db)
|
local ret = sqlite.last_insert_id(db)
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
return result(ret)
|
return result(ret)
|
||||||
end
|
end
|
||||||
|
@ -1,15 +1,3 @@
|
|||||||
# SQLiteDB
|
# SQLiteDB
|
||||||
This is an example project, generated by AntOS Development Kit
|
|
||||||
|
|
||||||
## Howto
|
"mimes":["application/vnd.sqlite3"],
|
||||||
Use the Antedit command palette to access to the SDK functionalities:
|
|
||||||
|
|
||||||
1. Create new project
|
|
||||||
2. Init the project from the current folder located in side bar
|
|
||||||
3. Build and run the project
|
|
||||||
4. Release the project in zip package
|
|
||||||
|
|
||||||
## Set up build target
|
|
||||||
|
|
||||||
Open the `build.json` file from the current project tree and add/remove
|
|
||||||
build target entries and jobs. Save the file
|
|
||||||
|
@ -3,15 +3,15 @@ local args=...
|
|||||||
|
|
||||||
-- require libs
|
-- require libs
|
||||||
local vfs = require("vfs")
|
local vfs = require("vfs")
|
||||||
local sqlite = modules.sqlite()
|
|
||||||
|
|
||||||
-- helper functions
|
-- helper functions
|
||||||
local result = function(data)
|
local result = function(data)
|
||||||
return { error = false, result = data }
|
return { error = false, result = data }
|
||||||
end
|
end
|
||||||
|
|
||||||
local error = function(msg)
|
local error = function(msg,...)
|
||||||
return {error = msg, result = false}
|
local err_msg = string.format(msg or "ERROR",...)
|
||||||
|
return {error = err_msg, result = false}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- handler object
|
-- handler object
|
||||||
@ -21,49 +21,204 @@ local handle = {}
|
|||||||
|
|
||||||
handle.init = function(data)
|
handle.init = function(data)
|
||||||
local os_path = vfs.ospath(data.db_source)
|
local os_path = vfs.ospath(data.db_source)
|
||||||
local db = sqlite._getdb(os_path)
|
local db = sqlite.db(os_path)
|
||||||
if not db then
|
if not db then
|
||||||
return error("Unable to open sqlite db file")
|
return error("init: Unable to open sqlite db file")
|
||||||
end
|
end
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
return result(true)
|
return result(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
handle.query = function(data)
|
handle.update = function(data)
|
||||||
|
if not data.table_name or not data.record or not data.db_source then
|
||||||
|
return error("update: Invalid request data")
|
||||||
|
end
|
||||||
|
if not data.record.id then
|
||||||
|
return error("update: unknown record id for record")
|
||||||
|
end
|
||||||
local os_path = vfs.ospath(data.db_source)
|
local os_path = vfs.ospath(data.db_source)
|
||||||
local db = sqlite._getdb(os_path)
|
local db = sqlite.db(os_path)
|
||||||
if not db then
|
if not db then
|
||||||
return error("Unable to open sqlite db file")
|
return error("update: Unable to open sqlite db file")
|
||||||
end
|
end
|
||||||
local ret = sqlite.query(db, data.query)
|
local tb = {}
|
||||||
|
local gen = SQLQueryGenerator:new({})
|
||||||
|
for k,v in pairs(data.record) do
|
||||||
|
if k ~= "id" then
|
||||||
|
table.insert(tb, string.format("%s=%s", k, gen:parse_value(v, {[type(v)] = true})))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sql = string.format("UPDATE %s SET %s WHERE id = %d", data.table_name, table.concat(tb,","), data.record.id)
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
if ret ~= 1 then
|
if not ret then
|
||||||
return error("error executing query")
|
return error("insert: Unable to insert to %s: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.drop_table = function(data)
|
||||||
|
if not data.table_name or not data.db_source then
|
||||||
|
return error("drop_table: Invalid request data")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("drop_table: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local sql = string.format("DROP TABLE IF EXISTS %s;", data.table_name)
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("drop_table: Unable to drop table %s: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.insert = function(data)
|
||||||
|
if not data.table_name or not data.record or not data.db_source then
|
||||||
|
return error("insert: Invalid request data")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("insert: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local keys = {}
|
||||||
|
local vals = {}
|
||||||
|
local gen = SQLQueryGenerator:new({})
|
||||||
|
for k,v in pairs(data.record) do
|
||||||
|
if k ~= "id" then
|
||||||
|
table.insert(keys,k)
|
||||||
|
table.insert(vals,gen:parse_value(v, {[type(v)] = true}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local sql = string.format("INSERT INTO %s (%s) VALUES(%s)", data.table_name, table.concat(keys,","), table.concat(vals,","))
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("insert: Unable to insert to %s: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle.create_table = function(data)
|
||||||
|
if not data.table_name or not data.scheme or not data.db_source then
|
||||||
|
return error("create_table: Invalid request data")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("create_table: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local tb = {}
|
||||||
|
for k,v in pairs(data.scheme) do
|
||||||
|
table.insert(tb, k.." "..v)
|
||||||
|
end
|
||||||
|
local sql = string.format("CREATE TABLE IF NOT EXISTS %s(id INTEGER PRIMARY KEY,%s)", data.table_name, table.concat(tb,","))
|
||||||
|
LOG_DEBUG("Execute query: [%s]", sql)
|
||||||
|
local ret,err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("create_table: Unable to create table %s with the provided scheme: %s", data.table_name, err)
|
||||||
|
else
|
||||||
|
return result(ret)
|
||||||
end
|
end
|
||||||
return result(true)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
handle.select = function(data)
|
handle.select = function(data)
|
||||||
local os_path = vfs.ospath(data.db_source)
|
if not data.filter then
|
||||||
local db = sqlite._getdb(os_path)
|
return error("select: No filter provided")
|
||||||
if not db then
|
|
||||||
return error("Unable to open sqlite db file")
|
|
||||||
end
|
end
|
||||||
local ret = sqlite.select(db, data.table, data.fields, data.cond);
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("select: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local generator = SQLQueryGenerator:new(data.filter)
|
||||||
|
local r,sql = generator:sql_select()
|
||||||
|
if not r then
|
||||||
|
return error(sql)
|
||||||
|
end
|
||||||
|
LOG_DEBUG("Execute query: %s", sql);
|
||||||
|
local ret, err = sqlite.query(db, sql);
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
if not ret then
|
if not ret then
|
||||||
return error("error executing select statement")
|
return error("select: error executing query statement: %s ", err)
|
||||||
|
end
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
handle.delete_records = function(data)
|
||||||
|
if not data.filter then
|
||||||
|
return error("delete_records: No filter provided")
|
||||||
|
end
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("delete_records: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local generator = SQLQueryGenerator:new(data.filter)
|
||||||
|
local r,sql = generator:sql_delete()
|
||||||
|
if not r then
|
||||||
|
return error(sql)
|
||||||
|
end
|
||||||
|
LOG_DEBUG("Execute query: %s", sql);
|
||||||
|
local ret, err = sqlite.exec(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("delete_records: error executing query statement: %s ", err)
|
||||||
|
end
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
handle.table_scheme = function(data)
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("table_scheme: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local sql = string.format("SELECT p.name, p.type FROM sqlite_master AS m JOIN pragma_table_info(m.name) AS p WHERE m.type ='table' AND m.name = '%s'", data.table_name)
|
||||||
|
local ret, err = sqlite.query(db, sql);
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("table_scheme: error executing query statement: %s", err)
|
||||||
|
end
|
||||||
|
return result(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
handle.list_table = function(data)
|
||||||
|
local os_path = vfs.ospath(data.db_source)
|
||||||
|
local db = sqlite.db(os_path)
|
||||||
|
if not db then
|
||||||
|
return error("list_table: Unable to open sqlite db file")
|
||||||
|
end
|
||||||
|
local sql = "SELECT name FROM sqlite_master WHERE type ='table'"
|
||||||
|
local ret, err = sqlite.query(db, sql)
|
||||||
|
|
||||||
|
sqlite.dbclose(db)
|
||||||
|
if not ret then
|
||||||
|
return error("table_scheme: error executing query statement: %s", err)
|
||||||
end
|
end
|
||||||
return result(ret)
|
return result(ret)
|
||||||
end
|
end
|
||||||
|
|
||||||
handle.last_insert_id = function(data)
|
handle.last_insert_id = function(data)
|
||||||
local os_path = vfs.ospath(data.db_source)
|
local os_path = vfs.ospath(data.db_source)
|
||||||
local db = sqlite._getdb(os_path)
|
local db = sqlite.db(os_path)
|
||||||
if not db then
|
if not db then
|
||||||
return error("Unable to open sqlite db file")
|
return error("last_insert_id: Unable to open sqlite db file")
|
||||||
end
|
end
|
||||||
local ret = sqlite.lastInsertID(db)
|
local ret = sqlite.last_insert_id(db)
|
||||||
sqlite.dbclose(db)
|
sqlite.dbclose(db)
|
||||||
return result(ret)
|
return result(ret)
|
||||||
end
|
end
|
||||||
|
@ -3,19 +3,238 @@ var OS;
|
|||||||
(function (OS) {
|
(function (OS) {
|
||||||
let API;
|
let API;
|
||||||
(function (API) {
|
(function (API) {
|
||||||
class SQLiteDBBase {
|
/**
|
||||||
|
* Generate SQL expression from input object
|
||||||
|
*
|
||||||
|
* Example of input object
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* where: {
|
||||||
|
* id$gte: 10,
|
||||||
|
* user: "dany'",
|
||||||
|
* $or: {
|
||||||
|
* 'user.email': "test@mail.com",
|
||||||
|
* age$lte: 30,
|
||||||
|
* $and: {
|
||||||
|
* 'user.birth$ne': 1986,
|
||||||
|
* age$not_between: [20,30],
|
||||||
|
* name$not_like: "%LE"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* fields: ['name as n', 'id', 'email'],
|
||||||
|
* order: ['user.name$asc', "id$desc"],
|
||||||
|
* joins: {
|
||||||
|
* cid: 'Category.id',
|
||||||
|
* did: 'Country.id'
|
||||||
|
* }
|
||||||
|
*}
|
||||||
|
* ```
|
||||||
|
* This will generate the followings expressions:
|
||||||
|
* - `( self.name as n,self.id,self.email )` for fields
|
||||||
|
* - condition:
|
||||||
|
* ```
|
||||||
|
* (
|
||||||
|
* ( self.id >= 10 ) AND
|
||||||
|
* ( self.user = 'dany''' ) AND
|
||||||
|
* (
|
||||||
|
* ( user.email = 'test@mail.com' ) OR
|
||||||
|
* ( self.age <= 30 ) OR
|
||||||
|
* (
|
||||||
|
* ( user.birth != 1986 ) AND
|
||||||
|
* ( self.age NOT BETWEEN 20 AND 30 ) AND
|
||||||
|
* ( self.name NOT LIKE '%LE' )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* - order: `user.name ASC,self.id DESC`
|
||||||
|
* - joining:
|
||||||
|
* ```
|
||||||
|
* INNER JOIN Category ON self.cid = Category.id
|
||||||
|
* INNER JOIN Country ON self.did = Country.id
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class SQLiteQueryGenerator {
|
||||||
|
constructor(obj) {
|
||||||
|
this._where = undefined;
|
||||||
|
this._fields = undefined;
|
||||||
|
this._order = undefined;
|
||||||
|
this._joins = undefined;
|
||||||
|
this._is_joining = false;
|
||||||
|
if (obj.joins) {
|
||||||
|
this._is_joining = true;
|
||||||
|
this._joins = this.joins(obj.joins);
|
||||||
|
}
|
||||||
|
if (obj.where) {
|
||||||
|
this._where = this.where("$and", obj.where);
|
||||||
|
}
|
||||||
|
if (obj.fields) {
|
||||||
|
this._fields = `( ${obj.fields.map(v => this.infer_field(v)).join(",")} )`;
|
||||||
|
}
|
||||||
|
if (obj.order) {
|
||||||
|
this._order = this.order_by(obj.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
infer_field(k) {
|
||||||
|
if (!this._is_joining || k.indexOf(".") > 0)
|
||||||
|
return k;
|
||||||
|
return `self.${k}`;
|
||||||
|
}
|
||||||
|
joins(data) {
|
||||||
|
let joins_arr = [];
|
||||||
|
for (let k in data) {
|
||||||
|
let v = data[k];
|
||||||
|
let arr = v.split('.');
|
||||||
|
if (arr.length != 2) {
|
||||||
|
throw new Error(__("Other table name parsing error: {0}", v).__());
|
||||||
|
}
|
||||||
|
joins_arr.push(`INNER JOIN ${arr[0]} ON ${this.infer_field(k)} = ${v}`);
|
||||||
|
}
|
||||||
|
return joins_arr.join(" ");
|
||||||
|
}
|
||||||
|
print() {
|
||||||
|
console.log(this._fields);
|
||||||
|
console.log(this._where);
|
||||||
|
console.log(this._order);
|
||||||
|
console.log(this._joins);
|
||||||
|
}
|
||||||
|
order_by(order) {
|
||||||
|
if (!Array.isArray(order)) {
|
||||||
|
throw new Error(__("Invalid type: expect array get {0}", typeof (order)).__());
|
||||||
|
}
|
||||||
|
return order.map((v, _) => {
|
||||||
|
const arr = v.split('$');
|
||||||
|
if (arr.length != 2) {
|
||||||
|
throw new Error(__("Invalid field order format {0}", v).__());
|
||||||
|
}
|
||||||
|
switch (arr[1]) {
|
||||||
|
case 'asc': return `${this.infer_field(arr[0])} ASC`;
|
||||||
|
case 'desc': return `${this.infer_field(arr[0])} DESC`;
|
||||||
|
default: throw new Error(__("Invalid field order type {0}", v).__());
|
||||||
|
}
|
||||||
|
}).join(",");
|
||||||
|
}
|
||||||
|
escape_string(s) {
|
||||||
|
let regex = /[']/g;
|
||||||
|
var chunkIndex = regex.lastIndex = 0;
|
||||||
|
var escapedVal = '';
|
||||||
|
var match;
|
||||||
|
while ((match = regex.exec(s))) {
|
||||||
|
escapedVal += s.slice(chunkIndex, match.index) + { '\'': '\'\'' }[match[0]];
|
||||||
|
chunkIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
if (chunkIndex === 0) {
|
||||||
|
// Nothing was escaped
|
||||||
|
return "'" + s + "'";
|
||||||
|
}
|
||||||
|
if (chunkIndex < s.length) {
|
||||||
|
return "'" + escapedVal + s.slice(chunkIndex) + "'";
|
||||||
|
}
|
||||||
|
return "'" + escapedVal + "'";
|
||||||
|
}
|
||||||
|
parse_value(v, t) {
|
||||||
|
if (!t.includes(typeof (v))) {
|
||||||
|
throw new Error(__("Invalid type: expect [{0}] get {1}", t.join(","), typeof (v)).__());
|
||||||
|
}
|
||||||
|
switch (typeof (v)) {
|
||||||
|
case 'number': return JSON.stringify(v);
|
||||||
|
case 'string': return this.escape_string(v);
|
||||||
|
default: throw new Error(__("Un supported value {0} of type {1}", v, typeof (v)).__());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binary(k, v) {
|
||||||
|
const arr = k.split("$");
|
||||||
|
if (arr.length > 2) {
|
||||||
|
throw new Error(__("Invalid left hand side format: {0}", k).__());
|
||||||
|
}
|
||||||
|
if (arr.length == 2) {
|
||||||
|
switch (arr[1]) {
|
||||||
|
case "gt":
|
||||||
|
return `( ${this.infer_field(arr[0])} > ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "gte":
|
||||||
|
return `( ${this.infer_field(arr[0])} >= ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "lt":
|
||||||
|
return `( ${this.infer_field(arr[0])} < ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "lte":
|
||||||
|
return `( ${this.infer_field(arr[0])} <= ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "ne":
|
||||||
|
return `( ${this.infer_field(arr[0])} != ${this.parse_value(v, ['number', 'string'])} )`;
|
||||||
|
case "between":
|
||||||
|
return `( ${this.infer_field(arr[0])} BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`;
|
||||||
|
case "not_between":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`;
|
||||||
|
case "in":
|
||||||
|
return `( ${this.infer_field(arr[0])} IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`;
|
||||||
|
case "not_in":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`;
|
||||||
|
case "like":
|
||||||
|
return `( ${this.infer_field(arr[0])} LIKE ${this.parse_value(v, ['string'])} )`;
|
||||||
|
case "not_like":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT LIKE ${this.parse_value(v, ['string'])} )`;
|
||||||
|
default: throw new Error(__("Unsupported operator `{0}`", arr[1]).__());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return `( ${this.infer_field(arr[0])} = ${this.parse_value(v, ['number', 'string'])} )`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
where(op, obj) {
|
||||||
|
let join_op = undefined;
|
||||||
|
switch (op) {
|
||||||
|
case "$and":
|
||||||
|
join_op = " AND ";
|
||||||
|
break;
|
||||||
|
case "$or":
|
||||||
|
join_op = " OR ";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(__("Invalid operator {0}", op).__());
|
||||||
|
}
|
||||||
|
if (typeof obj !== "object") {
|
||||||
|
throw new Error(__("Invalid input data for operator {0}", op).__());
|
||||||
|
}
|
||||||
|
let arr = [];
|
||||||
|
for (let k in obj) {
|
||||||
|
if (k == "$and" || k == "$or") {
|
||||||
|
arr.push(this.where(k, obj[k]));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
arr.push(this.binary(k, obj[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `( ${arr.join(join_op)} )`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class SQLiteDBCore {
|
||||||
constructor(path) {
|
constructor(path) {
|
||||||
|
if (!SQLiteDBCore.REGISTY) {
|
||||||
|
SQLiteDBCore.REGISTY = {};
|
||||||
|
}
|
||||||
this.db_file = path.asFileHandle();
|
this.db_file = path.asFileHandle();
|
||||||
|
if (SQLiteDBCore.REGISTY[this.db_file.path]) {
|
||||||
|
this.db_file = SQLiteDBCore.REGISTY[this.db_file.path];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SQLiteDBCore.REGISTY[this.db_file.path] = this.db_file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pwd() {
|
pwd() {
|
||||||
return "pkg://SQLiteDB/".asFileHandle();
|
return "pkg://SQLiteDB/".asFileHandle();
|
||||||
}
|
}
|
||||||
|
fileinfo() {
|
||||||
|
return this.db_file.info;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* init and create the db file if it doesnot exist
|
* init and create the db file if it does not exist
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
return new Promise(async (ok, reject) => {
|
return new Promise(async (ok, reject) => {
|
||||||
try {
|
try {
|
||||||
|
if (this.db_file.ready) {
|
||||||
|
return ok(true);
|
||||||
|
}
|
||||||
let request = {
|
let request = {
|
||||||
action: 'init',
|
action: 'init',
|
||||||
args: {
|
args: {
|
||||||
@ -64,36 +283,76 @@ var OS;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
query(sql) {
|
select(filter) {
|
||||||
let rq = {
|
let rq = {
|
||||||
action: 'query',
|
action: 'select',
|
||||||
args: {
|
args: {
|
||||||
query: sql
|
filter
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
select(table, fields, condition) {
|
delete_records(filter) {
|
||||||
let rq = {
|
let rq = {
|
||||||
action: 'select',
|
action: 'delete_records',
|
||||||
args: {
|
args: {
|
||||||
table: table,
|
filter
|
||||||
fields: fields.join(","),
|
|
||||||
cond: condition
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
|
drop_table(table_name) {
|
||||||
|
let rq = {
|
||||||
|
action: 'drop_table',
|
||||||
|
args: { table_name }
|
||||||
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
list_tables() {
|
list_tables() {
|
||||||
return new Promise(async (ok, reject) => {
|
let rq = {
|
||||||
try {
|
action: 'list_table',
|
||||||
let result = await this.select("sqlite_master", ["name"], "type ='table'");
|
args: {}
|
||||||
return ok(result.map((e) => e.name));
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
create_table(table, scheme) {
|
||||||
|
let rq = {
|
||||||
|
action: 'create_table',
|
||||||
|
args: {
|
||||||
|
table_name: table,
|
||||||
|
scheme
|
||||||
}
|
}
|
||||||
catch (e) {
|
};
|
||||||
reject(__e(e));
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
get_table_scheme(table_name) {
|
||||||
|
let rq = {
|
||||||
|
action: 'table_scheme',
|
||||||
|
args: {
|
||||||
|
table_name
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
insert(table_name, record) {
|
||||||
|
let rq = {
|
||||||
|
action: 'insert',
|
||||||
|
args: {
|
||||||
|
table_name,
|
||||||
|
record
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
update(table_name, record) {
|
||||||
|
let rq = {
|
||||||
|
action: 'update',
|
||||||
|
args: {
|
||||||
|
table_name,
|
||||||
|
record
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
last_insert_id() {
|
last_insert_id() {
|
||||||
let rq = {
|
let rq = {
|
||||||
@ -103,6 +362,229 @@ var OS;
|
|||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
API.SQLiteDBBase = SQLiteDBBase;
|
let VFS;
|
||||||
|
(function (VFS) {
|
||||||
|
/**
|
||||||
|
* SQLite VFS handle for database accessing
|
||||||
|
*
|
||||||
|
* A Sqlite file handle shall be in the following formats:
|
||||||
|
* * `sqlite://remote/path/to/file.db` refers to the entire databale (`remote/path/to/file.db` is relative to the home folder)
|
||||||
|
* - read operation, will list all available tables
|
||||||
|
* - write operations will create table
|
||||||
|
* - rm operation will delete table
|
||||||
|
* - meta operation will return file info
|
||||||
|
* - other operations are not supported
|
||||||
|
* * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database
|
||||||
|
* - meta operation will return fileinfo with table scheme information
|
||||||
|
* - read operation will read all records by filter defined by the filter operation
|
||||||
|
* - write operations will insert a new record
|
||||||
|
* - rm operation will delete records by filter defined by the filter operation
|
||||||
|
* - filter operation sets the filter for the table
|
||||||
|
* - other operations are not supported
|
||||||
|
* - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id`
|
||||||
|
* - read operation will read the current record
|
||||||
|
* - write operation will update current record
|
||||||
|
* - rm operation will delete current record
|
||||||
|
* - other operations are not supported
|
||||||
|
*
|
||||||
|
* Some example of filters:
|
||||||
|
* ```ts
|
||||||
|
* handle.filter = (filter) => {
|
||||||
|
* filter.fields()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @class SqliteFileHandle
|
||||||
|
* @extends {BaseFileHandle}
|
||||||
|
*/
|
||||||
|
class SqliteFileHandle extends VFS.BaseFileHandle {
|
||||||
|
/**
|
||||||
|
* Set a file path to the current file handle
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {string} p
|
||||||
|
* @returns {void}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
setPath(p) {
|
||||||
|
let arr = p.split("@");
|
||||||
|
super.setPath(arr[0]);
|
||||||
|
if (arr.length > 3) {
|
||||||
|
throw new Error(__("Invalid file path").__());
|
||||||
|
}
|
||||||
|
this.path = p;
|
||||||
|
this._table_name = arr[1];
|
||||||
|
this._id = arr[2] ? parseInt(arr[2]) : undefined;
|
||||||
|
this._handle = new SQLiteDBCore(`home://${this.genealogy.join("/")}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read database file meta-data
|
||||||
|
*
|
||||||
|
* Return file info on the target database file, if the table_name is specified
|
||||||
|
* return also the table scheme
|
||||||
|
*
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
meta() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await this._handle.init();
|
||||||
|
const d = { result: this._handle.fileinfo(), error: false };
|
||||||
|
if (this._table_name) {
|
||||||
|
const data = await this._handle.get_table_scheme(this._table_name);
|
||||||
|
if (data.length == 0) {
|
||||||
|
d.result.scheme = undefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d.result.scheme = {};
|
||||||
|
for (let v of data) {
|
||||||
|
d.result.scheme[v.name] = v.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolve(d);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Query the database based on the provided info
|
||||||
|
*
|
||||||
|
* If no table is provided, return list of tables in the
|
||||||
|
* data base.
|
||||||
|
* If the current table is specified:
|
||||||
|
* - if the record id is specfied return the record
|
||||||
|
* - otherwise, return the records in the table using the specified filter
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {any} t filter type
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
_rd(user_data) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (this._table_name && !this.info.scheme) {
|
||||||
|
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
|
||||||
|
}
|
||||||
|
if (!this._table_name) {
|
||||||
|
// return list of tables in form of data base file handles in ready mode
|
||||||
|
let list = await this._handle.list_tables();
|
||||||
|
const map = {};
|
||||||
|
for (let v of list) {
|
||||||
|
map[v.name] = `${this.path}@${v.name}`.asFileHandle();
|
||||||
|
}
|
||||||
|
this.cache = map;
|
||||||
|
resolve(map);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// return all the data in the table set by the filter
|
||||||
|
// if this is a table, return the filtered records
|
||||||
|
// otherwise, it is a record, fetch only that record
|
||||||
|
let filter = user_data;
|
||||||
|
if (!filter || this._id) {
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
filter.table_name = this._table_name;
|
||||||
|
if (this._id) {
|
||||||
|
filter.where = { id: this._id };
|
||||||
|
}
|
||||||
|
let data = await this._handle.select(filter);
|
||||||
|
if (this._id) {
|
||||||
|
this.cache = data[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.cache = data;
|
||||||
|
}
|
||||||
|
resolve(this.cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Write commit file cache to the remote database
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {string} t is table name, used only when create table
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
_wr(t) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!this.cache) {
|
||||||
|
throw new Error(__("No data to submit to remote database, please check the `cache` field").__());
|
||||||
|
}
|
||||||
|
if (this._id && this._table_name) {
|
||||||
|
this.cache.id = this._id;
|
||||||
|
const ret = await this._handle.update(this._table_name, this.cache);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._table_name) {
|
||||||
|
const ret = await this._handle.insert(this._table_name, this.cache);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create a new table with the scheme provided in the cache
|
||||||
|
let r = await this._handle.create_table(t, this.cache);
|
||||||
|
resolve({ result: r, error: false });
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Delete data from remote database
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {any} user_data is table name, for delete table, otherwise, filter object for deleting records
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
_rm(user_data) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (this._table_name && !this.info.scheme) {
|
||||||
|
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
|
||||||
|
}
|
||||||
|
if (!this._table_name) {
|
||||||
|
let table_name = user_data;
|
||||||
|
if (!table_name) {
|
||||||
|
throw new Error(__("No table specified for dropping").__());
|
||||||
|
}
|
||||||
|
let ret = await this._handle.drop_table(table_name);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
// delete the table
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let filter = user_data;
|
||||||
|
// delete the records in the table using the filter
|
||||||
|
if (!filter || this._id) {
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
filter.table_name = this._table_name;
|
||||||
|
if (this._id) {
|
||||||
|
filter.where = { id: this._id };
|
||||||
|
}
|
||||||
|
let ret = await this._handle.delete_records(filter);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VFS.register("^sqlite$", SqliteFileHandle);
|
||||||
|
})(VFS = API.VFS || (API.VFS = {}));
|
||||||
})(API = OS.API || (OS.API = {}));
|
})(API = OS.API || (OS.API = {}));
|
||||||
})(OS || (OS = {}));
|
})(OS || (OS = {}));
|
||||||
|
@ -4,8 +4,6 @@ var OS;
|
|||||||
(function (OS) {
|
(function (OS) {
|
||||||
let application;
|
let application;
|
||||||
(function (application) {
|
(function (application) {
|
||||||
;
|
|
||||||
;
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @class SQLiteDB
|
* @class SQLiteDB
|
||||||
@ -15,19 +13,200 @@ var OS;
|
|||||||
constructor(args) {
|
constructor(args) {
|
||||||
super("SQLiteDB", args);
|
super("SQLiteDB", args);
|
||||||
}
|
}
|
||||||
main() {
|
menu() {
|
||||||
// YOUR CODE HERE
|
return [
|
||||||
let handle = new OS.API.SQLiteDBBase("home://tmp/test.db");
|
{
|
||||||
handle.list_tables().then((list) => {
|
text: "__(File)",
|
||||||
console.log(list);
|
nodes: [
|
||||||
if (list.indexOf("contacts") < 0) {
|
{
|
||||||
handle.query("CREATE TABLE contacts (id INTEGER PRIMARY KEY,first_name TEXT NOT NULL,last_name TEXT NOT NULL,email TEXT NOT NULL UNIQUE,phone TEXT NOT NULL UNIQUE)");
|
text: "__(New)",
|
||||||
|
dataid: "new",
|
||||||
|
shortcut: 'A-N'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "__(Open)",
|
||||||
|
dataid: "open",
|
||||||
|
shortcut: 'A-O'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onchildselect: (e) => {
|
||||||
|
switch (e.data.item.data.dataid) {
|
||||||
|
case "new":
|
||||||
|
return this.newFile();
|
||||||
|
case "open":
|
||||||
|
return this.openFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
list_tables() {
|
||||||
|
this.filehandle.read()
|
||||||
|
.then((data) => {
|
||||||
|
const list = [];
|
||||||
|
for (let k in data) {
|
||||||
|
list.push({
|
||||||
|
text: k,
|
||||||
|
name: k,
|
||||||
|
handle: data[k]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.tbl_list.data = list;
|
||||||
|
if (list.length > 0) {
|
||||||
|
this.tbl_list.selected = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
handle.last_insert_id().then(o => console.log(o));
|
}
|
||||||
|
openFile() {
|
||||||
|
return this.openDialog("FileDialog", {
|
||||||
|
title: __("Open file"),
|
||||||
|
mimes: this.meta().mimes
|
||||||
|
}).then(async (d) => {
|
||||||
|
this.filehandle = `sqlite://${d.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle();
|
||||||
|
await this.filehandle.onready();
|
||||||
|
this.list_tables();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.error(__("Unable to open database file: {0}", e.toString()), e);
|
||||||
|
});
|
||||||
|
;
|
||||||
|
}
|
||||||
|
newFile() {
|
||||||
|
return this.openDialog("FileDialog", {
|
||||||
|
title: __("Save as"),
|
||||||
|
file: "Untitled.db"
|
||||||
|
}).then(async (f) => {
|
||||||
|
var d;
|
||||||
|
d = f.file.path.asFileHandle();
|
||||||
|
if (f.file.type === "file") {
|
||||||
|
d = d.parent();
|
||||||
|
}
|
||||||
|
const target = `${d.path}/${f.name}`.asFileHandle();
|
||||||
|
this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle();
|
||||||
|
await this.filehandle.onready();
|
||||||
|
this.list_tables();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.error(__("Unable to init database file: {0}", e.toString()), e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
main() {
|
||||||
|
this.filehandle = undefined;
|
||||||
|
this.tbl_list = this.find("tbl-list");
|
||||||
|
this.find("bt-add-table").onbtclick = (e) => {
|
||||||
|
this.openDialog(new NewTableDialog(), {
|
||||||
|
title: __("Create new table")
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
application.SQLiteDB = SQLiteDB;
|
application.SQLiteDB = SQLiteDB;
|
||||||
|
class NewTableDialog extends OS.GUI.BasicDialog {
|
||||||
|
/**
|
||||||
|
* Creates an instance of NewTableDialog.
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super("NewTableDialog");
|
||||||
|
}
|
||||||
|
init() {
|
||||||
|
console.log(this.constructor.scheme);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
main() {
|
||||||
|
super.main();
|
||||||
|
this.container = this.find("container");
|
||||||
|
this.find("btnCancel").onbtclick = (e) => this.quit();
|
||||||
|
this.find("btnAdd").onbtclick = (e) => this.addField("", "", true);
|
||||||
|
$(this.find("wrapper"));
|
||||||
|
$(this.container)
|
||||||
|
.css("overflow-y", "auto");
|
||||||
|
this.find("btnOk").onbtclick = (e) => {
|
||||||
|
const inputs = $("input", this.scheme);
|
||||||
|
let cdata = {};
|
||||||
|
for (let i = 0; i < inputs.length; i += 2) {
|
||||||
|
const key = inputs[i].value.trim();
|
||||||
|
if (key === "") {
|
||||||
|
return this.notify(__("Key cannot be empty"));
|
||||||
|
}
|
||||||
|
if (cdata[key]) {
|
||||||
|
return this.notify(__("Duplicate key: {0}", key));
|
||||||
|
}
|
||||||
|
cdata[key] = inputs[i + 1].value.trim();
|
||||||
|
}
|
||||||
|
if (this.handle)
|
||||||
|
this.handle(cdata);
|
||||||
|
this.quit();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add new input key-value field to the dialog
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
addField(key, value, removable) {
|
||||||
|
const div = $("<div>")
|
||||||
|
.css("width", "100%")
|
||||||
|
.css("display", "flex")
|
||||||
|
.css("flex-direction", "row")
|
||||||
|
.appendTo(this.container);
|
||||||
|
$("<input>")
|
||||||
|
.attr("type", "text")
|
||||||
|
.css("width", "50%")
|
||||||
|
.css("height", "25px")
|
||||||
|
.val(key)
|
||||||
|
.appendTo(div);
|
||||||
|
$("<afx-list-view>")
|
||||||
|
.css("width", "50%")
|
||||||
|
.css("height", "25px")
|
||||||
|
.appendTo(div);
|
||||||
|
if (removable) {
|
||||||
|
const btn = $("<afx-button>");
|
||||||
|
btn[0].uify(undefined);
|
||||||
|
$("button", btn)
|
||||||
|
.css("width", "25px")
|
||||||
|
.css("height", "25px");
|
||||||
|
btn[0].iconclass = "fa fa-minus";
|
||||||
|
btn
|
||||||
|
.on("click", () => {
|
||||||
|
div.remove();
|
||||||
|
})
|
||||||
|
.appendTo(div);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("<div>")
|
||||||
|
.css("width", "25px")
|
||||||
|
.appendTo(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Scheme definition
|
||||||
|
*/
|
||||||
|
NewTableDialog.scheme = `\
|
||||||
|
<afx-app-window width='350' height='300'>
|
||||||
|
<afx-hbox>
|
||||||
|
<div data-width="10" ></div>
|
||||||
|
<afx-vbox>
|
||||||
|
<div data-height="5" ></div>
|
||||||
|
<afx-label text="__(Table layout)" data-height="30"></afx-label>
|
||||||
|
<div data-id="container"></div>
|
||||||
|
<afx-hbox data-height="30">
|
||||||
|
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "30" ></afx-button>
|
||||||
|
<div ></div>
|
||||||
|
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
|
||||||
|
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
|
||||||
|
</afx-hbox>
|
||||||
|
<div data-height="5" ></div>
|
||||||
|
</afx-vbox>
|
||||||
|
<div data-width="10" ></div>
|
||||||
|
</afx-hbox>
|
||||||
|
</afx-app-window>`;
|
||||||
})(application = OS.application || (OS.application = {}));
|
})(application = OS.application || (OS.application = {}));
|
||||||
})(OS || (OS = {}));
|
})(OS || (OS = {}));
|
||||||
|
|
||||||
@ -36,19 +215,238 @@ var OS;
|
|||||||
(function (OS) {
|
(function (OS) {
|
||||||
let API;
|
let API;
|
||||||
(function (API) {
|
(function (API) {
|
||||||
class SQLiteDBBase {
|
/**
|
||||||
|
* Generate SQL expression from input object
|
||||||
|
*
|
||||||
|
* Example of input object
|
||||||
|
* ```ts
|
||||||
|
* {
|
||||||
|
* where: {
|
||||||
|
* id$gte: 10,
|
||||||
|
* user: "dany'",
|
||||||
|
* $or: {
|
||||||
|
* 'user.email': "test@mail.com",
|
||||||
|
* age$lte: 30,
|
||||||
|
* $and: {
|
||||||
|
* 'user.birth$ne': 1986,
|
||||||
|
* age$not_between: [20,30],
|
||||||
|
* name$not_like: "%LE"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* fields: ['name as n', 'id', 'email'],
|
||||||
|
* order: ['user.name$asc', "id$desc"],
|
||||||
|
* joins: {
|
||||||
|
* cid: 'Category.id',
|
||||||
|
* did: 'Country.id'
|
||||||
|
* }
|
||||||
|
*}
|
||||||
|
* ```
|
||||||
|
* This will generate the followings expressions:
|
||||||
|
* - `( self.name as n,self.id,self.email )` for fields
|
||||||
|
* - condition:
|
||||||
|
* ```
|
||||||
|
* (
|
||||||
|
* ( self.id >= 10 ) AND
|
||||||
|
* ( self.user = 'dany''' ) AND
|
||||||
|
* (
|
||||||
|
* ( user.email = 'test@mail.com' ) OR
|
||||||
|
* ( self.age <= 30 ) OR
|
||||||
|
* (
|
||||||
|
* ( user.birth != 1986 ) AND
|
||||||
|
* ( self.age NOT BETWEEN 20 AND 30 ) AND
|
||||||
|
* ( self.name NOT LIKE '%LE' )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* - order: `user.name ASC,self.id DESC`
|
||||||
|
* - joining:
|
||||||
|
* ```
|
||||||
|
* INNER JOIN Category ON self.cid = Category.id
|
||||||
|
* INNER JOIN Country ON self.did = Country.id
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class SQLiteQueryGenerator {
|
||||||
|
constructor(obj) {
|
||||||
|
this._where = undefined;
|
||||||
|
this._fields = undefined;
|
||||||
|
this._order = undefined;
|
||||||
|
this._joins = undefined;
|
||||||
|
this._is_joining = false;
|
||||||
|
if (obj.joins) {
|
||||||
|
this._is_joining = true;
|
||||||
|
this._joins = this.joins(obj.joins);
|
||||||
|
}
|
||||||
|
if (obj.where) {
|
||||||
|
this._where = this.where("$and", obj.where);
|
||||||
|
}
|
||||||
|
if (obj.fields) {
|
||||||
|
this._fields = `( ${obj.fields.map(v => this.infer_field(v)).join(",")} )`;
|
||||||
|
}
|
||||||
|
if (obj.order) {
|
||||||
|
this._order = this.order_by(obj.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
infer_field(k) {
|
||||||
|
if (!this._is_joining || k.indexOf(".") > 0)
|
||||||
|
return k;
|
||||||
|
return `self.${k}`;
|
||||||
|
}
|
||||||
|
joins(data) {
|
||||||
|
let joins_arr = [];
|
||||||
|
for (let k in data) {
|
||||||
|
let v = data[k];
|
||||||
|
let arr = v.split('.');
|
||||||
|
if (arr.length != 2) {
|
||||||
|
throw new Error(__("Other table name parsing error: {0}", v).__());
|
||||||
|
}
|
||||||
|
joins_arr.push(`INNER JOIN ${arr[0]} ON ${this.infer_field(k)} = ${v}`);
|
||||||
|
}
|
||||||
|
return joins_arr.join(" ");
|
||||||
|
}
|
||||||
|
print() {
|
||||||
|
console.log(this._fields);
|
||||||
|
console.log(this._where);
|
||||||
|
console.log(this._order);
|
||||||
|
console.log(this._joins);
|
||||||
|
}
|
||||||
|
order_by(order) {
|
||||||
|
if (!Array.isArray(order)) {
|
||||||
|
throw new Error(__("Invalid type: expect array get {0}", typeof (order)).__());
|
||||||
|
}
|
||||||
|
return order.map((v, _) => {
|
||||||
|
const arr = v.split('$');
|
||||||
|
if (arr.length != 2) {
|
||||||
|
throw new Error(__("Invalid field order format {0}", v).__());
|
||||||
|
}
|
||||||
|
switch (arr[1]) {
|
||||||
|
case 'asc': return `${this.infer_field(arr[0])} ASC`;
|
||||||
|
case 'desc': return `${this.infer_field(arr[0])} DESC`;
|
||||||
|
default: throw new Error(__("Invalid field order type {0}", v).__());
|
||||||
|
}
|
||||||
|
}).join(",");
|
||||||
|
}
|
||||||
|
escape_string(s) {
|
||||||
|
let regex = /[']/g;
|
||||||
|
var chunkIndex = regex.lastIndex = 0;
|
||||||
|
var escapedVal = '';
|
||||||
|
var match;
|
||||||
|
while ((match = regex.exec(s))) {
|
||||||
|
escapedVal += s.slice(chunkIndex, match.index) + { '\'': '\'\'' }[match[0]];
|
||||||
|
chunkIndex = regex.lastIndex;
|
||||||
|
}
|
||||||
|
if (chunkIndex === 0) {
|
||||||
|
// Nothing was escaped
|
||||||
|
return "'" + s + "'";
|
||||||
|
}
|
||||||
|
if (chunkIndex < s.length) {
|
||||||
|
return "'" + escapedVal + s.slice(chunkIndex) + "'";
|
||||||
|
}
|
||||||
|
return "'" + escapedVal + "'";
|
||||||
|
}
|
||||||
|
parse_value(v, t) {
|
||||||
|
if (!t.includes(typeof (v))) {
|
||||||
|
throw new Error(__("Invalid type: expect [{0}] get {1}", t.join(","), typeof (v)).__());
|
||||||
|
}
|
||||||
|
switch (typeof (v)) {
|
||||||
|
case 'number': return JSON.stringify(v);
|
||||||
|
case 'string': return this.escape_string(v);
|
||||||
|
default: throw new Error(__("Un supported value {0} of type {1}", v, typeof (v)).__());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binary(k, v) {
|
||||||
|
const arr = k.split("$");
|
||||||
|
if (arr.length > 2) {
|
||||||
|
throw new Error(__("Invalid left hand side format: {0}", k).__());
|
||||||
|
}
|
||||||
|
if (arr.length == 2) {
|
||||||
|
switch (arr[1]) {
|
||||||
|
case "gt":
|
||||||
|
return `( ${this.infer_field(arr[0])} > ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "gte":
|
||||||
|
return `( ${this.infer_field(arr[0])} >= ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "lt":
|
||||||
|
return `( ${this.infer_field(arr[0])} < ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "lte":
|
||||||
|
return `( ${this.infer_field(arr[0])} <= ${this.parse_value(v, ['number'])} )`;
|
||||||
|
case "ne":
|
||||||
|
return `( ${this.infer_field(arr[0])} != ${this.parse_value(v, ['number', 'string'])} )`;
|
||||||
|
case "between":
|
||||||
|
return `( ${this.infer_field(arr[0])} BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`;
|
||||||
|
case "not_between":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT BETWEEN ${this.parse_value(v[0], ['number'])} AND ${this.parse_value(v[1], ['number'])} )`;
|
||||||
|
case "in":
|
||||||
|
return `( ${this.infer_field(arr[0])} IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`;
|
||||||
|
case "not_in":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT IN [${this.parse_value(v[0], ['number'])}, ${this.parse_value(v[1], ['number'])}] )`;
|
||||||
|
case "like":
|
||||||
|
return `( ${this.infer_field(arr[0])} LIKE ${this.parse_value(v, ['string'])} )`;
|
||||||
|
case "not_like":
|
||||||
|
return `( ${this.infer_field(arr[0])} NOT LIKE ${this.parse_value(v, ['string'])} )`;
|
||||||
|
default: throw new Error(__("Unsupported operator `{0}`", arr[1]).__());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return `( ${this.infer_field(arr[0])} = ${this.parse_value(v, ['number', 'string'])} )`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
where(op, obj) {
|
||||||
|
let join_op = undefined;
|
||||||
|
switch (op) {
|
||||||
|
case "$and":
|
||||||
|
join_op = " AND ";
|
||||||
|
break;
|
||||||
|
case "$or":
|
||||||
|
join_op = " OR ";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(__("Invalid operator {0}", op).__());
|
||||||
|
}
|
||||||
|
if (typeof obj !== "object") {
|
||||||
|
throw new Error(__("Invalid input data for operator {0}", op).__());
|
||||||
|
}
|
||||||
|
let arr = [];
|
||||||
|
for (let k in obj) {
|
||||||
|
if (k == "$and" || k == "$or") {
|
||||||
|
arr.push(this.where(k, obj[k]));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
arr.push(this.binary(k, obj[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `( ${arr.join(join_op)} )`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class SQLiteDBCore {
|
||||||
constructor(path) {
|
constructor(path) {
|
||||||
|
if (!SQLiteDBCore.REGISTY) {
|
||||||
|
SQLiteDBCore.REGISTY = {};
|
||||||
|
}
|
||||||
this.db_file = path.asFileHandle();
|
this.db_file = path.asFileHandle();
|
||||||
|
if (SQLiteDBCore.REGISTY[this.db_file.path]) {
|
||||||
|
this.db_file = SQLiteDBCore.REGISTY[this.db_file.path];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SQLiteDBCore.REGISTY[this.db_file.path] = this.db_file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pwd() {
|
pwd() {
|
||||||
return "pkg://SQLiteDB/".asFileHandle();
|
return "pkg://SQLiteDB/".asFileHandle();
|
||||||
}
|
}
|
||||||
|
fileinfo() {
|
||||||
|
return this.db_file.info;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* init and create the db file if it doesnot exist
|
* init and create the db file if it does not exist
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
return new Promise(async (ok, reject) => {
|
return new Promise(async (ok, reject) => {
|
||||||
try {
|
try {
|
||||||
|
if (this.db_file.ready) {
|
||||||
|
return ok(true);
|
||||||
|
}
|
||||||
let request = {
|
let request = {
|
||||||
action: 'init',
|
action: 'init',
|
||||||
args: {
|
args: {
|
||||||
@ -97,36 +495,76 @@ var OS;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
query(sql) {
|
select(filter) {
|
||||||
let rq = {
|
let rq = {
|
||||||
action: 'query',
|
action: 'select',
|
||||||
args: {
|
args: {
|
||||||
query: sql
|
filter
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
select(table, fields, condition) {
|
delete_records(filter) {
|
||||||
let rq = {
|
let rq = {
|
||||||
action: 'select',
|
action: 'delete_records',
|
||||||
args: {
|
args: {
|
||||||
table: table,
|
filter
|
||||||
fields: fields.join(","),
|
|
||||||
cond: condition
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
|
drop_table(table_name) {
|
||||||
|
let rq = {
|
||||||
|
action: 'drop_table',
|
||||||
|
args: { table_name }
|
||||||
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
list_tables() {
|
list_tables() {
|
||||||
return new Promise(async (ok, reject) => {
|
let rq = {
|
||||||
try {
|
action: 'list_table',
|
||||||
let result = await this.select("sqlite_master", ["name"], "type ='table'");
|
args: {}
|
||||||
return ok(result.map((e) => e.name));
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
create_table(table, scheme) {
|
||||||
|
let rq = {
|
||||||
|
action: 'create_table',
|
||||||
|
args: {
|
||||||
|
table_name: table,
|
||||||
|
scheme
|
||||||
}
|
}
|
||||||
catch (e) {
|
};
|
||||||
reject(__e(e));
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
get_table_scheme(table_name) {
|
||||||
|
let rq = {
|
||||||
|
action: 'table_scheme',
|
||||||
|
args: {
|
||||||
|
table_name
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
insert(table_name, record) {
|
||||||
|
let rq = {
|
||||||
|
action: 'insert',
|
||||||
|
args: {
|
||||||
|
table_name,
|
||||||
|
record
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.request(rq);
|
||||||
|
}
|
||||||
|
update(table_name, record) {
|
||||||
|
let rq = {
|
||||||
|
action: 'update',
|
||||||
|
args: {
|
||||||
|
table_name,
|
||||||
|
record
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
last_insert_id() {
|
last_insert_id() {
|
||||||
let rq = {
|
let rq = {
|
||||||
@ -136,6 +574,229 @@ var OS;
|
|||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
API.SQLiteDBBase = SQLiteDBBase;
|
let VFS;
|
||||||
|
(function (VFS) {
|
||||||
|
/**
|
||||||
|
* SQLite VFS handle for database accessing
|
||||||
|
*
|
||||||
|
* A Sqlite file handle shall be in the following formats:
|
||||||
|
* * `sqlite://remote/path/to/file.db` refers to the entire databale (`remote/path/to/file.db` is relative to the home folder)
|
||||||
|
* - read operation, will list all available tables
|
||||||
|
* - write operations will create table
|
||||||
|
* - rm operation will delete table
|
||||||
|
* - meta operation will return file info
|
||||||
|
* - other operations are not supported
|
||||||
|
* * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database
|
||||||
|
* - meta operation will return fileinfo with table scheme information
|
||||||
|
* - read operation will read all records by filter defined by the filter operation
|
||||||
|
* - write operations will insert a new record
|
||||||
|
* - rm operation will delete records by filter defined by the filter operation
|
||||||
|
* - filter operation sets the filter for the table
|
||||||
|
* - other operations are not supported
|
||||||
|
* - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id`
|
||||||
|
* - read operation will read the current record
|
||||||
|
* - write operation will update current record
|
||||||
|
* - rm operation will delete current record
|
||||||
|
* - other operations are not supported
|
||||||
|
*
|
||||||
|
* Some example of filters:
|
||||||
|
* ```ts
|
||||||
|
* handle.filter = (filter) => {
|
||||||
|
* filter.fields()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @class SqliteFileHandle
|
||||||
|
* @extends {BaseFileHandle}
|
||||||
|
*/
|
||||||
|
class SqliteFileHandle extends VFS.BaseFileHandle {
|
||||||
|
/**
|
||||||
|
* Set a file path to the current file handle
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {string} p
|
||||||
|
* @returns {void}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
setPath(p) {
|
||||||
|
let arr = p.split("@");
|
||||||
|
super.setPath(arr[0]);
|
||||||
|
if (arr.length > 3) {
|
||||||
|
throw new Error(__("Invalid file path").__());
|
||||||
|
}
|
||||||
|
this.path = p;
|
||||||
|
this._table_name = arr[1];
|
||||||
|
this._id = arr[2] ? parseInt(arr[2]) : undefined;
|
||||||
|
this._handle = new SQLiteDBCore(`home://${this.genealogy.join("/")}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Read database file meta-data
|
||||||
|
*
|
||||||
|
* Return file info on the target database file, if the table_name is specified
|
||||||
|
* return also the table scheme
|
||||||
|
*
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
meta() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
await this._handle.init();
|
||||||
|
const d = { result: this._handle.fileinfo(), error: false };
|
||||||
|
if (this._table_name) {
|
||||||
|
const data = await this._handle.get_table_scheme(this._table_name);
|
||||||
|
if (data.length == 0) {
|
||||||
|
d.result.scheme = undefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d.result.scheme = {};
|
||||||
|
for (let v of data) {
|
||||||
|
d.result.scheme[v.name] = v.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolve(d);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Query the database based on the provided info
|
||||||
|
*
|
||||||
|
* If no table is provided, return list of tables in the
|
||||||
|
* data base.
|
||||||
|
* If the current table is specified:
|
||||||
|
* - if the record id is specfied return the record
|
||||||
|
* - otherwise, return the records in the table using the specified filter
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {any} t filter type
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
_rd(user_data) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (this._table_name && !this.info.scheme) {
|
||||||
|
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
|
||||||
|
}
|
||||||
|
if (!this._table_name) {
|
||||||
|
// return list of tables in form of data base file handles in ready mode
|
||||||
|
let list = await this._handle.list_tables();
|
||||||
|
const map = {};
|
||||||
|
for (let v of list) {
|
||||||
|
map[v.name] = `${this.path}@${v.name}`.asFileHandle();
|
||||||
|
}
|
||||||
|
this.cache = map;
|
||||||
|
resolve(map);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// return all the data in the table set by the filter
|
||||||
|
// if this is a table, return the filtered records
|
||||||
|
// otherwise, it is a record, fetch only that record
|
||||||
|
let filter = user_data;
|
||||||
|
if (!filter || this._id) {
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
filter.table_name = this._table_name;
|
||||||
|
if (this._id) {
|
||||||
|
filter.where = { id: this._id };
|
||||||
|
}
|
||||||
|
let data = await this._handle.select(filter);
|
||||||
|
if (this._id) {
|
||||||
|
this.cache = data[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.cache = data;
|
||||||
|
}
|
||||||
|
resolve(this.cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Write commit file cache to the remote database
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {string} t is table name, used only when create table
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
_wr(t) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!this.cache) {
|
||||||
|
throw new Error(__("No data to submit to remote database, please check the `cache` field").__());
|
||||||
|
}
|
||||||
|
if (this._id && this._table_name) {
|
||||||
|
this.cache.id = this._id;
|
||||||
|
const ret = await this._handle.update(this._table_name, this.cache);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._table_name) {
|
||||||
|
const ret = await this._handle.insert(this._table_name, this.cache);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create a new table with the scheme provided in the cache
|
||||||
|
let r = await this._handle.create_table(t, this.cache);
|
||||||
|
resolve({ result: r, error: false });
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Delete data from remote database
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {any} user_data is table name, for delete table, otherwise, filter object for deleting records
|
||||||
|
* @returns {Promise<RequestResult>}
|
||||||
|
* @memberof SqliteFileHandle
|
||||||
|
*/
|
||||||
|
_rm(user_data) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (this._table_name && !this.info.scheme) {
|
||||||
|
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
|
||||||
|
}
|
||||||
|
if (!this._table_name) {
|
||||||
|
let table_name = user_data;
|
||||||
|
if (!table_name) {
|
||||||
|
throw new Error(__("No table specified for dropping").__());
|
||||||
|
}
|
||||||
|
let ret = await this._handle.drop_table(table_name);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
// delete the table
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let filter = user_data;
|
||||||
|
// delete the records in the table using the filter
|
||||||
|
if (!filter || this._id) {
|
||||||
|
filter = {};
|
||||||
|
}
|
||||||
|
filter.table_name = this._table_name;
|
||||||
|
if (this._id) {
|
||||||
|
filter.where = { id: this._id };
|
||||||
|
}
|
||||||
|
let ret = await this._handle.delete_records(filter);
|
||||||
|
resolve({ result: ret, error: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return reject(__e(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VFS.register("^sqlite$", SqliteFileHandle);
|
||||||
|
})(VFS = API.VFS || (API.VFS = {}));
|
||||||
})(API = OS.API || (OS.API = {}));
|
})(API = OS.API || (OS.API = {}));
|
||||||
})(OS || (OS = {}));
|
})(OS || (OS = {}));
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"pkgname": "SQLiteDB",
|
"pkgname": "SQLiteDB",
|
||||||
"app":"SQLiteDB",
|
"app":"SQLiteDB",
|
||||||
"name":"SQLiteDB",
|
"name":"SQLite3 VFS API",
|
||||||
"description":"SQLiteDB",
|
"description":"API for manipulate SQLite database as VFS handle",
|
||||||
"info":{
|
"info":{
|
||||||
"author": "",
|
"author": "",
|
||||||
"email": ""
|
"email": ""
|
||||||
},
|
},
|
||||||
"version":"0.0.1-a",
|
"version":"0.1.0-a",
|
||||||
"category":"Other",
|
"category":"Other",
|
||||||
"iconclass":"fa fa-adn",
|
"iconclass":"fa fa-adn",
|
||||||
"mimes":["none"],
|
"mimes":[".*"],
|
||||||
"dependencies":[],
|
"dependencies":[],
|
||||||
"locale": {}
|
"locale": {}
|
||||||
}
|
}
|
@ -1,3 +1,19 @@
|
|||||||
<afx-app-window apptitle="SQLiteDB" width="500" height="400" data-id="SQLiteDB">
|
<afx-app-window apptitle="SQLite browser" width="600" height="500" data-id="SQLiteDB">
|
||||||
<afx-hbox ></afx-hbox>
|
<afx-vbox>
|
||||||
|
<afx-hbox data-height="35">
|
||||||
|
<afx-button data-id = "bt-add-table" iconclass = "bi bi-plus-lg" data-width="content"></afx-button>
|
||||||
|
<afx-list-view dropdown = "true" data-id="tbl-list"></afx-list-view>
|
||||||
|
</afx-hbox>
|
||||||
|
|
||||||
|
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40">
|
||||||
|
<afx-hbox tabname="__(Struture)" iconclass = "bi bi-layout-wtf" >
|
||||||
|
<afx-grid-view></afx-grid-view>
|
||||||
|
</afx-hbox>
|
||||||
|
|
||||||
|
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
|
||||||
|
<afx-grid-view></afx-grid-view>
|
||||||
|
</afx-hbox>
|
||||||
|
</afx-tab-container>
|
||||||
|
|
||||||
|
</afx-vbox>
|
||||||
</afx-app-window>
|
</afx-app-window>
|
228
SQLiteDB/main.ts
228
SQLiteDB/main.ts
@ -1,34 +1,230 @@
|
|||||||
namespace OS {
|
namespace OS {
|
||||||
export namespace application {
|
export namespace application {
|
||||||
interface SQLiteDBBaseConstructor{
|
|
||||||
new(pqth: API.VFS.BaseFileHandle| string): SQLiteDBBase;
|
|
||||||
};
|
|
||||||
interface SQLiteDBBase{
|
|
||||||
list_tables(): Promise<Array<string>>,
|
|
||||||
last_insert_id(): Promise<number>,
|
|
||||||
query(sql): Promise<any>
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @class SQLiteDB
|
* @class SQLiteDB
|
||||||
* @extends {BaseApplication}
|
* @extends {BaseApplication}
|
||||||
*/
|
*/
|
||||||
export class SQLiteDB extends BaseApplication {
|
export class SQLiteDB extends BaseApplication {
|
||||||
|
|
||||||
|
private filehandle: API.VFS.BaseFileHandle;
|
||||||
|
private tbl_list: GUI.tag.ListViewTag;
|
||||||
|
|
||||||
constructor(args: AppArgumentsType[]) {
|
constructor(args: AppArgumentsType[]) {
|
||||||
super("SQLiteDB", args);
|
super("SQLiteDB", args);
|
||||||
}
|
}
|
||||||
main(): void {
|
|
||||||
// YOUR CODE HERE
|
menu() {
|
||||||
let handle = new ((OS.API as any).SQLiteDBBase as SQLiteDBBaseConstructor)("home://tmp/test.db");
|
return [
|
||||||
handle.list_tables().then((list) => {
|
|
||||||
console.log(list);
|
|
||||||
if(list.indexOf("contacts") < 0)
|
|
||||||
{
|
{
|
||||||
handle.query("CREATE TABLE contacts (id INTEGER PRIMARY KEY,first_name TEXT NOT NULL,last_name TEXT NOT NULL,email TEXT NOT NULL UNIQUE,phone TEXT NOT NULL UNIQUE)");
|
text: "__(File)",
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
text: "__(New)",
|
||||||
|
dataid: "new",
|
||||||
|
shortcut: 'A-N'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "__(Open)",
|
||||||
|
dataid: "open",
|
||||||
|
shortcut: 'A-O'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onchildselect: (e) => {
|
||||||
|
switch (e.data.item.data.dataid) {
|
||||||
|
case "new":
|
||||||
|
return this.newFile();
|
||||||
|
case "open":
|
||||||
|
return this.openFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
private list_tables()
|
||||||
|
{
|
||||||
|
this.filehandle.read()
|
||||||
|
.then((data) => {
|
||||||
|
const list = [];
|
||||||
|
for(let k in data)
|
||||||
|
{
|
||||||
|
list.push({
|
||||||
|
text: k,
|
||||||
|
name: k,
|
||||||
|
handle: data[k]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.tbl_list.data = list;
|
||||||
|
if(list.length > 0)
|
||||||
|
{
|
||||||
|
this.tbl_list.selected = 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
private openFile() {
|
||||||
|
return this.openDialog("FileDialog", {
|
||||||
|
title: __("Open file"),
|
||||||
|
mimes: this.meta().mimes
|
||||||
|
}).then(async (d) => {
|
||||||
|
this.filehandle = `sqlite://${d.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle();
|
||||||
|
await this.filehandle.onready();
|
||||||
|
this.list_tables();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.error(__("Unable to open database file: {0}", e.toString()), e);
|
||||||
|
});;
|
||||||
|
}
|
||||||
|
|
||||||
|
private newFile() {
|
||||||
|
return this.openDialog("FileDialog", {
|
||||||
|
title: __("Save as"),
|
||||||
|
file: "Untitled.db"
|
||||||
|
}).then(async (f) => {
|
||||||
|
var d;
|
||||||
|
d = f.file.path.asFileHandle();
|
||||||
|
if (f.file.type === "file") {
|
||||||
|
d = d.parent();
|
||||||
|
}
|
||||||
|
const target = `${d.path}/${f.name}`.asFileHandle();
|
||||||
|
this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle();
|
||||||
|
await this.filehandle.onready();
|
||||||
|
this.list_tables();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.error(__("Unable to init database file: {0}", e.toString()), e);
|
||||||
});
|
});
|
||||||
handle.last_insert_id().then(o => console.log(o));
|
}
|
||||||
|
|
||||||
|
main(): void {
|
||||||
|
this.filehandle = undefined;
|
||||||
|
this.tbl_list = this.find("tbl-list") as GUI.tag.ListViewTag;
|
||||||
|
(this.find("bt-add-table") as GUI.tag.ButtonTag).onbtclick = (e) => {
|
||||||
|
this.openDialog(new NewTableDialog(), {
|
||||||
|
title: __("Create new table")
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NewTableDialog extends GUI.BasicDialog {
|
||||||
|
/**
|
||||||
|
* Reference to the form container
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {HTMLDivElement}
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
private container: HTMLDivElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of NewTableDialog.
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super("NewTableDialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
main(): void {
|
||||||
|
super.main();
|
||||||
|
this.container = this.find("container") as HTMLDivElement;
|
||||||
|
(this.find("btnCancel") as GUI.tag.ButtonTag).onbtclick = (e) => this.quit();
|
||||||
|
(this.find("btnAdd") as GUI.tag.ButtonTag).onbtclick = (e) => this.addField("", "", true);
|
||||||
|
$(this.find("wrapper"))
|
||||||
|
$(this.container)
|
||||||
|
.css("overflow-y", "auto");
|
||||||
|
(this.find("btnOk") as GUI.tag.ButtonTag).onbtclick = (e) => {
|
||||||
|
const inputs = $("input", this.scheme) as JQuery<HTMLInputElement>;
|
||||||
|
let cdata: GenericObject<string> = {};
|
||||||
|
for (let i = 0; i < inputs.length; i += 2) {
|
||||||
|
const key = inputs[i].value.trim();
|
||||||
|
if (key === "") {
|
||||||
|
return this.notify(__("Key cannot be empty"));
|
||||||
|
}
|
||||||
|
if (cdata[key]) {
|
||||||
|
return this.notify(__("Duplicate key: {0}", key));
|
||||||
|
}
|
||||||
|
cdata[key] = inputs[i + 1].value.trim();
|
||||||
|
}
|
||||||
|
if (this.handle)
|
||||||
|
this.handle(cdata);
|
||||||
|
this.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new input key-value field to the dialog
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @memberof NewTableDialog
|
||||||
|
*/
|
||||||
|
private addField(key: string, value: string, removable: boolean): void {
|
||||||
|
const div = $("<div>")
|
||||||
|
.css("width", "100%")
|
||||||
|
.css("display", "flex")
|
||||||
|
.css("flex-direction", "row")
|
||||||
|
.appendTo(this.container);
|
||||||
|
$("<input>")
|
||||||
|
.attr("type", "text")
|
||||||
|
.css("width", "50%")
|
||||||
|
.css("height", "25px")
|
||||||
|
.val(key)
|
||||||
|
.appendTo(div);
|
||||||
|
$("<afx-list-view>")
|
||||||
|
.css("width", "50%")
|
||||||
|
.css("height", "25px")
|
||||||
|
.appendTo(div);
|
||||||
|
if (removable) {
|
||||||
|
const btn = $("<afx-button>");
|
||||||
|
btn[0].uify(undefined);
|
||||||
|
$("button", btn)
|
||||||
|
.css("width", "25px")
|
||||||
|
.css("height", "25px");
|
||||||
|
(btn[0] as GUI.tag.ButtonTag).iconclass = "fa fa-minus";
|
||||||
|
btn
|
||||||
|
.on("click", () => {
|
||||||
|
div.remove();
|
||||||
|
})
|
||||||
|
.appendTo(div);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("<div>")
|
||||||
|
.css("width", "25px")
|
||||||
|
.appendTo(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheme definition
|
||||||
|
*/
|
||||||
|
NewTableDialog.scheme = `\
|
||||||
|
<afx-app-window width='350' height='300'>
|
||||||
|
<afx-hbox>
|
||||||
|
<div data-width="10" ></div>
|
||||||
|
<afx-vbox>
|
||||||
|
<div data-height="5" ></div>
|
||||||
|
<afx-label text="__(Table layout)" data-height="30"></afx-label>
|
||||||
|
<div data-id="container"></div>
|
||||||
|
<afx-hbox data-height="30">
|
||||||
|
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "30" ></afx-button>
|
||||||
|
<div ></div>
|
||||||
|
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
|
||||||
|
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
|
||||||
|
</afx-hbox>
|
||||||
|
<div data-height="5" ></div>
|
||||||
|
</afx-vbox>
|
||||||
|
<div data-width="10" ></div>
|
||||||
|
</afx-hbox>
|
||||||
|
</afx-app-window>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"pkgname": "SQLiteDB",
|
"pkgname": "SQLiteDB",
|
||||||
"app":"SQLiteDB",
|
"app":"SQLiteDB",
|
||||||
"name":"SQLiteDB",
|
"name":"SQLite3 VFS API",
|
||||||
"description":"SQLiteDB",
|
"description":"API for manipulate SQLite database as VFS handle",
|
||||||
"info":{
|
"info":{
|
||||||
"author": "",
|
"author": "",
|
||||||
"email": ""
|
"email": ""
|
||||||
},
|
},
|
||||||
"version":"0.0.1-a",
|
"version":"0.1.0-a",
|
||||||
"category":"Other",
|
"category":"Other",
|
||||||
"iconclass":"fa fa-adn",
|
"iconclass":"fa fa-adn",
|
||||||
"mimes":["none"],
|
"mimes":[".*"],
|
||||||
"dependencies":[],
|
"dependencies":[],
|
||||||
"locale": {}
|
"locale": {}
|
||||||
}
|
}
|
@ -1,3 +1,19 @@
|
|||||||
<afx-app-window apptitle="SQLiteDB" width="500" height="400" data-id="SQLiteDB">
|
<afx-app-window apptitle="SQLite browser" width="600" height="500" data-id="SQLiteDB">
|
||||||
<afx-hbox ></afx-hbox>
|
<afx-vbox>
|
||||||
|
<afx-hbox data-height="35">
|
||||||
|
<afx-button data-id = "bt-add-table" iconclass = "bi bi-plus-lg" data-width="content"></afx-button>
|
||||||
|
<afx-list-view dropdown = "true" data-id="tbl-list"></afx-list-view>
|
||||||
|
</afx-hbox>
|
||||||
|
|
||||||
|
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40">
|
||||||
|
<afx-hbox tabname="__(Struture)" iconclass = "bi bi-layout-wtf" >
|
||||||
|
<afx-grid-view></afx-grid-view>
|
||||||
|
</afx-hbox>
|
||||||
|
|
||||||
|
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
|
||||||
|
<afx-grid-view></afx-grid-view>
|
||||||
|
</afx-hbox>
|
||||||
|
</afx-tab-container>
|
||||||
|
|
||||||
|
</afx-vbox>
|
||||||
</afx-app-window>
|
</afx-app-window>
|
Loading…
Reference in New Issue
Block a user