mirror of
https://github.com/antos-rde/antosdk-apps.git
synced 2024-12-25 03:38:21 +01:00
Update AntOSDK and SQLiteDB
- AntOSDK: add some public API on grid view - SQLiteDB: add main application as a basic SQLiteBrowser
This commit is contained in:
parent
cd5b0f66cc
commit
cf21ef60e0
@ -1,259 +1,6 @@
|
|||||||
namespace OS {
|
namespace OS {
|
||||||
export namespace API
|
export namespace API
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* 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 {
|
class SQLiteDBCore {
|
||||||
static REGISTY: GenericObject<VFS.BaseFileHandle>;
|
static REGISTY: GenericObject<VFS.BaseFileHandle>;
|
||||||
|
|
||||||
@ -394,7 +141,7 @@ namespace OS {
|
|||||||
return this.request(rq);
|
return this.request(rq);
|
||||||
}
|
}
|
||||||
|
|
||||||
create_table(table, scheme): Promise<any>
|
create_table(table: string, scheme: GenericObject<string>): Promise<any>
|
||||||
{
|
{
|
||||||
let rq = {
|
let rq = {
|
||||||
action: 'create_table',
|
action: 'create_table',
|
||||||
@ -464,10 +211,9 @@ namespace OS {
|
|||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
* * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database
|
* * `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
|
* - meta operation will return fileinfo with table scheme information
|
||||||
* - read operation will read all records by filter defined by the filter operation
|
* - read operation will read all records by filter defined by the filter as parameters
|
||||||
* - write operations will insert a new record
|
* - write operations will insert a new record
|
||||||
* - rm operation will delete records by filter defined by the filter operation
|
* - rm operation will delete records by filter as parameters
|
||||||
* - filter operation sets the filter for the table
|
|
||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
* - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id`
|
* - `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
|
* - read operation will read the current record
|
||||||
@ -475,12 +221,57 @@ namespace OS {
|
|||||||
* - rm operation will delete current record
|
* - rm operation will delete current record
|
||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
*
|
*
|
||||||
* Some example of filters:
|
* Example of filter:
|
||||||
* ```ts
|
* ```ts
|
||||||
* handle.filter = (filter) => {
|
* {
|
||||||
* filter.fields()
|
* table_name:'contacts';
|
||||||
* }
|
* 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',
|
||||||
|
* uid: "User.id"
|
||||||
|
* }
|
||||||
|
*}
|
||||||
|
* ```
|
||||||
|
* This will generate the followings expressions:
|
||||||
|
* - `( self.name as n,self.id,self.email )` for fields
|
||||||
|
* - condition:
|
||||||
|
* ```
|
||||||
|
* (
|
||||||
|
* ( contacts.id >= 10 ) AND
|
||||||
|
* ( contacts.user = 'dany''' ) AND
|
||||||
|
* (
|
||||||
|
* ( user.email = 'test@mail.com' ) OR
|
||||||
|
* ( contacts.age <= 30 ) OR
|
||||||
|
* (
|
||||||
|
* ( user.birth != 1986 ) AND
|
||||||
|
* ( contacts.age NOT BETWEEN 20 AND 30 ) AND
|
||||||
|
* ( contacts.name NOT LIKE '%LE' )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* - order: `user.name ASC,contacts.id DESC`
|
||||||
|
* - joining:
|
||||||
|
* ```
|
||||||
|
* INNER JOIN Category ON contacts.cid = Category.id
|
||||||
|
* INNER JOIN Country ON contacts.did = Country.id
|
||||||
|
* INNER JOIN Country ON contacts.did = Country.id
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @class SqliteFileHandle
|
* @class SqliteFileHandle
|
||||||
* @extends {BaseFileHandle}
|
* @extends {BaseFileHandle}
|
||||||
|
@ -3,210 +3,6 @@ var OS;
|
|||||||
(function (OS) {
|
(function (OS) {
|
||||||
let API;
|
let API;
|
||||||
(function (API) {
|
(function (API) {
|
||||||
/**
|
|
||||||
* 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 {
|
class SQLiteDBCore {
|
||||||
constructor(path) {
|
constructor(path) {
|
||||||
if (!SQLiteDBCore.REGISTY) {
|
if (!SQLiteDBCore.REGISTY) {
|
||||||
@ -376,10 +172,9 @@ var OS;
|
|||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
* * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database
|
* * `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
|
* - meta operation will return fileinfo with table scheme information
|
||||||
* - read operation will read all records by filter defined by the filter operation
|
* - read operation will read all records by filter defined by the filter as parameters
|
||||||
* - write operations will insert a new record
|
* - write operations will insert a new record
|
||||||
* - rm operation will delete records by filter defined by the filter operation
|
* - rm operation will delete records by filter as parameters
|
||||||
* - filter operation sets the filter for the table
|
|
||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
* - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id`
|
* - `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
|
* - read operation will read the current record
|
||||||
@ -387,12 +182,57 @@ var OS;
|
|||||||
* - rm operation will delete current record
|
* - rm operation will delete current record
|
||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
*
|
*
|
||||||
* Some example of filters:
|
* Example of filter:
|
||||||
* ```ts
|
* ```ts
|
||||||
* handle.filter = (filter) => {
|
* {
|
||||||
* filter.fields()
|
* table_name:'contacts';
|
||||||
* }
|
* 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',
|
||||||
|
* uid: "User.id"
|
||||||
|
* }
|
||||||
|
*}
|
||||||
|
* ```
|
||||||
|
* This will generate the followings expressions:
|
||||||
|
* - `( self.name as n,self.id,self.email )` for fields
|
||||||
|
* - condition:
|
||||||
|
* ```
|
||||||
|
* (
|
||||||
|
* ( contacts.id >= 10 ) AND
|
||||||
|
* ( contacts.user = 'dany''' ) AND
|
||||||
|
* (
|
||||||
|
* ( user.email = 'test@mail.com' ) OR
|
||||||
|
* ( contacts.age <= 30 ) OR
|
||||||
|
* (
|
||||||
|
* ( user.birth != 1986 ) AND
|
||||||
|
* ( contacts.age NOT BETWEEN 20 AND 30 ) AND
|
||||||
|
* ( contacts.name NOT LIKE '%LE' )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* - order: `user.name ASC,contacts.id DESC`
|
||||||
|
* - joining:
|
||||||
|
* ```
|
||||||
|
* INNER JOIN Category ON contacts.cid = Category.id
|
||||||
|
* INNER JOIN Country ON contacts.did = Country.id
|
||||||
|
* INNER JOIN Country ON contacts.did = Country.id
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @class SqliteFileHandle
|
* @class SqliteFileHandle
|
||||||
* @extends {BaseFileHandle}
|
* @extends {BaseFileHandle}
|
||||||
|
@ -57,47 +57,155 @@ var OS;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
openFile() {
|
async openFile() {
|
||||||
return this.openDialog("FileDialog", {
|
try {
|
||||||
title: __("Open file"),
|
const d_1 = await this.openDialog("FileDialog", {
|
||||||
mimes: this.meta().mimes
|
title: __("Open file"),
|
||||||
}).then(async (d) => {
|
mimes: this.meta().mimes
|
||||||
this.filehandle = `sqlite://${d.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle();
|
});
|
||||||
|
this.filehandle = `sqlite://${d_1.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle();
|
||||||
await this.filehandle.onready();
|
await this.filehandle.onready();
|
||||||
this.list_tables();
|
this.list_tables();
|
||||||
})
|
}
|
||||||
.catch((e) => {
|
catch (e) {
|
||||||
this.error(__("Unable to open database file: {0}", e.toString()), e);
|
this.error(__("Unable to open database file: {0}", e.toString()), e);
|
||||||
});
|
}
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
newFile() {
|
async newFile() {
|
||||||
return this.openDialog("FileDialog", {
|
try {
|
||||||
title: __("Save as"),
|
const f = await this.openDialog("FileDialog", {
|
||||||
file: "Untitled.db"
|
title: __("Save as"),
|
||||||
}).then(async (f) => {
|
file: "Untitled.db"
|
||||||
var d;
|
});
|
||||||
d = f.file.path.asFileHandle();
|
var d_1 = f.file.path.asFileHandle();
|
||||||
if (f.file.type === "file") {
|
if (f.file.type === "file") {
|
||||||
d = d.parent();
|
d_1 = d_1.parent();
|
||||||
}
|
}
|
||||||
const target = `${d.path}/${f.name}`.asFileHandle();
|
const target = `${d_1.path}/${f.name}`.asFileHandle();
|
||||||
this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle();
|
this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle();
|
||||||
await this.filehandle.onready();
|
await this.filehandle.onready();
|
||||||
this.list_tables();
|
this.list_tables();
|
||||||
})
|
}
|
||||||
.catch((e) => {
|
catch (e) {
|
||||||
this.error(__("Unable to init database file: {0}", e.toString()), e);
|
this.error(__("Unable to init database file: {0}", e.toString()), e);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
main() {
|
main() {
|
||||||
this.filehandle = undefined;
|
this.filehandle = undefined;
|
||||||
this.tbl_list = this.find("tbl-list");
|
this.tbl_list = this.find("tbl-list");
|
||||||
|
this.grid_table = this.find("tb-browser");
|
||||||
|
this.grid_scheme = this.find("sch-browser");
|
||||||
|
this.grid_table.resizable = true;
|
||||||
|
this.grid_scheme.resizable = true;
|
||||||
|
this.grid_scheme.header = [
|
||||||
|
{ text: __("Field name") },
|
||||||
|
{ text: __("Field type") },
|
||||||
|
];
|
||||||
|
this.btn_loadmore = this.find("bt-load-next");
|
||||||
|
this.container = this.find("container");
|
||||||
|
this.bindKey("ALT-N", () => {
|
||||||
|
return this.newFile();
|
||||||
|
});
|
||||||
|
this.bindKey("ALT-O", () => {
|
||||||
|
return this.openFile();
|
||||||
|
});
|
||||||
|
this.container.ontabselect = (e) => {
|
||||||
|
if (this.container.selectedIndex == 0) {
|
||||||
|
if (!this.tbl_list.selectedItem)
|
||||||
|
return;
|
||||||
|
const scheme = this.tbl_list.selectedItem.data.handle.info.scheme;
|
||||||
|
if (!scheme)
|
||||||
|
return;
|
||||||
|
const data = [];
|
||||||
|
for (let k in scheme) {
|
||||||
|
data.push([
|
||||||
|
{ text: k },
|
||||||
|
{ text: scheme[k] }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
this.grid_scheme.rows = data;
|
||||||
|
}
|
||||||
|
};
|
||||||
this.find("bt-add-table").onbtclick = (e) => {
|
this.find("bt-add-table").onbtclick = (e) => {
|
||||||
|
if (!this.filehandle) {
|
||||||
|
return this.notify(__("Please open a database file"));
|
||||||
|
}
|
||||||
this.openDialog(new NewTableDialog(), {
|
this.openDialog(new NewTableDialog(), {
|
||||||
title: __("Create new table")
|
title: __("Create new table")
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
this.btn_loadmore.onbtclick = async (e) => {
|
||||||
|
try {
|
||||||
|
await this.load_table();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.error(__("Error reading table: {0}", e.toString()), e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.tbl_list.onlistselect = async (_) => {
|
||||||
|
try {
|
||||||
|
if (!this.tbl_list.selectedItem)
|
||||||
|
return;
|
||||||
|
const handle = this.tbl_list.selectedItem.data.handle;
|
||||||
|
await handle.onready();
|
||||||
|
this.last_max_id = 0;
|
||||||
|
this.grid_table.rows = [];
|
||||||
|
const headers = Object.getOwnPropertyNames(handle.info.scheme).map((e) => {
|
||||||
|
return { text: e };
|
||||||
|
});
|
||||||
|
this.grid_table.header = headers;
|
||||||
|
const records = await handle.read({ fields: ["COUNT(*)"] });
|
||||||
|
this.n_records = records[0]["(COUNT(*))"];
|
||||||
|
await this.load_table();
|
||||||
|
this.container.selectedIndex = 1;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.error(__("Error reading table: {0}", e.toString()), e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.openFile();
|
||||||
|
}
|
||||||
|
async load_table() {
|
||||||
|
if (this.grid_table.rows.length >= this.n_records) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.tbl_list.selectedItem)
|
||||||
|
return;
|
||||||
|
const handle = this.tbl_list.selectedItem.data.handle;
|
||||||
|
await handle.onready();
|
||||||
|
const headers = Object.getOwnPropertyNames(handle.info.scheme).map((e) => {
|
||||||
|
return { text: e };
|
||||||
|
});
|
||||||
|
// read all records
|
||||||
|
const records = await handle.read({
|
||||||
|
where: { id$gt: this.last_max_id },
|
||||||
|
limit: 10
|
||||||
|
});
|
||||||
|
if (records && records.length > 0) {
|
||||||
|
for (let e of records) {
|
||||||
|
const row = [];
|
||||||
|
if (e.id && e.id > this.last_max_id) {
|
||||||
|
this.last_max_id = e.id;
|
||||||
|
}
|
||||||
|
for (let v in headers) {
|
||||||
|
let text = e[headers[v].text];
|
||||||
|
if (text.length > 100) {
|
||||||
|
text = text.substring(0, 100);
|
||||||
|
}
|
||||||
|
row.push({
|
||||||
|
text: text,
|
||||||
|
record: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.grid_table.push(row, false);
|
||||||
|
}
|
||||||
|
this.grid_table.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
application.SQLiteDB = SQLiteDB;
|
application.SQLiteDB = SQLiteDB;
|
||||||
@ -109,9 +217,6 @@ var OS;
|
|||||||
constructor() {
|
constructor() {
|
||||||
super("NewTableDialog");
|
super("NewTableDialog");
|
||||||
}
|
}
|
||||||
init() {
|
|
||||||
console.log(this.constructor.scheme);
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Main entry point
|
* Main entry point
|
||||||
*
|
*
|
||||||
@ -121,22 +226,31 @@ var OS;
|
|||||||
super.main();
|
super.main();
|
||||||
this.container = this.find("container");
|
this.container = this.find("container");
|
||||||
this.find("btnCancel").onbtclick = (e) => this.quit();
|
this.find("btnCancel").onbtclick = (e) => this.quit();
|
||||||
this.find("btnAdd").onbtclick = (e) => this.addField("", "", true);
|
this.find("btnAdd").onbtclick = (e) => this.addField();
|
||||||
$(this.find("wrapper"));
|
$(this.find("wrapper"));
|
||||||
$(this.container)
|
$(this.container)
|
||||||
.css("overflow-y", "auto");
|
.css("overflow-y", "auto");
|
||||||
|
this.addField();
|
||||||
this.find("btnOk").onbtclick = (e) => {
|
this.find("btnOk").onbtclick = (e) => {
|
||||||
const inputs = $("input", this.scheme);
|
const input = this.find("txt-tblname");
|
||||||
|
if (!input.value || input.value == "") {
|
||||||
|
return this.notify(__("Please enter table name"));
|
||||||
|
}
|
||||||
|
const inputs = $("input", this.container);
|
||||||
|
const lists = $("afx-list-view", this.container);
|
||||||
|
if (inputs.length == 0) {
|
||||||
|
return this.notify(__("Please define table fields"));
|
||||||
|
}
|
||||||
let cdata = {};
|
let cdata = {};
|
||||||
for (let i = 0; i < inputs.length; i += 2) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
const key = inputs[i].value.trim();
|
const key = inputs[i].value.trim();
|
||||||
if (key === "") {
|
if (key === "") {
|
||||||
return this.notify(__("Key cannot be empty"));
|
return this.notify(__("Field name cannot be empty"));
|
||||||
}
|
}
|
||||||
if (cdata[key]) {
|
if (cdata[key]) {
|
||||||
return this.notify(__("Duplicate key: {0}", key));
|
return this.notify(__("Duplicate field: {0}", key));
|
||||||
}
|
}
|
||||||
cdata[key] = inputs[i + 1].value.trim();
|
cdata[key] = lists[i].selectedItem.data.text;
|
||||||
}
|
}
|
||||||
if (this.handle)
|
if (this.handle)
|
||||||
this.handle(cdata);
|
this.handle(cdata);
|
||||||
@ -149,64 +263,55 @@ var OS;
|
|||||||
* @private
|
* @private
|
||||||
* @memberof NewTableDialog
|
* @memberof NewTableDialog
|
||||||
*/
|
*/
|
||||||
addField(key, value, removable) {
|
addField() {
|
||||||
const div = $("<div>")
|
const div = $("<div>")
|
||||||
.css("width", "100%")
|
|
||||||
.css("display", "flex")
|
.css("display", "flex")
|
||||||
.css("flex-direction", "row")
|
.css("flex-direction", "row")
|
||||||
.appendTo(this.container);
|
.appendTo(this.container);
|
||||||
$("<input>")
|
$("<input>")
|
||||||
.attr("type", "text")
|
.attr("type", "text")
|
||||||
.css("width", "50%")
|
.css("flex", "1")
|
||||||
.css("height", "25px")
|
|
||||||
.val(key)
|
|
||||||
.appendTo(div);
|
.appendTo(div);
|
||||||
$("<afx-list-view>")
|
let list = $("<afx-list-view>")
|
||||||
.css("width", "50%")
|
.css("flex", "1")
|
||||||
.css("height", "25px")
|
.appendTo(div)[0];
|
||||||
|
list.uify(this.observable);
|
||||||
|
list.dropdown = true;
|
||||||
|
list.data = [
|
||||||
|
{ text: "TEXT" },
|
||||||
|
{ text: "INTEGER" },
|
||||||
|
{ text: "REAL" },
|
||||||
|
{ text: "NUMERIC" },
|
||||||
|
];
|
||||||
|
list.selected = 0;
|
||||||
|
const btn = $("<afx-button>");
|
||||||
|
btn[0].uify(undefined);
|
||||||
|
btn[0].iconclass = "fa fa-minus";
|
||||||
|
btn
|
||||||
|
.on("click", () => {
|
||||||
|
div.remove();
|
||||||
|
})
|
||||||
.appendTo(div);
|
.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
|
* Scheme definition
|
||||||
*/
|
*/
|
||||||
NewTableDialog.scheme = `\
|
NewTableDialog.scheme = `\
|
||||||
<afx-app-window width='350' height='300'>
|
<afx-app-window width='400' height='350'>
|
||||||
<afx-hbox>
|
<afx-vbox padding = "10">
|
||||||
<div data-width="10" ></div>
|
<afx-input label="__(Table name)" data-id="txt-tblname" data-height="content"></afx-input>
|
||||||
<afx-vbox>
|
<afx-label text="__(Fields in table:)" data-height="30"></afx-label>
|
||||||
<div data-height="5" ></div>
|
<div data-id="container" style="position:relative;"></div>
|
||||||
<afx-label text="__(Table layout)" data-height="30"></afx-label>
|
<afx-hbox data-height="35">
|
||||||
<div data-id="container"></div>
|
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "35" ></afx-button>
|
||||||
<afx-hbox data-height="30">
|
<div style = "text-align: right;">
|
||||||
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "30" ></afx-button>
|
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
|
||||||
<div ></div>
|
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
|
||||||
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
|
</div>
|
||||||
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
|
</afx-hbox>
|
||||||
</afx-hbox>
|
</afx-vbox>
|
||||||
<div data-height="5" ></div>
|
</afx-app-window>`;
|
||||||
</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 = {}));
|
||||||
|
|
||||||
@ -215,210 +320,6 @@ var OS;
|
|||||||
(function (OS) {
|
(function (OS) {
|
||||||
let API;
|
let API;
|
||||||
(function (API) {
|
(function (API) {
|
||||||
/**
|
|
||||||
* 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 {
|
class SQLiteDBCore {
|
||||||
constructor(path) {
|
constructor(path) {
|
||||||
if (!SQLiteDBCore.REGISTY) {
|
if (!SQLiteDBCore.REGISTY) {
|
||||||
@ -588,10 +489,9 @@ var OS;
|
|||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
* * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database
|
* * `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
|
* - meta operation will return fileinfo with table scheme information
|
||||||
* - read operation will read all records by filter defined by the filter operation
|
* - read operation will read all records by filter defined by the filter as parameters
|
||||||
* - write operations will insert a new record
|
* - write operations will insert a new record
|
||||||
* - rm operation will delete records by filter defined by the filter operation
|
* - rm operation will delete records by filter as parameters
|
||||||
* - filter operation sets the filter for the table
|
|
||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
* - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id`
|
* - `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
|
* - read operation will read the current record
|
||||||
@ -599,12 +499,57 @@ var OS;
|
|||||||
* - rm operation will delete current record
|
* - rm operation will delete current record
|
||||||
* - other operations are not supported
|
* - other operations are not supported
|
||||||
*
|
*
|
||||||
* Some example of filters:
|
* Example of filter:
|
||||||
* ```ts
|
* ```ts
|
||||||
* handle.filter = (filter) => {
|
* {
|
||||||
* filter.fields()
|
* table_name:'contacts';
|
||||||
* }
|
* 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',
|
||||||
|
* uid: "User.id"
|
||||||
|
* }
|
||||||
|
*}
|
||||||
|
* ```
|
||||||
|
* This will generate the followings expressions:
|
||||||
|
* - `( self.name as n,self.id,self.email )` for fields
|
||||||
|
* - condition:
|
||||||
|
* ```
|
||||||
|
* (
|
||||||
|
* ( contacts.id >= 10 ) AND
|
||||||
|
* ( contacts.user = 'dany''' ) AND
|
||||||
|
* (
|
||||||
|
* ( user.email = 'test@mail.com' ) OR
|
||||||
|
* ( contacts.age <= 30 ) OR
|
||||||
|
* (
|
||||||
|
* ( user.birth != 1986 ) AND
|
||||||
|
* ( contacts.age NOT BETWEEN 20 AND 30 ) AND
|
||||||
|
* ( contacts.name NOT LIKE '%LE' )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* - order: `user.name ASC,contacts.id DESC`
|
||||||
|
* - joining:
|
||||||
|
* ```
|
||||||
|
* INNER JOIN Category ON contacts.cid = Category.id
|
||||||
|
* INNER JOIN Country ON contacts.did = Country.id
|
||||||
|
* INNER JOIN Country ON contacts.did = Country.id
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @class SqliteFileHandle
|
* @class SqliteFileHandle
|
||||||
* @extends {BaseFileHandle}
|
* @extends {BaseFileHandle}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<afx-app-window apptitle="SQLite browser" width="600" height="500" data-id="SQLiteDB">
|
<afx-app-window apptitle="SQLite browser" width="600" height="500" data-id="SQLiteDB">
|
||||||
<afx-vbox>
|
<afx-vbox padding="5">
|
||||||
<afx-hbox data-height="35">
|
<afx-hbox data-height="35">
|
||||||
<afx-button data-id = "bt-add-table" iconclass = "bi bi-plus-lg" data-width="content"></afx-button>
|
<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-list-view dropdown = "true" data-id="tbl-list"></afx-list-view>
|
||||||
@ -7,12 +7,22 @@
|
|||||||
|
|
||||||
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40">
|
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40">
|
||||||
<afx-hbox tabname="__(Struture)" iconclass = "bi bi-layout-wtf" >
|
<afx-hbox tabname="__(Struture)" iconclass = "bi bi-layout-wtf" >
|
||||||
<afx-grid-view></afx-grid-view>
|
<afx-grid-view data-id="sch-browser"></afx-grid-view>
|
||||||
</afx-hbox>
|
</afx-hbox>
|
||||||
|
|
||||||
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
|
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
|
||||||
<afx-grid-view></afx-grid-view>
|
<afx-vbox>
|
||||||
|
<afx-grid-view data-id="tb-browser"></afx-grid-view>
|
||||||
|
<afx-hbox data-height="35">
|
||||||
|
<afx-button iconclass_end="bi bi-chevron-double-right" data-id="bt-load-next" data-width="content"></afx-button>
|
||||||
|
<div></div>
|
||||||
|
<afx-button iconclass="bi bi-plus-lg" data-width="content"></afx-button>
|
||||||
|
<afx-button iconclass="bi bi-pencil-square" data-width="content"></afx-button>
|
||||||
|
<afx-button iconclass="bi bi-trash-fill" data-width="content"></afx-button>
|
||||||
|
</afx-hbox>
|
||||||
|
</afx-vbox>
|
||||||
</afx-hbox>
|
</afx-hbox>
|
||||||
|
|
||||||
</afx-tab-container>
|
</afx-tab-container>
|
||||||
|
|
||||||
</afx-vbox>
|
</afx-vbox>
|
||||||
|
292
SQLiteDB/main.ts
292
SQLiteDB/main.ts
@ -10,7 +10,12 @@ namespace OS {
|
|||||||
|
|
||||||
private filehandle: API.VFS.BaseFileHandle;
|
private filehandle: API.VFS.BaseFileHandle;
|
||||||
private tbl_list: GUI.tag.ListViewTag;
|
private tbl_list: GUI.tag.ListViewTag;
|
||||||
|
private grid_table: GUI.tag.GridViewTag;
|
||||||
|
private grid_scheme: GUI.tag.GridViewTag;
|
||||||
|
private container: GUI.tag.TabContainerTag;
|
||||||
|
private last_max_id: number;
|
||||||
|
private n_records: number;
|
||||||
|
private btn_loadmore: GUI.tag.ButtonTag;
|
||||||
constructor(args: AppArgumentsType[]) {
|
constructor(args: AppArgumentsType[]) {
|
||||||
super("SQLiteDB", args);
|
super("SQLiteDB", args);
|
||||||
}
|
}
|
||||||
@ -62,48 +67,177 @@ namespace OS {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
private openFile() {
|
private async openFile() {
|
||||||
return this.openDialog("FileDialog", {
|
try {
|
||||||
title: __("Open file"),
|
const d_1 = await this.openDialog("FileDialog",{
|
||||||
mimes: this.meta().mimes
|
title: __("Open file"),
|
||||||
}).then(async (d) => {
|
mimes: this.meta().mimes
|
||||||
this.filehandle = `sqlite://${d.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle();
|
});
|
||||||
|
this.filehandle=`sqlite://${d_1.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle();
|
||||||
await this.filehandle.onready();
|
await this.filehandle.onready();
|
||||||
this.list_tables();
|
this.list_tables();
|
||||||
})
|
}
|
||||||
.catch((e) => {
|
catch(e)
|
||||||
this.error(__("Unable to open database file: {0}", e.toString()), e);
|
{
|
||||||
});;
|
this.error(__("Unable to open database file: {0}",e.toString()),e);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private newFile() {
|
private async newFile() {
|
||||||
return this.openDialog("FileDialog", {
|
try {
|
||||||
title: __("Save as"),
|
const f = await this.openDialog("FileDialog",{
|
||||||
file: "Untitled.db"
|
title: __("Save as"),
|
||||||
}).then(async (f) => {
|
file: "Untitled.db"
|
||||||
var d;
|
});
|
||||||
d = f.file.path.asFileHandle();
|
var d_1 = f.file.path.asFileHandle();
|
||||||
if (f.file.type === "file") {
|
if(f.file.type==="file") {
|
||||||
d = d.parent();
|
d_1=d_1.parent();
|
||||||
}
|
}
|
||||||
const target = `${d.path}/${f.name}`.asFileHandle();
|
const target=`${d_1.path}/${f.name}`.asFileHandle();
|
||||||
this.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle();
|
this.filehandle=`sqlite://${target.genealogy.join("/")}`.asFileHandle();
|
||||||
await this.filehandle.onready();
|
await this.filehandle.onready();
|
||||||
this.list_tables();
|
this.list_tables();
|
||||||
})
|
}
|
||||||
.catch((e) => {
|
catch(e) {
|
||||||
this.error(__("Unable to init database file: {0}", e.toString()), e);
|
this.error(__("Unable to init database file: {0}",e.toString()),e);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main(): void {
|
main(): void {
|
||||||
this.filehandle = undefined;
|
this.filehandle = undefined;
|
||||||
this.tbl_list = this.find("tbl-list") as GUI.tag.ListViewTag;
|
this.tbl_list = this.find("tbl-list") as GUI.tag.ListViewTag;
|
||||||
|
this.grid_table = this.find("tb-browser") as GUI.tag.GridViewTag;
|
||||||
|
this.grid_scheme = this.find("sch-browser") as GUI.tag.GridViewTag;
|
||||||
|
this.grid_table.resizable = true;
|
||||||
|
this.grid_scheme.resizable = true;
|
||||||
|
this.grid_scheme.header = [
|
||||||
|
{ text: __("Field name") },
|
||||||
|
{ text: __("Field type") },
|
||||||
|
];
|
||||||
|
this.btn_loadmore = this.find("bt-load-next") as GUI.tag.ButtonTag;
|
||||||
|
this.container = this.find("container") as GUI.tag.TabContainerTag;
|
||||||
|
this.bindKey("ALT-N", () => {
|
||||||
|
return this.newFile();
|
||||||
|
});
|
||||||
|
this.bindKey("ALT-O", () => {
|
||||||
|
return this.openFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.container.ontabselect = (e) => {
|
||||||
|
if(this.container.selectedIndex == 0)
|
||||||
|
{
|
||||||
|
if(!this.tbl_list.selectedItem)
|
||||||
|
return;
|
||||||
|
const scheme = this.tbl_list.selectedItem.data.handle.info.scheme;
|
||||||
|
if(!scheme)
|
||||||
|
return;
|
||||||
|
const data = [];
|
||||||
|
for(let k in scheme)
|
||||||
|
{
|
||||||
|
data.push([
|
||||||
|
{ text: k},
|
||||||
|
{text: scheme[k]}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
this.grid_scheme.rows = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(this.find("bt-add-table") as GUI.tag.ButtonTag).onbtclick = (e) => {
|
(this.find("bt-add-table") as GUI.tag.ButtonTag).onbtclick = (e) => {
|
||||||
|
if(!this.filehandle)
|
||||||
|
{
|
||||||
|
return this.notify(__("Please open a database file"));
|
||||||
|
}
|
||||||
this.openDialog(new NewTableDialog(), {
|
this.openDialog(new NewTableDialog(), {
|
||||||
title: __("Create new table")
|
title: __("Create new table")
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.btn_loadmore.onbtclick = async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.load_table();
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
this.error(__("Error reading table: {0}", e.toString()),e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.tbl_list.onlistselect = async (_) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(!this.tbl_list.selectedItem)
|
||||||
|
return;
|
||||||
|
const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle;
|
||||||
|
await handle.onready();
|
||||||
|
this.last_max_id = 0;
|
||||||
|
this.grid_table.rows = [];
|
||||||
|
const headers =
|
||||||
|
Object.getOwnPropertyNames(handle.info.scheme).map((e)=>{
|
||||||
|
return {text: e}
|
||||||
|
});
|
||||||
|
this.grid_table.header = headers;
|
||||||
|
const records = await handle.read({fields:["COUNT(*)"]});
|
||||||
|
this.n_records = records[0]["(COUNT(*))"];
|
||||||
|
await this.load_table();
|
||||||
|
this.container.selectedIndex = 1;
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
this.error(__("Error reading table: {0}", e.toString()),e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.openFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async load_table()
|
||||||
|
{
|
||||||
|
if(this.grid_table.rows.length >= this.n_records)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!this.tbl_list.selectedItem)
|
||||||
|
return;
|
||||||
|
const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle;
|
||||||
|
await handle.onready();
|
||||||
|
const headers =
|
||||||
|
Object.getOwnPropertyNames(handle.info.scheme).map((e)=>{
|
||||||
|
return {text: e}
|
||||||
|
});
|
||||||
|
// read all records
|
||||||
|
const records = await handle.read({
|
||||||
|
where:{ id$gt: this.last_max_id },
|
||||||
|
limit: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
if(records && records.length > 0)
|
||||||
|
{
|
||||||
|
for(let e of records){
|
||||||
|
const row = [];
|
||||||
|
if(e.id && e.id > this.last_max_id)
|
||||||
|
{
|
||||||
|
this.last_max_id = e.id;
|
||||||
|
}
|
||||||
|
for(let v in headers)
|
||||||
|
{
|
||||||
|
let text:string = e[headers[v].text];
|
||||||
|
if(text.length > 100)
|
||||||
|
{
|
||||||
|
text = text.substring(0,100);
|
||||||
|
}
|
||||||
|
row.push({
|
||||||
|
text: text,
|
||||||
|
record: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.grid_table.push(row, false);
|
||||||
|
}
|
||||||
|
(this.grid_table as any).scroll_to_bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,22 +269,36 @@ namespace OS {
|
|||||||
super.main();
|
super.main();
|
||||||
this.container = this.find("container") as HTMLDivElement;
|
this.container = this.find("container") as HTMLDivElement;
|
||||||
(this.find("btnCancel") as GUI.tag.ButtonTag).onbtclick = (e) => this.quit();
|
(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("btnAdd") as GUI.tag.ButtonTag).onbtclick = (e) => this.addField();
|
||||||
$(this.find("wrapper"))
|
$(this.find("wrapper"))
|
||||||
$(this.container)
|
$(this.container)
|
||||||
.css("overflow-y", "auto");
|
.css("overflow-y", "auto");
|
||||||
|
this.addField();
|
||||||
(this.find("btnOk") as GUI.tag.ButtonTag).onbtclick = (e) => {
|
(this.find("btnOk") as GUI.tag.ButtonTag).onbtclick = (e) => {
|
||||||
const inputs = $("input", this.scheme) as JQuery<HTMLInputElement>;
|
const input = this.find("txt-tblname") as GUI.tag.InputTag;
|
||||||
|
|
||||||
|
if(!input.value || input.value == "")
|
||||||
|
{
|
||||||
|
return this.notify(__("Please enter table name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = $("input", this.container) as JQuery<HTMLInputElement>;
|
||||||
|
const lists = $("afx-list-view", this.container) as JQuery<GUI.tag.ListViewTag>;
|
||||||
|
if(inputs.length == 0)
|
||||||
|
{
|
||||||
|
return this.notify(__("Please define table fields"));
|
||||||
|
}
|
||||||
|
|
||||||
let cdata: GenericObject<string> = {};
|
let cdata: GenericObject<string> = {};
|
||||||
for (let i = 0; i < inputs.length; i += 2) {
|
for (let i = 0; i < inputs.length; i ++) {
|
||||||
const key = inputs[i].value.trim();
|
const key = inputs[i].value.trim();
|
||||||
if (key === "") {
|
if (key === "") {
|
||||||
return this.notify(__("Key cannot be empty"));
|
return this.notify(__("Field name cannot be empty"));
|
||||||
}
|
}
|
||||||
if (cdata[key]) {
|
if (cdata[key]) {
|
||||||
return this.notify(__("Duplicate key: {0}", key));
|
return this.notify(__("Duplicate field: {0}", key));
|
||||||
}
|
}
|
||||||
cdata[key] = inputs[i + 1].value.trim();
|
cdata[key] = lists[i].selectedItem.data.text;
|
||||||
}
|
}
|
||||||
if (this.handle)
|
if (this.handle)
|
||||||
this.handle(cdata);
|
this.handle(cdata);
|
||||||
@ -165,41 +313,35 @@ namespace OS {
|
|||||||
* @private
|
* @private
|
||||||
* @memberof NewTableDialog
|
* @memberof NewTableDialog
|
||||||
*/
|
*/
|
||||||
private addField(key: string, value: string, removable: boolean): void {
|
private addField(): void {
|
||||||
const div = $("<div>")
|
const div = $("<div>")
|
||||||
.css("width", "100%")
|
|
||||||
.css("display", "flex")
|
.css("display", "flex")
|
||||||
.css("flex-direction", "row")
|
.css("flex-direction", "row")
|
||||||
.appendTo(this.container);
|
.appendTo(this.container);
|
||||||
$("<input>")
|
$("<input>")
|
||||||
.attr("type", "text")
|
.attr("type", "text")
|
||||||
.css("width", "50%")
|
.css("flex", "1")
|
||||||
.css("height", "25px")
|
|
||||||
.val(key)
|
|
||||||
.appendTo(div);
|
.appendTo(div);
|
||||||
$("<afx-list-view>")
|
let list = $("<afx-list-view>")
|
||||||
.css("width", "50%")
|
.css("flex", "1")
|
||||||
.css("height", "25px")
|
.appendTo(div)[0] as GUI.tag.ListViewTag;
|
||||||
|
list.uify(this.observable);
|
||||||
|
list.dropdown = true;
|
||||||
|
list.data = [
|
||||||
|
{text:"TEXT"},
|
||||||
|
{text:"INTEGER"},
|
||||||
|
{text:"REAL"},
|
||||||
|
{text:"NUMERIC"},
|
||||||
|
];
|
||||||
|
list.selected = 0;
|
||||||
|
const btn = $("<afx-button>");
|
||||||
|
btn[0].uify(undefined);
|
||||||
|
(btn[0] as GUI.tag.ButtonTag).iconclass = "fa fa-minus";
|
||||||
|
btn
|
||||||
|
.on("click", () => {
|
||||||
|
div.remove();
|
||||||
|
})
|
||||||
.appendTo(div);
|
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -208,23 +350,19 @@ namespace OS {
|
|||||||
* Scheme definition
|
* Scheme definition
|
||||||
*/
|
*/
|
||||||
NewTableDialog.scheme = `\
|
NewTableDialog.scheme = `\
|
||||||
<afx-app-window width='350' height='300'>
|
<afx-app-window width='400' height='350'>
|
||||||
<afx-hbox>
|
<afx-vbox padding = "10">
|
||||||
<div data-width="10" ></div>
|
<afx-input label="__(Table name)" data-id="txt-tblname" data-height="content"></afx-input>
|
||||||
<afx-vbox>
|
<afx-label text="__(Fields in table:)" data-height="30"></afx-label>
|
||||||
<div data-height="5" ></div>
|
<div data-id="container" style="position:relative;"></div>
|
||||||
<afx-label text="__(Table layout)" data-height="30"></afx-label>
|
<afx-hbox data-height="35">
|
||||||
<div data-id="container"></div>
|
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "35" ></afx-button>
|
||||||
<afx-hbox data-height="30">
|
<div style = "text-align: right;">
|
||||||
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "30" ></afx-button>
|
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
|
||||||
<div ></div>
|
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
|
||||||
<afx-button data-id = "btnOk" text = "__(Ok)" data-width = "40" ></afx-button>
|
</div>
|
||||||
<afx-button data-id = "btnCancel" text = "__(Cancel)" data-width = "50" ></afx-button>
|
</afx-hbox>
|
||||||
</afx-hbox>
|
</afx-vbox>
|
||||||
<div data-height="5" ></div>
|
</afx-app-window>`;
|
||||||
</afx-vbox>
|
|
||||||
<div data-width="10" ></div>
|
|
||||||
</afx-hbox>
|
|
||||||
</afx-app-window>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<afx-app-window apptitle="SQLite browser" width="600" height="500" data-id="SQLiteDB">
|
<afx-app-window apptitle="SQLite browser" width="600" height="500" data-id="SQLiteDB">
|
||||||
<afx-vbox>
|
<afx-vbox padding="5">
|
||||||
<afx-hbox data-height="35">
|
<afx-hbox data-height="35">
|
||||||
<afx-button data-id = "bt-add-table" iconclass = "bi bi-plus-lg" data-width="content"></afx-button>
|
<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-list-view dropdown = "true" data-id="tbl-list"></afx-list-view>
|
||||||
@ -7,12 +7,22 @@
|
|||||||
|
|
||||||
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40">
|
<afx-tab-container data-id = "container" dir = "column" tabbarheight= "40">
|
||||||
<afx-hbox tabname="__(Struture)" iconclass = "bi bi-layout-wtf" >
|
<afx-hbox tabname="__(Struture)" iconclass = "bi bi-layout-wtf" >
|
||||||
<afx-grid-view></afx-grid-view>
|
<afx-grid-view data-id="sch-browser"></afx-grid-view>
|
||||||
</afx-hbox>
|
</afx-hbox>
|
||||||
|
|
||||||
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
|
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
|
||||||
<afx-grid-view></afx-grid-view>
|
<afx-vbox>
|
||||||
|
<afx-grid-view data-id="tb-browser"></afx-grid-view>
|
||||||
|
<afx-hbox data-height="35">
|
||||||
|
<afx-button iconclass_end="bi bi-chevron-double-right" data-id="bt-load-next" data-width="content"></afx-button>
|
||||||
|
<div></div>
|
||||||
|
<afx-button iconclass="bi bi-plus-lg" data-width="content"></afx-button>
|
||||||
|
<afx-button iconclass="bi bi-pencil-square" data-width="content"></afx-button>
|
||||||
|
<afx-button iconclass="bi bi-trash-fill" data-width="content"></afx-button>
|
||||||
|
</afx-hbox>
|
||||||
|
</afx-vbox>
|
||||||
</afx-hbox>
|
</afx-hbox>
|
||||||
|
|
||||||
</afx-tab-container>
|
</afx-tab-container>
|
||||||
|
|
||||||
</afx-vbox>
|
</afx-vbox>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
AntOSDK: development API for AntOS based applications/projects
|
AntOSDK: development API for AntOS based applications/projects
|
||||||
|
|
||||||
## Change logs
|
## Change logs
|
||||||
|
- 0.0.18: Add some public API on grid view
|
||||||
- 0.0.17: Use lastest AntOS d.ts file
|
- 0.0.17: Use lastest AntOS d.ts file
|
||||||
- 0.0.16: Fix jquery encoding error
|
- 0.0.16: Fix jquery encoding error
|
||||||
- 0.0.15: App name differ from libname, update AntOS API
|
- 0.0.15: App name differ from libname, update AntOS API
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
AntOSDK: development API for AntOS based applications/projects
|
AntOSDK: development API for AntOS based applications/projects
|
||||||
|
|
||||||
## Change logs
|
## Change logs
|
||||||
|
- 0.0.18: Add some public API on grid view
|
||||||
- 0.0.17: Use lastest AntOS d.ts file
|
- 0.0.17: Use lastest AntOS d.ts file
|
||||||
- 0.0.16: Fix jquery encoding error
|
- 0.0.16: Fix jquery encoding error
|
||||||
- 0.0.15: App name differ from libname, update AntOS API
|
- 0.0.15: App name differ from libname, update AntOS API
|
||||||
|
48
libantosdk/build/debug/core/ts/antos.d.ts
vendored
48
libantosdk/build/debug/core/ts/antos.d.ts
vendored
@ -475,6 +475,7 @@ declare namespace OS {
|
|||||||
* @extends {BaseDialog}
|
* @extends {BaseDialog}
|
||||||
*/
|
*/
|
||||||
class BasicDialog extends BaseDialog {
|
class BasicDialog extends BaseDialog {
|
||||||
|
['constructor']: typeof BasicDialog;
|
||||||
/**
|
/**
|
||||||
* Placeholder for the UI scheme to be rendered. This can
|
* Placeholder for the UI scheme to be rendered. This can
|
||||||
* be either the string definition of the scheme or
|
* be either the string definition of the scheme or
|
||||||
@ -4421,6 +4422,14 @@ declare namespace OS {
|
|||||||
* @memberof ListViewTag
|
* @memberof ListViewTag
|
||||||
*/
|
*/
|
||||||
private _selectedItems;
|
private _selectedItems;
|
||||||
|
/**
|
||||||
|
* The anchor element that the list view positioned on
|
||||||
|
* This is helpful when rendering dropdown list
|
||||||
|
* @private
|
||||||
|
* @type{HTMLElement}
|
||||||
|
* @memberof ListViewTag
|
||||||
|
*/
|
||||||
|
private _anchor;
|
||||||
/**
|
/**
|
||||||
* Data placeholder of the list
|
* Data placeholder of the list
|
||||||
*
|
*
|
||||||
@ -5539,6 +5548,18 @@ declare namespace OS {
|
|||||||
* @memberof LabelTag
|
* @memberof LabelTag
|
||||||
*/
|
*/
|
||||||
set iconclass(v: string);
|
set iconclass(v: string);
|
||||||
|
/**
|
||||||
|
* Set the CSS class of the label icon on the right side
|
||||||
|
*
|
||||||
|
* @memberof LabelTag
|
||||||
|
*/
|
||||||
|
set iconclass_end(v: string);
|
||||||
|
/**
|
||||||
|
* Set the CSS class of the label icon on the right side
|
||||||
|
*
|
||||||
|
* @memberof LabelTag
|
||||||
|
*/
|
||||||
|
set iconclass$(v: string);
|
||||||
/**
|
/**
|
||||||
* Setter: Set the text of the label
|
* Setter: Set the text of the label
|
||||||
*
|
*
|
||||||
@ -6295,13 +6316,25 @@ declare namespace OS {
|
|||||||
* @memberof GridViewTag
|
* @memberof GridViewTag
|
||||||
*/
|
*/
|
||||||
delete(row: GridRowTag): void;
|
delete(row: GridRowTag): void;
|
||||||
|
/**
|
||||||
|
* Scroll the grid view to bottom
|
||||||
|
*
|
||||||
|
* @memberof GridViewTag
|
||||||
|
*/
|
||||||
|
scroll_to_bottom(): void;
|
||||||
|
/**
|
||||||
|
* Scroll the grid view to top
|
||||||
|
*
|
||||||
|
* @memberof GridViewTag
|
||||||
|
*/
|
||||||
|
scroll_to_top(): void;
|
||||||
/**
|
/**
|
||||||
* Push a row to the grid
|
* Push a row to the grid
|
||||||
*
|
*
|
||||||
* @param {GenericObject<any>[]} row list of cell data
|
* @param {GenericObject<any>[]} row list of cell data
|
||||||
* @param {boolean} flag indicates where the row is add to beginning or end
|
* @param {boolean} flag indicates where the row is add to beginning or end
|
||||||
* of the row
|
* of the row
|
||||||
* @memberof GridViewTags
|
* @memberof GridViewTag
|
||||||
*/
|
*/
|
||||||
push(row: GenericObject<any>[], flag: boolean): void;
|
push(row: GenericObject<any>[], flag: boolean): void;
|
||||||
/**
|
/**
|
||||||
@ -6622,6 +6655,19 @@ declare namespace OS {
|
|||||||
* @memberof ButtonTag
|
* @memberof ButtonTag
|
||||||
*/
|
*/
|
||||||
set iconclass(v: string);
|
set iconclass(v: string);
|
||||||
|
/**
|
||||||
|
* Set the icon class on the right side of the button, this property
|
||||||
|
* allows to style the button icon using CSS
|
||||||
|
*
|
||||||
|
* @memberof ButtonTag
|
||||||
|
*/
|
||||||
|
set iconclass$(v: string);
|
||||||
|
/**
|
||||||
|
* Set the CSS class of the label icon on the right side
|
||||||
|
*
|
||||||
|
* @memberof ButtonTag
|
||||||
|
*/
|
||||||
|
set iconclass_end(v: string);
|
||||||
/**
|
/**
|
||||||
* Setter: Set the text of the button
|
* Setter: Set the text of the button
|
||||||
*
|
*
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"author": "Xuan Sang LE",
|
"author": "Xuan Sang LE",
|
||||||
"email": "mrsang@iohub.dev"
|
"email": "mrsang@iohub.dev"
|
||||||
},
|
},
|
||||||
"version": "0.0.17-a",
|
"version": "0.0.18-a",
|
||||||
"category": "Development",
|
"category": "Development",
|
||||||
"iconclass": "fa fa-cog",
|
"iconclass": "fa fa-cog",
|
||||||
"mimes": [
|
"mimes": [
|
||||||
|
Binary file not shown.
48
libantosdk/core/ts/antos.d.ts
vendored
48
libantosdk/core/ts/antos.d.ts
vendored
@ -475,6 +475,7 @@ declare namespace OS {
|
|||||||
* @extends {BaseDialog}
|
* @extends {BaseDialog}
|
||||||
*/
|
*/
|
||||||
class BasicDialog extends BaseDialog {
|
class BasicDialog extends BaseDialog {
|
||||||
|
['constructor']: typeof BasicDialog;
|
||||||
/**
|
/**
|
||||||
* Placeholder for the UI scheme to be rendered. This can
|
* Placeholder for the UI scheme to be rendered. This can
|
||||||
* be either the string definition of the scheme or
|
* be either the string definition of the scheme or
|
||||||
@ -4421,6 +4422,14 @@ declare namespace OS {
|
|||||||
* @memberof ListViewTag
|
* @memberof ListViewTag
|
||||||
*/
|
*/
|
||||||
private _selectedItems;
|
private _selectedItems;
|
||||||
|
/**
|
||||||
|
* The anchor element that the list view positioned on
|
||||||
|
* This is helpful when rendering dropdown list
|
||||||
|
* @private
|
||||||
|
* @type{HTMLElement}
|
||||||
|
* @memberof ListViewTag
|
||||||
|
*/
|
||||||
|
private _anchor;
|
||||||
/**
|
/**
|
||||||
* Data placeholder of the list
|
* Data placeholder of the list
|
||||||
*
|
*
|
||||||
@ -5539,6 +5548,18 @@ declare namespace OS {
|
|||||||
* @memberof LabelTag
|
* @memberof LabelTag
|
||||||
*/
|
*/
|
||||||
set iconclass(v: string);
|
set iconclass(v: string);
|
||||||
|
/**
|
||||||
|
* Set the CSS class of the label icon on the right side
|
||||||
|
*
|
||||||
|
* @memberof LabelTag
|
||||||
|
*/
|
||||||
|
set iconclass_end(v: string);
|
||||||
|
/**
|
||||||
|
* Set the CSS class of the label icon on the right side
|
||||||
|
*
|
||||||
|
* @memberof LabelTag
|
||||||
|
*/
|
||||||
|
set iconclass$(v: string);
|
||||||
/**
|
/**
|
||||||
* Setter: Set the text of the label
|
* Setter: Set the text of the label
|
||||||
*
|
*
|
||||||
@ -6295,13 +6316,25 @@ declare namespace OS {
|
|||||||
* @memberof GridViewTag
|
* @memberof GridViewTag
|
||||||
*/
|
*/
|
||||||
delete(row: GridRowTag): void;
|
delete(row: GridRowTag): void;
|
||||||
|
/**
|
||||||
|
* Scroll the grid view to bottom
|
||||||
|
*
|
||||||
|
* @memberof GridViewTag
|
||||||
|
*/
|
||||||
|
scroll_to_bottom(): void;
|
||||||
|
/**
|
||||||
|
* Scroll the grid view to top
|
||||||
|
*
|
||||||
|
* @memberof GridViewTag
|
||||||
|
*/
|
||||||
|
scroll_to_top(): void;
|
||||||
/**
|
/**
|
||||||
* Push a row to the grid
|
* Push a row to the grid
|
||||||
*
|
*
|
||||||
* @param {GenericObject<any>[]} row list of cell data
|
* @param {GenericObject<any>[]} row list of cell data
|
||||||
* @param {boolean} flag indicates where the row is add to beginning or end
|
* @param {boolean} flag indicates where the row is add to beginning or end
|
||||||
* of the row
|
* of the row
|
||||||
* @memberof GridViewTags
|
* @memberof GridViewTag
|
||||||
*/
|
*/
|
||||||
push(row: GenericObject<any>[], flag: boolean): void;
|
push(row: GenericObject<any>[], flag: boolean): void;
|
||||||
/**
|
/**
|
||||||
@ -6622,6 +6655,19 @@ declare namespace OS {
|
|||||||
* @memberof ButtonTag
|
* @memberof ButtonTag
|
||||||
*/
|
*/
|
||||||
set iconclass(v: string);
|
set iconclass(v: string);
|
||||||
|
/**
|
||||||
|
* Set the icon class on the right side of the button, this property
|
||||||
|
* allows to style the button icon using CSS
|
||||||
|
*
|
||||||
|
* @memberof ButtonTag
|
||||||
|
*/
|
||||||
|
set iconclass$(v: string);
|
||||||
|
/**
|
||||||
|
* Set the CSS class of the label icon on the right side
|
||||||
|
*
|
||||||
|
* @memberof ButtonTag
|
||||||
|
*/
|
||||||
|
set iconclass_end(v: string);
|
||||||
/**
|
/**
|
||||||
* Setter: Set the text of the button
|
* Setter: Set the text of the button
|
||||||
*
|
*
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"author": "Xuan Sang LE",
|
"author": "Xuan Sang LE",
|
||||||
"email": "mrsang@iohub.dev"
|
"email": "mrsang@iohub.dev"
|
||||||
},
|
},
|
||||||
"version": "0.0.17-a",
|
"version": "0.0.18-a",
|
||||||
"category": "Development",
|
"category": "Development",
|
||||||
"iconclass": "fa fa-cog",
|
"iconclass": "fa fa-cog",
|
||||||
"mimes": [
|
"mimes": [
|
||||||
|
@ -225,7 +225,7 @@
|
|||||||
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/libantosdk/README.md",
|
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/libantosdk/README.md",
|
||||||
"category": "Development",
|
"category": "Development",
|
||||||
"author": "Xuan Sang LE",
|
"author": "Xuan Sang LE",
|
||||||
"version": "0.0.17-a",
|
"version": "0.0.18-a",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/libantosdk/build/release/libantosdk.zip"
|
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/libantosdk/build/release/libantosdk.zip"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user