SQLiteDB: first working version

This commit is contained in:
DanyLE 2023-02-17 12:26:59 +01:00
parent cf21ef60e0
commit 04050f124f
12 changed files with 667 additions and 136 deletions

View File

@ -164,25 +164,27 @@ namespace OS {
return this.request(rq);
}
insert(table_name:string, record: GenericObject<any>): Promise<any>
insert(table_name:string, record: GenericObject<any>, pk:string): Promise<any>
{
let rq = {
action: 'insert',
args: {
table_name,
record
record,
pk
}
}
return this.request(rq);
}
update(table_name:string, record: GenericObject<any>): Promise<any>
update(table_name:string, record: GenericObject<any>, pk:string ): Promise<any>
{
let rq = {
action: 'update',
args: {
table_name,
record
record,
pk
}
}
return this.request(rq);
@ -317,20 +319,33 @@ namespace OS {
try {
await this._handle.init();
const d = {result: this._handle.fileinfo(), error: false};
let d = {
result: {
file:this._handle.fileinfo(),
schema: undefined
}, 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
d.result.schema = undefined
}
else
{
d.result.scheme = {}
d.result.schema = {
fields: [],
types: {},
pk: undefined
}
d.result.schema.fields = data.map(e=>e.name);
for(let v of data)
{
d.result.scheme[v.name] = v.type;
d.result.schema.types[v.name] = v.type;
if(v.pk)
{
d.result.schema.pk = v.name;
}
}
}
}
@ -358,7 +373,7 @@ namespace OS {
protected _rd(user_data: any): Promise<any> {
return new Promise(async (resolve, reject) => {
try{
if(this._table_name && ! this.info.scheme)
if(this._table_name && ! this.info.schema)
{
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
}
@ -422,17 +437,18 @@ namespace OS {
{
throw new Error(__("No data to submit to remote database, please check the `cache` field").__());
}
await this.onready();
if(this._id && this._table_name)
{
this.cache.id = this._id;
const ret = await this._handle.update(this._table_name, this.cache);
const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk);
resolve({result:ret, error: false});
return
}
if(this._table_name)
{
const ret = await this._handle.insert(this._table_name, this.cache);
const ret = await this._handle.insert(this._table_name, this.cache, this.info.schema.pk);
resolve({result:ret, error: false});
return
}
@ -458,7 +474,7 @@ namespace OS {
protected _rm(user_data: any): Promise<RequestResult> {
return new Promise(async (resolve, reject) => {
try {
if(this._table_name && ! this.info.scheme)
if(this._table_name && ! this.info.schema)
{
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
}

View File

@ -1,3 +1,11 @@
# SQLiteDB
"mimes":["application/vnd.sqlite3"],
This package contains the SQLiteDB API binding for AntOS applications
and a simple sqlite3 browser application that uses the library as reference
Note: in AntOS, file with extension `.db` is considered as sqlite3 database
file and has the following mimetype `application/vnd.sqlite3`. Applications
shall use this mime in `package.json`
## Change logs
- v0.1.0a: initial version with functioning library binding

View File

@ -44,7 +44,7 @@ handle.update = function(data)
local tb = {}
local gen = SQLQueryGenerator:new({})
for k,v in pairs(data.record) do
if k ~= "id" then
if k ~= data.pk then
table.insert(tb, string.format("%s=%s", k, gen:parse_value(v, {[type(v)] = true})))
end
end
@ -92,7 +92,7 @@ handle.insert = function(data)
local vals = {}
local gen = SQLQueryGenerator:new({})
for k,v in pairs(data.record) do
if k ~= "id" then
if k ~= data.pk then
table.insert(keys,k)
table.insert(vals,gen:parse_value(v, {[type(v)] = true}))
end
@ -119,8 +119,10 @@ handle.create_table = function(data)
end
local tb = {}
for k,v in pairs(data.scheme) do
if k ~= "id" then
table.insert(tb, k.." "..v)
end
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);
@ -186,7 +188,7 @@ handle.table_scheme = function(data)
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 sql = string.format("SELECT p.name, p.type, p.pk 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

View File

@ -1,3 +1,11 @@
# SQLiteDB
"mimes":["application/vnd.sqlite3"],
This package contains the SQLiteDB API binding for AntOS applications
and a simple sqlite3 browser application that uses the library as reference
Note: in AntOS, file with extension `.db` is considered as sqlite3 database
file and has the following mimetype `application/vnd.sqlite3`. Applications
shall use this mime in `package.json`
## Change logs
- v0.1.0a: initial version with functioning library binding

View File

@ -44,7 +44,7 @@ handle.update = function(data)
local tb = {}
local gen = SQLQueryGenerator:new({})
for k,v in pairs(data.record) do
if k ~= "id" then
if k ~= data.pk then
table.insert(tb, string.format("%s=%s", k, gen:parse_value(v, {[type(v)] = true})))
end
end
@ -92,7 +92,7 @@ handle.insert = function(data)
local vals = {}
local gen = SQLQueryGenerator:new({})
for k,v in pairs(data.record) do
if k ~= "id" then
if k ~= data.pk then
table.insert(keys,k)
table.insert(vals,gen:parse_value(v, {[type(v)] = true}))
end
@ -119,8 +119,10 @@ handle.create_table = function(data)
end
local tb = {}
for k,v in pairs(data.scheme) do
if k ~= "id" then
table.insert(tb, k.." "..v)
end
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);
@ -186,7 +188,7 @@ handle.table_scheme = function(data)
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 sql = string.format("SELECT p.name, p.type, p.pk 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

View File

@ -130,22 +130,24 @@ var OS;
};
return this.request(rq);
}
insert(table_name, record) {
insert(table_name, record, pk) {
let rq = {
action: 'insert',
args: {
table_name,
record
record,
pk
}
};
return this.request(rq);
}
update(table_name, record) {
update(table_name, record, pk) {
let rq = {
action: 'update',
args: {
table_name,
record
record,
pk
}
};
return this.request(rq);
@ -270,16 +272,29 @@ var OS;
return new Promise(async (resolve, reject) => {
try {
await this._handle.init();
const d = { result: this._handle.fileinfo(), error: false };
let d = {
result: {
file: this._handle.fileinfo(),
schema: undefined
}, 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;
d.result.schema = undefined;
}
else {
d.result.scheme = {};
d.result.schema = {
fields: [],
types: {},
pk: undefined
};
d.result.schema.fields = data.map(e => e.name);
for (let v of data) {
d.result.scheme[v.name] = v.type;
d.result.schema.types[v.name] = v.type;
if (v.pk) {
d.result.schema.pk = v.name;
}
}
}
}
@ -307,7 +322,7 @@ var OS;
_rd(user_data) {
return new Promise(async (resolve, reject) => {
try {
if (this._table_name && !this.info.scheme) {
if (this._table_name && !this.info.schema) {
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
}
if (!this._table_name) {
@ -361,14 +376,15 @@ var OS;
if (!this.cache) {
throw new Error(__("No data to submit to remote database, please check the `cache` field").__());
}
await this.onready();
if (this._id && this._table_name) {
this.cache.id = this._id;
const ret = await this._handle.update(this._table_name, this.cache);
const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk);
resolve({ result: ret, error: false });
return;
}
if (this._table_name) {
const ret = await this._handle.insert(this._table_name, this.cache);
const ret = await this._handle.insert(this._table_name, this.cache, this.info.schema.pk);
resolve({ result: ret, error: false });
return;
}
@ -392,7 +408,7 @@ var OS;
_rm(user_data) {
return new Promise(async (resolve, reject) => {
try {
if (this._table_name && !this.info.scheme) {
if (this._table_name && !this.info.schema) {
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
}
if (!this._table_name) {

View File

@ -6,12 +6,12 @@ var OS;
(function (application) {
/**
*
* @class SQLiteDB
* @class SQLiteDBBrowser
* @extends {BaseApplication}
*/
class SQLiteDB extends application.BaseApplication {
class SQLiteDBBrowser extends application.BaseApplication {
constructor(args) {
super("SQLiteDB", args);
super("SQLiteDBBrowser", args);
}
menu() {
return [
@ -19,12 +19,12 @@ var OS;
text: "__(File)",
nodes: [
{
text: "__(New)",
text: "__(New database)",
dataid: "new",
shortcut: 'A-N'
},
{
text: "__(Open)",
text: "__(Open database)",
dataid: "open",
shortcut: 'A-O'
},
@ -53,7 +53,7 @@ var OS;
}
this.tbl_list.data = list;
if (list.length > 0) {
this.tbl_list.selected = 0;
this.tbl_list.selected = list.length - 1;
}
});
}
@ -114,29 +114,65 @@ var OS;
if (this.container.selectedIndex == 0) {
if (!this.tbl_list.selectedItem)
return;
const scheme = this.tbl_list.selectedItem.data.handle.info.scheme;
if (!scheme)
const schema = this.tbl_list.selectedItem.data.handle.info.schema;
if (!schema)
return;
const data = [];
for (let k in scheme) {
for (let k in schema.types) {
data.push([
{ text: k },
{ text: scheme[k] }
{ text: schema.types[k] }
]);
}
this.grid_scheme.rows = data;
}
};
this.find("bt-add-table").onbtclick = (e) => {
this.find("bt-rm-table").onbtclick = async (e) => {
try {
if (!this.filehandle) {
return this.notify(__("Please open a database file"));
}
this.openDialog(new NewTableDialog(), {
title: __("Create new table")
})
.then((data) => {
console.log(data);
if (this.tbl_list.selectedItem == undefined) {
return;
}
const table = this.tbl_list.selectedItem.data.name;
const ret = await this.openDialog("YesNoDialog", {
title: __("Confirm delete?"),
text: __("Do you realy want to delete table: {0}", table)
});
if (ret) {
await this.filehandle.remove(table);
this.list_tables();
}
}
catch (e) {
this.error(__("Unable to execute action table delete: {0}", e.toString()), e);
}
};
this.find("bt-add-table").onbtclick = async (e) => {
try {
if (!this.filehandle) {
return this.notify(__("Please open a database file"));
}
const data = await this.openDialog(new NewTableDialog(), {
title: __("Create new table")
});
this.filehandle.cache = data.schema;
await this.filehandle.write(data.name);
this.list_tables();
}
catch (e) {
this.error(__("Unable to create table: {0}", e.toString()), e);
}
};
this.find("btn-edit-record").onbtclick = async (e) => {
this.edit_record();
};
this.find("btn-add-record").onbtclick = async (e) => {
this.add_record();
};
this.find("btn-delete-record").onbtclick = async (e) => {
this.remove_record();
};
this.btn_loadmore.onbtclick = async (e) => {
try {
@ -153,13 +189,14 @@ var OS;
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) => {
const headers = handle.info.schema.fields.map((e) => {
return { text: e };
});
this.grid_table.header = headers;
this.grid_table.rows = [];
const records = await handle.read({ fields: ["COUNT(*)"] });
this.n_records = records[0]["(COUNT(*))"];
this.btn_loadmore.text = `0/${this.n_records}`;
await this.load_table();
this.container.selectedIndex = 1;
}
@ -167,24 +204,119 @@ var OS;
this.error(__("Error reading table: {0}", e.toString()), e);
}
};
this.grid_table.oncelldbclick = async (e) => {
this.edit_record();
};
this.openFile();
}
async add_record() {
try {
const table_handle = this.tbl_list.selectedItem;
if (!table_handle) {
return;
}
const file_hd = table_handle.data.handle;
const schema = table_handle.data.handle.info.schema;
const model = {};
for (let k in schema.types) {
if (["INTEGER", "REAL", "NUMERIC"].includes(schema.types[k])) {
model[k] = 0;
}
else {
model[k] = "";
}
}
console.log(model);
const data = await this.openDialog(new RecordEditDialog(), {
title: __("New record"),
schema: schema,
record: model
});
file_hd.cache = data;
await file_hd.write(undefined);
this.n_records += 1;
await this.load_table();
}
catch (e) {
this.error(__("Error edit/view record: {0}", e.toString()), e);
}
}
async remove_record() {
try {
const cell = this.grid_table.selectedCell;
const row = this.grid_table.selectedRow;
const table_handle = this.tbl_list.selectedItem;
if (!cell || !table_handle) {
return;
}
const pk_id = cell.data.record[table_handle.data.handle.info.schema.pk];
const ret = await this.openDialog("YesNoDialog", {
title: __("Delete record"),
text: __("Do you realy want to delete record {0}", pk_id)
});
if (!ret) {
return;
}
const file_hd = `${table_handle.data.handle.path}@${pk_id}`.asFileHandle();
await file_hd.remove();
this.n_records--;
// remove the target row
this.grid_table.delete(row);
this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`;
}
catch (e) {
this.error(__("Error deleting record: {0}", e.toString()), e);
}
}
async edit_record() {
try {
const cell = this.grid_table.selectedCell;
const row = this.grid_table.selectedRow;
const table_handle = this.tbl_list.selectedItem;
if (!cell || !table_handle) {
return;
}
const data = await this.openDialog(new RecordEditDialog(), {
title: __("View/edit record"),
schema: table_handle.data.handle.info.schema,
record: cell.data.record
});
const pk_id = cell.data.record[table_handle.data.handle.info.schema.pk];
const file_hd = `${table_handle.data.handle.path}@${pk_id}`.asFileHandle();
file_hd.cache = data;
await file_hd.write(undefined);
const row_data = [];
for (let k of file_hd.info.schema.fields) {
let text = data[k];
if (text.length > 100) {
text = text.substring(0, 100);
}
row_data.push({
text: text,
record: data
});
}
row.data = row_data;
}
catch (e) {
this.error(__("Error edit/view record: {0}", e.toString()), e);
}
}
async load_table() {
if (this.grid_table.rows.length >= this.n_records) {
if (this.grid_table.rows && 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) => {
const headers = handle.info.schema.fields.map((e) => {
return { text: e };
});
// read all records
const records = await handle.read({
where: { id$gt: this.last_max_id },
limit: 10
});
const filter = { where: {}, limit: 10 };
filter.where[`${handle.info.schema.pk}\$gt`] = this.last_max_id;
const records = await handle.read(filter);
if (records && records.length > 0) {
for (let e of records) {
const row = [];
@ -208,7 +340,10 @@ var OS;
this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`;
}
}
application.SQLiteDB = SQLiteDB;
application.SQLiteDBBrowser = SQLiteDBBrowser;
SQLiteDBBrowser.dependencies = [
"pkg://SQLiteDB/libsqlite.js"
];
class NewTableDialog extends OS.GUI.BasicDialog {
/**
* Creates an instance of NewTableDialog.
@ -227,7 +362,6 @@ var OS;
this.container = this.find("container");
this.find("btnCancel").onbtclick = (e) => this.quit();
this.find("btnAdd").onbtclick = (e) => this.addField();
$(this.find("wrapper"));
$(this.container)
.css("overflow-y", "auto");
this.addField();
@ -236,6 +370,7 @@ var OS;
if (!input.value || input.value == "") {
return this.notify(__("Please enter table name"));
}
const tblname = input.value;
const inputs = $("input", this.container);
const lists = $("afx-list-view", this.container);
if (inputs.length == 0) {
@ -253,7 +388,7 @@ var OS;
cdata[key] = lists[i].selectedItem.data.text;
}
if (this.handle)
this.handle(cdata);
this.handle({ name: tblname, schema: cdata });
this.quit();
};
}
@ -304,13 +439,78 @@ var OS;
<afx-label text="__(Fields in table:)" data-height="30"></afx-label>
<div data-id="container" style="position:relative;"></div>
<afx-hbox data-height="35">
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "35" ></afx-button>
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "content" ></afx-button>
<div style = "text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>`;
class RecordEditDialog extends OS.GUI.BasicDialog {
/**
* Creates an instance of RecordEditDialog.
* @memberof RecordEditDialog
*/
constructor() {
super("RecordEditDialog");
}
/**
* Main entry point
*
* @memberof RecordEditDialog
*/
main() {
super.main();
this.container = this.find("container");
this.find("btnCancel").onbtclick = (e) => this.quit();
$(this.container)
.css("overflow-y", "auto");
if (!this.data || !this.data.schema) {
throw new Error(__("No data provided for dialog").__());
}
for (let k in this.data.schema.types) {
const input = $("<afx-input>").appendTo(this.container)[0];
input.uify(this.observable);
input.label = k;
if (k == this.data.schema.pk) {
input.disable = true;
}
if (this.data.schema.types[k] == "TEXT") {
input.verbose = true;
$(input).css("height", "100px");
}
if (this.data.record[k] != undefined) {
input.value = this.data.record[k];
}
}
this.find("btnOk").onbtclick = (e) => {
const inputs = $("afx-input", this.container);
const data = {};
for (let input of inputs) {
data[input.label.__()] = input.value;
}
if (this.handle)
this.handle(data);
this.quit();
};
}
}
/**
* Scheme definition
*/
RecordEditDialog.scheme = `\
<afx-app-window width='550' height='500'>
<afx-vbox padding = "5">
<div data-id="container" style="row-gap: 5px;"></div>
<afx-hbox data-height="35">
<div></div>
<div data-width="content">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>`;
})(application = OS.application || (OS.application = {}));
})(OS || (OS = {}));
@ -447,22 +647,24 @@ var OS;
};
return this.request(rq);
}
insert(table_name, record) {
insert(table_name, record, pk) {
let rq = {
action: 'insert',
args: {
table_name,
record
record,
pk
}
};
return this.request(rq);
}
update(table_name, record) {
update(table_name, record, pk) {
let rq = {
action: 'update',
args: {
table_name,
record
record,
pk
}
};
return this.request(rq);
@ -587,16 +789,29 @@ var OS;
return new Promise(async (resolve, reject) => {
try {
await this._handle.init();
const d = { result: this._handle.fileinfo(), error: false };
let d = {
result: {
file: this._handle.fileinfo(),
schema: undefined
}, 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;
d.result.schema = undefined;
}
else {
d.result.scheme = {};
d.result.schema = {
fields: [],
types: {},
pk: undefined
};
d.result.schema.fields = data.map(e => e.name);
for (let v of data) {
d.result.scheme[v.name] = v.type;
d.result.schema.types[v.name] = v.type;
if (v.pk) {
d.result.schema.pk = v.name;
}
}
}
}
@ -624,7 +839,7 @@ var OS;
_rd(user_data) {
return new Promise(async (resolve, reject) => {
try {
if (this._table_name && !this.info.scheme) {
if (this._table_name && !this.info.schema) {
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
}
if (!this._table_name) {
@ -678,14 +893,15 @@ var OS;
if (!this.cache) {
throw new Error(__("No data to submit to remote database, please check the `cache` field").__());
}
await this.onready();
if (this._id && this._table_name) {
this.cache.id = this._id;
const ret = await this._handle.update(this._table_name, this.cache);
const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk);
resolve({ result: ret, error: false });
return;
}
if (this._table_name) {
const ret = await this._handle.insert(this._table_name, this.cache);
const ret = await this._handle.insert(this._table_name, this.cache, this.info.schema.pk);
resolve({ result: ret, error: false });
return;
}
@ -709,7 +925,7 @@ var OS;
_rm(user_data) {
return new Promise(async (resolve, reject) => {
try {
if (this._table_name && !this.info.scheme) {
if (this._table_name && !this.info.schema) {
throw new Error(__("Table `{0}` does not exists in database: {1}", this._table_name, this.path).__());
}
if (!this._table_name) {

View File

@ -1,16 +1,16 @@
{
"pkgname": "SQLiteDB",
"app":"SQLiteDB",
"name":"SQLite3 VFS API",
"app":"SQLiteDBBrowser",
"name":"SQLite3 Browser",
"description":"API for manipulate SQLite database as VFS handle",
"info":{
"author": "",
"email": ""
"author": "Dany LE",
"email": "mrsang@iohub.dev"
},
"version":"0.1.0-a",
"category":"Other",
"iconclass":"fa fa-adn",
"mimes":[".*"],
"category":"Library",
"iconclass":"bi bi-database",
"mimes":["application/vnd.sqlite3"],
"dependencies":[],
"locale": {}
}

View File

@ -2,6 +2,7 @@
<afx-vbox padding="5">
<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-rm-table" iconclass = "bi bi-dash-lg" data-width="content"></afx-button>
<afx-list-view dropdown = "true" data-id="tbl-list"></afx-list-view>
</afx-hbox>
@ -13,12 +14,13 @@
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
<afx-vbox>
<afx-grid-view data-id="tb-browser"></afx-grid-view>
<div data-height="5"></div>
<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-button iconclass="bi bi-plus-lg" data-id="btn-add-record" data-width="content"></afx-button>
<afx-button iconclass="bi bi-pencil-square" data-id="btn-edit-record" data-width="content"></afx-button>
<afx-button iconclass="bi bi-trash-fill" data-id= "btn-delete-record" data-width="content"></afx-button>
</afx-hbox>
</afx-vbox>
</afx-hbox>

View File

@ -1,12 +1,29 @@
/**
* Define missing API in Array interface
*
* @interface Array
* @template T
*/
interface Array<T> {
/**
* Check if the array includes an element
*
* @param {T} element to check
* @returns {boolean}
* @memberof Array
*/
includes(el: T):boolean;
}
namespace OS {
export namespace application {
/**
*
* @class SQLiteDB
* @class SQLiteDBBrowser
* @extends {BaseApplication}
*/
export class SQLiteDB extends BaseApplication {
export class SQLiteDBBrowser extends BaseApplication {
private filehandle: API.VFS.BaseFileHandle;
private tbl_list: GUI.tag.ListViewTag;
@ -17,7 +34,7 @@ namespace OS {
private n_records: number;
private btn_loadmore: GUI.tag.ButtonTag;
constructor(args: AppArgumentsType[]) {
super("SQLiteDB", args);
super("SQLiteDBBrowser", args);
}
menu() {
@ -26,12 +43,12 @@ namespace OS {
text: "__(File)",
nodes: [
{
text: "__(New)",
text: "__(New database)",
dataid: "new",
shortcut: 'A-N'
},
{
text: "__(Open)",
text: "__(Open database)",
dataid: "open",
shortcut: 'A-O'
},
@ -63,7 +80,7 @@ namespace OS {
this.tbl_list.data = list;
if(list.length > 0)
{
this.tbl_list.selected = 0;
this.tbl_list.selected = list.length - 1;
}
})
}
@ -128,32 +145,75 @@ namespace OS {
{
if(!this.tbl_list.selectedItem)
return;
const scheme = this.tbl_list.selectedItem.data.handle.info.scheme;
if(!scheme)
const schema = this.tbl_list.selectedItem.data.handle.info.schema;
if(!schema)
return;
const data = [];
for(let k in scheme)
for(let k in schema.types)
{
data.push([
{ text: k},
{text: scheme[k]}
{text: schema.types[k]}
])
}
this.grid_scheme.rows = data;
}
}
(this.find("bt-add-table") as GUI.tag.ButtonTag).onbtclick = (e) => {
(this.find("bt-rm-table") as GUI.tag.ButtonTag).onbtclick = async (e) => {
try {
if(!this.filehandle)
{
return this.notify(__("Please open a database file"));
}
this.openDialog(new NewTableDialog(), {
title: __("Create new table")
})
.then((data) => {
console.log(data);
if(this.tbl_list.selectedItem == undefined)
{
return;
}
const table = this.tbl_list.selectedItem.data.name;
const ret = await this.openDialog("YesNoDialog", {
title: __("Confirm delete?"),
text: __("Do you realy want to delete table: {0}", table)
});
if(ret)
{
await this.filehandle.remove(table);
this.list_tables();
}
}
catch(e)
{
this.error(__("Unable to execute action table delete: {0}", e.toString()), e);
}
}
(this.find("bt-add-table") as GUI.tag.ButtonTag).onbtclick = async (e) => {
try
{
if(!this.filehandle)
{
return this.notify(__("Please open a database file"));
}
const data = await this.openDialog(new NewTableDialog(), {
title: __("Create new table")
});
this.filehandle.cache = data.schema;
await this.filehandle.write(data.name);
this.list_tables();
}
catch(e)
{
this.error(__("Unable to create table: {0}", e.toString()), e);
}
}
(this.find("btn-edit-record") as GUI.tag.ButtonTag).onbtclick = async (e) => {
this.edit_record();
}
(this.find("btn-add-record") as GUI.tag.ButtonTag).onbtclick = async (e) => {
this.add_record();
}
(this.find("btn-delete-record") as GUI.tag.ButtonTag).onbtclick = async (e) => {
this.remove_record();
}
this.btn_loadmore.onbtclick = async (e) => {
try
@ -173,14 +233,14 @@ namespace OS {
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)=>{
const headers = handle.info.schema.fields.map((e) => {
return {text: e}
});
this.grid_table.header = headers;
this.grid_table.rows = [];
const records = await handle.read({fields:["COUNT(*)"]});
this.n_records = records[0]["(COUNT(*))"];
this.btn_loadmore.text = `0/${this.n_records}`;
await this.load_table();
this.container.selectedIndex = 1;
}
@ -189,12 +249,126 @@ namespace OS {
this.error(__("Error reading table: {0}", e.toString()),e);
}
}
this.grid_table.oncelldbclick = async (e) => {
this.edit_record();
}
this.openFile();
}
private async add_record()
{
try
{
const table_handle = this.tbl_list.selectedItem;
if(!table_handle)
{
return;
}
const file_hd = table_handle.data.handle;
const schema = table_handle.data.handle.info.schema;
const model = {};
for(let k in schema.types)
{
if(["INTEGER", "REAL", "NUMERIC"].includes(schema.types[k]))
{
model[k] = 0;
}
else
{
model[k] = "";
}
}
console.log(model);
const data = await this.openDialog(new RecordEditDialog(), {
title: __("New record"),
schema: schema,
record: model
});
file_hd.cache = data;
await file_hd.write(undefined);
this.n_records += 1;
await this.load_table();
}
catch (e)
{
this.error(__("Error edit/view record: {0}", e.toString()), e);
}
}
private async remove_record()
{
try
{
const cell = this.grid_table.selectedCell;
const row = this.grid_table.selectedRow;
const table_handle = this.tbl_list.selectedItem;
if(!cell || !table_handle)
{
return;
}
const pk_id = cell.data.record[table_handle.data.handle.info.schema.pk];
const ret = await this.openDialog("YesNoDialog", {
title: __("Delete record"),
text: __("Do you realy want to delete record {0}",pk_id)
});
if(!ret)
{
return;
}
const file_hd = `${table_handle.data.handle.path}@${pk_id}`.asFileHandle();
await file_hd.remove();
this.n_records--;
// remove the target row
this.grid_table.delete(row);
this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`;
}
catch(e)
{
this.error(__("Error deleting record: {0}", e.toString()),e);
}
}
private async edit_record()
{
try
{
const cell = this.grid_table.selectedCell;
const row = this.grid_table.selectedRow;
const table_handle = this.tbl_list.selectedItem;
if(!cell || !table_handle)
{
return;
}
const data = await this.openDialog(new RecordEditDialog(), {
title: __("View/edit record"),
schema: table_handle.data.handle.info.schema,
record: cell.data.record
});
const pk_id = cell.data.record[table_handle.data.handle.info.schema.pk];
const file_hd = `${table_handle.data.handle.path}@${pk_id}`.asFileHandle();
file_hd.cache = data;
await file_hd.write(undefined);
const row_data = [];
for(let k of file_hd.info.schema.fields)
{
let text:string = data[k];
if(text.length > 100)
{
text = text.substring(0,100);
}
row_data.push({
text: text,
record: data
});
}
row.data = row_data;
}
catch (e)
{
this.error(__("Error edit/view record: {0}", e.toString()), e);
}
}
private async load_table()
{
if(this.grid_table.rows.length >= this.n_records)
if(this.grid_table.rows && this.grid_table.rows.length >= this.n_records)
{
return;
}
@ -202,15 +376,13 @@ namespace OS {
return;
const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle;
await handle.onready();
const headers =
Object.getOwnPropertyNames(handle.info.scheme).map((e)=>{
const headers = handle.info.schema.fields.map((e) => {
return {text: e}
});
// read all records
const records = await handle.read({
where:{ id$gt: this.last_max_id },
limit: 10
});
const filter = { where: {}, limit: 10}
filter.where[`${handle.info.schema.pk}\$gt`] = this.last_max_id;
const records = await handle.read(filter);
if(records && records.length > 0)
{
@ -234,13 +406,16 @@ namespace OS {
}
this.grid_table.push(row, false);
}
(this.grid_table as any).scroll_to_bottom();
this.grid_table.scroll_to_bottom();
}
this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`;
}
}
SQLiteDBBrowser.dependencies = [
"pkg://SQLiteDB/libsqlite.js"
]
class NewTableDialog extends GUI.BasicDialog {
/**
@ -270,7 +445,6 @@ namespace OS {
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();
$(this.find("wrapper"))
$(this.container)
.css("overflow-y", "auto");
this.addField();
@ -281,7 +455,7 @@ namespace OS {
{
return this.notify(__("Please enter table name"));
}
const tblname = input.value;
const inputs = $("input", this.container) as JQuery<HTMLInputElement>;
const lists = $("afx-list-view", this.container) as JQuery<GUI.tag.ListViewTag>;
if(inputs.length == 0)
@ -301,7 +475,7 @@ namespace OS {
cdata[key] = lists[i].selectedItem.data.text;
}
if (this.handle)
this.handle(cdata);
this.handle({ name: tblname, schema: cdata});
this.quit();
}
}
@ -356,7 +530,7 @@ namespace OS {
<afx-label text="__(Fields in table:)" data-height="30"></afx-label>
<div data-id="container" style="position:relative;"></div>
<afx-hbox data-height="35">
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "35" ></afx-button>
<afx-button data-id = "btnAdd" iconclass="fa fa-plus" data-width = "content" ></afx-button>
<div style = "text-align: right;">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
@ -364,5 +538,90 @@ namespace OS {
</afx-hbox>
</afx-vbox>
</afx-app-window>`;
class RecordEditDialog extends GUI.BasicDialog
{
/**
* Reference to the form container
*
* @private
* @type {HTMLDivElement}
* @memberof RecordEditDialog
*/
private container: HTMLDivElement;
/**
* Creates an instance of RecordEditDialog.
* @memberof RecordEditDialog
*/
constructor() {
super("RecordEditDialog");
}
/**
* Main entry point
*
* @memberof RecordEditDialog
*/
main(): void {
super.main();
this.container = this.find("container") as HTMLDivElement;
(this.find("btnCancel") as GUI.tag.ButtonTag).onbtclick = (e) => this.quit();
$(this.container)
.css("overflow-y", "auto");
if(!this.data || !this.data.schema)
{
throw new Error(__("No data provided for dialog").__());
}
for(let k in this.data.schema.types)
{
const input = $("<afx-input>").appendTo(this.container)[0] as GUI.tag.InputTag;
input.uify(this.observable);
input.label = k;
if(k == this.data.schema.pk)
{
input.disable = true;
}
if(this.data.schema.types[k] == "TEXT")
{
input.verbose = true;
$(input).css("height", "100px");
}
if(this.data.record[k] != undefined)
{
input.value = this.data.record[k];
}
}
(this.find("btnOk") as GUI.tag.ButtonTag).onbtclick = (e) => {
const inputs = $("afx-input", this.container) as JQuery<GUI.tag.InputTag>;
const data = {};
for(let input of inputs)
{
data[input.label.__()] = input.value;
}
if (this.handle)
this.handle(data);
this.quit();
}
}
}
/**
* Scheme definition
*/
RecordEditDialog.scheme = `\
<afx-app-window width='550' height='500'>
<afx-vbox padding = "5">
<div data-id="container" style="row-gap: 5px;"></div>
<afx-hbox data-height="35">
<div></div>
<div data-width="content">
<afx-button data-id = "btnOk" text = "__(Ok)"></afx-button>
<afx-button data-id = "btnCancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>`;
}
}

View File

@ -1,16 +1,16 @@
{
"pkgname": "SQLiteDB",
"app":"SQLiteDB",
"name":"SQLite3 VFS API",
"app":"SQLiteDBBrowser",
"name":"SQLite3 Browser",
"description":"API for manipulate SQLite database as VFS handle",
"info":{
"author": "",
"email": ""
"author": "Dany LE",
"email": "mrsang@iohub.dev"
},
"version":"0.1.0-a",
"category":"Other",
"iconclass":"fa fa-adn",
"mimes":[".*"],
"category":"Library",
"iconclass":"bi bi-database",
"mimes":["application/vnd.sqlite3"],
"dependencies":[],
"locale": {}
}

View File

@ -2,6 +2,7 @@
<afx-vbox padding="5">
<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-rm-table" iconclass = "bi bi-dash-lg" data-width="content"></afx-button>
<afx-list-view dropdown = "true" data-id="tbl-list"></afx-list-view>
</afx-hbox>
@ -13,12 +14,13 @@
<afx-hbox tabname = "__(Browse data)" iconclass = "bi bi-table">
<afx-vbox>
<afx-grid-view data-id="tb-browser"></afx-grid-view>
<div data-height="5"></div>
<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-button iconclass="bi bi-plus-lg" data-id="btn-add-record" data-width="content"></afx-button>
<afx-button iconclass="bi bi-pencil-square" data-id="btn-edit-record" data-width="content"></afx-button>
<afx-button iconclass="bi bi-trash-fill" data-id= "btn-delete-record" data-width="content"></afx-button>
</afx-hbox>
</afx-vbox>
</afx-hbox>