mirror of
https://github.com/frappe/books.git
synced 2024-11-09 23:30:56 +00:00
collapse frappe folder and start desk
This commit is contained in:
parent
f89c6e778e
commit
e3a94dc997
6
app.js
Normal file
6
app.js
Normal file
@ -0,0 +1,6 @@
|
||||
const server = require('frappe-core/server');
|
||||
|
||||
server.start({
|
||||
backend: 'sqllite',
|
||||
connection_params: {db_path: 'test.db'}
|
||||
});
|
130
backends/rest_client.js
Normal file
130
backends/rest_client.js
Normal file
@ -0,0 +1,130 @@
|
||||
const frappe = require('frappe-core');
|
||||
const path = require('path');
|
||||
|
||||
class RESTClient {
|
||||
constructor({server, protocol='http'}) {
|
||||
this.server = server;
|
||||
this.protocol = protocol;
|
||||
|
||||
this.init_type_map();
|
||||
|
||||
this.json_headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
||||
}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
doc.doctype = doctype;
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.json_headers,
|
||||
body: JSON.stringify(doc)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async get(doctype, name) {
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.json_headers
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async get_all({doctype, fields, filters, start, limit, sort_by, order}) {
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
||||
|
||||
url = url + "?" + this.get_query_string({
|
||||
fields: JSON.stringify(fields),
|
||||
filters: JSON.stringify(filters),
|
||||
start: start,
|
||||
limit: limit,
|
||||
sort_by: sort_by,
|
||||
order: order
|
||||
});
|
||||
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.json_headers
|
||||
});
|
||||
return await response.json();
|
||||
|
||||
}
|
||||
|
||||
async update(doctype, doc) {
|
||||
doc.doctype = doctype;
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${doc.name}`);
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: this.json_headers,
|
||||
body: JSON.stringify(doc)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
||||
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: this.json_headers
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
get_query_string(params) {
|
||||
return Object.keys(params)
|
||||
.map(k => params[k] != null ? encodeURIComponent(k) + '=' + encodeURIComponent(params[k]) : null)
|
||||
.filter(v => v)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
init_type_map() {
|
||||
this.type_map = {
|
||||
'Currency': true
|
||||
,'Int': true
|
||||
,'Float': true
|
||||
,'Percent': true
|
||||
,'Check': true
|
||||
,'Small Text': true
|
||||
,'Long Text': true
|
||||
,'Code': true
|
||||
,'Text Editor': true
|
||||
,'Date': true
|
||||
,'Datetime': true
|
||||
,'Time': true
|
||||
,'Text': true
|
||||
,'Data': true
|
||||
,'Link': true
|
||||
,'Dynamic Link':true
|
||||
,'Password': true
|
||||
,'Select': true
|
||||
,'Read Only': true
|
||||
,'Attach': true
|
||||
,'Attach Image':true
|
||||
,'Signature': true
|
||||
,'Color': true
|
||||
,'Barcode': true
|
||||
,'Geolocation': true
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Database: RESTClient
|
||||
}
|
258
backends/sqlite.js
Normal file
258
backends/sqlite.js
Normal file
@ -0,0 +1,258 @@
|
||||
const frappe = require('frappe-core');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
|
||||
class sqliteDatabase {
|
||||
constructor({db_path}) {
|
||||
this.db_path = db_path;
|
||||
this.init_type_map();
|
||||
}
|
||||
|
||||
connect(db_path) {
|
||||
if (db_path) {
|
||||
this.db_path = db_path;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
this.conn = new sqlite3.Database(this.db_path, () => {
|
||||
// debug
|
||||
// this.conn.on('trace', (trace) => console.log(trace));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
for (let doctype in frappe.models.data.doctype) {
|
||||
if (await this.table_exists(doctype)) {
|
||||
await this.alter_table(doctype);
|
||||
} else {
|
||||
await this.create_table(doctype);
|
||||
}
|
||||
}
|
||||
await this.commit();
|
||||
}
|
||||
|
||||
async create_table(doctype) {
|
||||
let meta = frappe.get_meta(doctype);
|
||||
let columns = [];
|
||||
let values = [];
|
||||
|
||||
for (let df of this.get_fields(meta)) {
|
||||
if (this.type_map[df.fieldtype]) {
|
||||
columns.push(this.get_column_definition(df));
|
||||
if (df.default) {
|
||||
values.push(df.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = `CREATE TABLE IF NOT EXISTS ${frappe.slug(doctype)} (
|
||||
${columns.join(", ")})`;
|
||||
|
||||
return await this.run(query, values);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.conn.close();
|
||||
}
|
||||
|
||||
get_column_definition(df) {
|
||||
return `${df.fieldname} ${this.type_map[df.fieldtype]} ${df.reqd ? "not null" : ""} ${df.default ? "default ?" : ""}`
|
||||
}
|
||||
|
||||
async alter_table(doctype) {
|
||||
// get columns
|
||||
let table_columns = (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
|
||||
let meta = frappe.get_meta(doctype);
|
||||
let values = [];
|
||||
|
||||
for (let df of this.get_fields(meta)) {
|
||||
if (!table_columns.includes(df.fieldname) && this.type_map[df.fieldtype]) {
|
||||
values = []
|
||||
if (df.default) {
|
||||
values.push(df.default);
|
||||
}
|
||||
await this.run(`ALTER TABLE ${frappe.slug(doctype)} ADD COLUMN ${this.get_column_definition(df)}`, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get(doctype, name, fields='*') {
|
||||
if (fields instanceof Array) {
|
||||
fields = fields.join(", ");
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.get(`select ${fields} from ${frappe.slug(doctype)}
|
||||
where name = ?`, name,
|
||||
(err, row) => {
|
||||
resolve(row || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
let placeholders = Object.keys(doc).map(d => '?').join(', ');
|
||||
return await this.run(`insert into ${frappe.slug(doctype)}
|
||||
(${Object.keys(doc).join(", ")})
|
||||
values (${placeholders})`, this.get_formatted_values(doc));
|
||||
}
|
||||
|
||||
async update(doctype, doc) {
|
||||
let assigns = Object.keys(doc).map(key => `${key} = ?`);
|
||||
let values = this.get_formatted_values(doc);
|
||||
values.push(doc.name);
|
||||
|
||||
return await this.run(`update ${frappe.slug(doctype)}
|
||||
set ${assigns.join(", ")} where name=?`, values);
|
||||
}
|
||||
|
||||
get_formatted_values(doc) {
|
||||
return Object.values(doc).map(value => {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
return await this.run(`delete from ${frappe.slug(doctype)} where name=?`, name);
|
||||
}
|
||||
|
||||
get_all({doctype, fields=['name'], filters, start, limit, order_by='modified', order='desc'} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let conditions = this.get_filter_conditions(filters);
|
||||
|
||||
this.conn.all(`select ${fields.join(", ")}
|
||||
from ${frappe.slug(doctype)}
|
||||
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
||||
${order_by ? ("order by " + order_by) : ""} ${order_by ? (order || "asc") : ""}
|
||||
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`, conditions.values,
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get_filter_conditions(filters) {
|
||||
// {"status": "Open"} => `status = "Open"`
|
||||
// {"status": "Open", "name": ["like", "apple%"]}
|
||||
// => `status="Open" and name like "apple%"
|
||||
let conditions = [];
|
||||
let values = [];
|
||||
for (let key in filters) {
|
||||
const value = filters[key];
|
||||
if (value instanceof Array) {
|
||||
conditions.push(`${key} ${value[0]} ?`);
|
||||
values.push(value[1]);
|
||||
} else {
|
||||
conditions.push(`${key} = ?`);
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
return {
|
||||
conditions: conditions.length ? conditions.join(" and ") : "",
|
||||
values: values
|
||||
};
|
||||
}
|
||||
|
||||
run(query, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.run(query, params, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sql(query, params) {
|
||||
return new Promise((resolve) => {
|
||||
this.conn.all(query, params, (err, rows) => {
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async commit() {
|
||||
try {
|
||||
await this.run('commit');
|
||||
} catch (e) {
|
||||
if (e.errno !== 1) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async get_value(doctype, filters, fieldname='name') {
|
||||
if (typeof filters==='string') {
|
||||
filters = {name: filters};
|
||||
}
|
||||
|
||||
let row = await this.get_all({
|
||||
doctype:doctype,
|
||||
fields: [fieldname],
|
||||
filters: filters,
|
||||
start: 0,
|
||||
limit: 1});
|
||||
return row.length ? row[0][fieldname] : null;
|
||||
}
|
||||
|
||||
get_fields(meta) {
|
||||
// add standard fields
|
||||
let fields = frappe.model.standard_fields.slice();
|
||||
if (meta.istable) {
|
||||
fields = fields.concat(frappe.model.child_fields);
|
||||
}
|
||||
|
||||
// add model fields
|
||||
fields = fields.concat(meta.fields);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
async table_exists(table) {
|
||||
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
|
||||
return (name && name.length) ? true : false;
|
||||
}
|
||||
|
||||
init_type_map() {
|
||||
this.type_map = {
|
||||
'Currency': 'real'
|
||||
,'Int': 'integer'
|
||||
,'Float': 'real'
|
||||
,'Percent': 'real'
|
||||
,'Check': 'integer'
|
||||
,'Small Text': 'text'
|
||||
,'Long Text': 'text'
|
||||
,'Code': 'text'
|
||||
,'Text Editor': 'text'
|
||||
,'Date': 'text'
|
||||
,'Datetime': 'text'
|
||||
,'Time': 'text'
|
||||
,'Text': 'text'
|
||||
,'Data': 'text'
|
||||
,'Link': 'text'
|
||||
,'Dynamic Link':'text'
|
||||
,'Password': 'text'
|
||||
,'Select': 'text'
|
||||
,'Read Only': 'text'
|
||||
,'Attach': 'text'
|
||||
,'Attach Image':'text'
|
||||
,'Signature': 'text'
|
||||
,'Color': 'text'
|
||||
,'Barcode': 'text'
|
||||
,'Geolocation': 'text'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Database: sqliteDatabase };
|
46
client/desk/index.js
Normal file
46
client/desk/index.js
Normal file
@ -0,0 +1,46 @@
|
||||
const frappe = require('frappe-core');
|
||||
const Search = require('./search');
|
||||
const Router = require('./router');
|
||||
|
||||
module.exports = class Desk {
|
||||
constructor() {
|
||||
frappe.router = new Router();
|
||||
|
||||
this.wrapper = document.querySelector('.desk');
|
||||
|
||||
this.nav = frappe.ui.add('header', 'nav text-center', this.wrapper);
|
||||
|
||||
this.body = frappe.ui.add('div', 'desk-body two-column', this.wrapper);
|
||||
this.sidebar = frappe.ui.add('div', 'sidebar', this.body);
|
||||
this.main = frappe.ui.add('div', 'main', this.body);
|
||||
|
||||
this.sidebar_items = [];
|
||||
this.list_pages = {};
|
||||
this.edit_pages = {};
|
||||
|
||||
// this.search = new Search(this.nav);
|
||||
}
|
||||
|
||||
init_routes() {
|
||||
frappe.router.on('list/:doctype', (params) => {
|
||||
|
||||
})
|
||||
frappe.router.on('edit/:doctype/:name', (params) => {
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
add_sidebar_item(label, action) {
|
||||
let item = frappe.ui.add('a', '', frappe.ui.add('p', null, frappe.desk.sidebar));
|
||||
item.textContent = label;
|
||||
if (typeof action === 'string') {
|
||||
item.href = action;
|
||||
} else {
|
||||
item.addEventHandler('click', () => {
|
||||
action();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
82
client/desk/router.js
Normal file
82
client/desk/router.js
Normal file
@ -0,0 +1,82 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
module.exports = class Router {
|
||||
constructor() {
|
||||
this.current_page = null;
|
||||
this.static_routes = {};
|
||||
this.dynamic_routes = {};
|
||||
this.listen();
|
||||
}
|
||||
|
||||
add(route, handler) {
|
||||
let page = {handler: handler};
|
||||
|
||||
// '/todo/:name/:place'.match(/:([^/]+)/g);
|
||||
page.param_keys = route.match(/:([^/]+)/g);
|
||||
|
||||
if (page.param_keys) {
|
||||
// make expression
|
||||
// '/todo/:name/:place'.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
||||
page.expression = route.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
||||
this.dynamic_routes[route] = page;
|
||||
} else {
|
||||
this.static_routes[route] = page;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
listen() {
|
||||
window.onhashchange = this.changed.bind(this);
|
||||
this.changed();
|
||||
}
|
||||
|
||||
async changed(event) {
|
||||
if (window.location.hash.length > 0) {
|
||||
const page_name = window.location.hash.substr(1);
|
||||
this.show(page_name);
|
||||
} else if (this.static_routes['default']) {
|
||||
this.show('default');
|
||||
}
|
||||
}
|
||||
|
||||
show(route) {
|
||||
if (!route) {
|
||||
route = 'default';
|
||||
}
|
||||
|
||||
if (route[0]==='#') {
|
||||
route = route.substr(1);
|
||||
}
|
||||
|
||||
let page = this.match(route);
|
||||
|
||||
if (page) {
|
||||
if (typeof page.handler==='function') {
|
||||
page.handler(page.params);
|
||||
} else {
|
||||
page.handler.show(page.params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match(route) {
|
||||
for(let key in this.static_routes) {
|
||||
if (key === route) {
|
||||
return {handler: this.static_routes[key].handler};
|
||||
}
|
||||
}
|
||||
|
||||
for(let key in this.dynamic_routes) {
|
||||
let page = this.dynamic_routes[key];
|
||||
let matches = route.match(new RegExp(page.expression));
|
||||
|
||||
if (matches && matches.length == page.param_keys.length + 1) {
|
||||
let params = {}
|
||||
for (let i=0; i < page.param_keys.length; i++) {
|
||||
params[page.param_keys[i].substr(1)] = matches[i + 1];
|
||||
}
|
||||
return {handler:page.handler, params: params};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
client/desk/search.js
Normal file
16
client/desk/search.js
Normal file
@ -0,0 +1,16 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
module.exports = class Search {
|
||||
constructor(parent) {
|
||||
this.input = frappe.ui.add('input', 'form-control nav-search', parent);
|
||||
this.input.addEventListener('keypress', function(event) {
|
||||
if (event.keyCode===13) {
|
||||
let list = frappe.router.current_page.list;
|
||||
if (list) {
|
||||
list.search_text = this.value;
|
||||
list.run();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
20
client/index.js
Normal file
20
client/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
const common = require('frappe-core/common');
|
||||
const Database = require('frappe-core/backends/rest_client').Database;
|
||||
const frappe = require('frappe-core');
|
||||
frappe.ui = require('./ui');
|
||||
const Desk = require('./desk');
|
||||
|
||||
module.exports = {
|
||||
async start({server, container}) {
|
||||
window.frappe = frappe;
|
||||
frappe.init();
|
||||
common.init_libs(frappe);
|
||||
|
||||
frappe.fetch = window.fetch.bind();
|
||||
frappe.db = await new Database({server: server});
|
||||
|
||||
frappe.desk = new Desk();
|
||||
await frappe.login();
|
||||
}
|
||||
};
|
||||
|
89
client/ui/dropdown.js
Normal file
89
client/ui/dropdown.js
Normal file
@ -0,0 +1,89 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Dropdown {
|
||||
constructor({parent, label, btn_class = 'btn-secondary', items = []}) {
|
||||
Object.assign(this, arguments[0]);
|
||||
|
||||
this.dropdown_items = [];
|
||||
this.setup_background_click();
|
||||
this.make();
|
||||
|
||||
// init items
|
||||
if (this.items) {
|
||||
for (item of this.items) {
|
||||
this.add_item(item.label, item.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup_background_click() {
|
||||
if (!document.dropdown_setup) {
|
||||
frappe.dropdowns = [];
|
||||
// setup hiding all dropdowns on click
|
||||
document.addEventListener('click', (event) => {
|
||||
for (let d of frappe.dropdowns) {
|
||||
if (d.button !== event.target) {
|
||||
d.collapse();
|
||||
}
|
||||
}
|
||||
});
|
||||
document.dropdown_setup = true;
|
||||
}
|
||||
frappe.dropdowns.push(this);
|
||||
}
|
||||
|
||||
make() {
|
||||
this.dropdown = frappe.ui.add('div', 'dropdown', this.parent);
|
||||
this.make_button();
|
||||
this.dropdown_menu = frappe.ui.add('div', 'dropdown-menu', this.dropdown);
|
||||
}
|
||||
|
||||
make_button() {
|
||||
this.button = frappe.ui.add('button', 'btn ' + this.btn_class,
|
||||
this.dropdown);
|
||||
frappe.ui.add_class(this.button, 'dropdown-toggle');
|
||||
this.button.textContent = this.label;
|
||||
this.button.addEventListener('click', () => {
|
||||
this.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
expand() {
|
||||
this.dropdown.classList.add('show');
|
||||
this.dropdown_menu.classList.add('show');
|
||||
}
|
||||
|
||||
collapse() {
|
||||
this.dropdown.classList.remove('show');
|
||||
this.dropdown_menu.classList.remove('show');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.dropdown.classList.toggle('show');
|
||||
this.dropdown_menu.classList.toggle('show');
|
||||
}
|
||||
|
||||
add_item(label, action) {
|
||||
let item = frappe.ui.add('a', 'dropdown-item', this.dropdown_menu);
|
||||
item.textContent = label;
|
||||
if (typeof action === 'string') {
|
||||
item.src = action;
|
||||
item.addEventListener('click', () => {
|
||||
this.toggle();
|
||||
});
|
||||
} else {
|
||||
item.addEventListener('click', async () => {
|
||||
await action();
|
||||
this.toggle();
|
||||
});
|
||||
}
|
||||
this.dropdown_items.push(item);
|
||||
}
|
||||
|
||||
float_right() {
|
||||
frappe.ui.add_class(this.dropdown, 'float-right');
|
||||
frappe.ui.add_class(this.dropdown_menu, 'dropdown-menu-right');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dropdown;
|
46
client/ui/index.js
Normal file
46
client/ui/index.js
Normal file
@ -0,0 +1,46 @@
|
||||
const frappe = require('frappe-core');
|
||||
const Dropdown = require('./dropdown');
|
||||
|
||||
module.exports = {
|
||||
add(tag, className, parent) {
|
||||
let element = document.createElement(tag);
|
||||
if (className) {
|
||||
for (let c of className.split(' ')) {
|
||||
this.add_class(element, c);
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
parent.appendChild(element);
|
||||
}
|
||||
return element;
|
||||
},
|
||||
|
||||
remove(element) {
|
||||
element.parentNode.removeChild(element);
|
||||
},
|
||||
|
||||
add_class(element, className) {
|
||||
if (element.classList) {
|
||||
element.classList.add(className);
|
||||
} else {
|
||||
element.className += " " + className;
|
||||
}
|
||||
},
|
||||
|
||||
remove_class(element, className) {
|
||||
if (element.classList) {
|
||||
element.classList.remove(className);
|
||||
} else {
|
||||
element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
},
|
||||
|
||||
toggle(element, default_display = '') {
|
||||
element.style.display = element.style.display === 'none' ? default_display : 'none';
|
||||
},
|
||||
|
||||
make_dropdown(label, parent, btn_class = 'btn-secondary') {
|
||||
return new Dropdown({parent: parent, label:label, btn_class:btn_class});
|
||||
}
|
||||
|
||||
}
|
101
client/view/controls/base.js
Normal file
101
client/view/controls/base.js
Normal file
@ -0,0 +1,101 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class BaseControl {
|
||||
constructor(docfield, parent) {
|
||||
Object.assign(this, docfield);
|
||||
if (!this.fieldname) {
|
||||
this.fieldname = frappe.slug(this.label);
|
||||
}
|
||||
this.parent = parent;
|
||||
if (this.setup) {
|
||||
this.setup();
|
||||
}
|
||||
}
|
||||
|
||||
bind(doc) {
|
||||
this.doc = doc;
|
||||
|
||||
this.doc.add_handler(this.fieldname, () => {
|
||||
this.set_doc_value();
|
||||
});
|
||||
|
||||
this.set_doc_value();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.make();
|
||||
this.set_doc_value();
|
||||
}
|
||||
|
||||
set_doc_value() {
|
||||
if (this.doc) {
|
||||
this.set_input_value(this.doc.get(this.fieldname));
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
if (!this.form_group) {
|
||||
this.make_form_group();
|
||||
this.make_label();
|
||||
this.make_input();
|
||||
this.set_input_name();
|
||||
this.make_description();
|
||||
this.bind_change_event();
|
||||
}
|
||||
}
|
||||
|
||||
make_form_group() {
|
||||
this.form_group = frappe.ui.add('div', 'form-group', this.parent);
|
||||
}
|
||||
|
||||
make_label() {
|
||||
this.label_element = frappe.ui.add('label', null, this.form_group);
|
||||
this.label_element.textContent = this.label;
|
||||
}
|
||||
|
||||
make_input() {
|
||||
this.input = frappe.ui.add('input', 'form-control', this.form_group);
|
||||
}
|
||||
|
||||
set_input_name() {
|
||||
this.input.setAttribute('name', this.fieldname);
|
||||
}
|
||||
|
||||
make_description() {
|
||||
if (this.description) {
|
||||
this.description_element = frappe.ui.add('small', 'form-text text-muted', this.form_group);
|
||||
this.description_element.textContent = this.description;
|
||||
}
|
||||
}
|
||||
|
||||
set_input_value(value) {
|
||||
if (value === undefined || value === null) {
|
||||
value = '';
|
||||
}
|
||||
this.input.value = value;
|
||||
}
|
||||
|
||||
async get_input_value() {
|
||||
return await this.parse(this.input.value);
|
||||
}
|
||||
|
||||
async parse(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
async validate(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
bind_change_event() {
|
||||
this.input.addEventListener('change', (e) => this.handle_change(e));
|
||||
}
|
||||
|
||||
async handle_change(e) {
|
||||
let value = await this.get_input_value();
|
||||
value = await this.validate(value);
|
||||
await this.doc.set(this.fieldname, value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseControl;
|
18
client/view/controls/index.js
Normal file
18
client/view/controls/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
const control_classes = {
|
||||
Data: require('./data'),
|
||||
Text: require('./text'),
|
||||
Select: require('./select')
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
get_control_class(fieldtype) {
|
||||
return control_classes[fieldtype];
|
||||
},
|
||||
make_control(field, parent) {
|
||||
const control_class = this.get_control_class(field.fieldtype);
|
||||
let control = new control_class(field, parent);
|
||||
control.make();
|
||||
return control;
|
||||
}
|
||||
}
|
24
client/view/controls/select.js
Normal file
24
client/view/controls/select.js
Normal file
@ -0,0 +1,24 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class SelectControl extends BaseControl {
|
||||
make_input() {
|
||||
this.input = frappe.ui.add('select', 'form-control', this.form_group);
|
||||
|
||||
let options = this.options;
|
||||
if (typeof options==='string') {
|
||||
options = options.split('\n');
|
||||
}
|
||||
|
||||
for (let value of options) {
|
||||
let option = frappe.ui.add('option', null, this.input);
|
||||
option.textContent = value;
|
||||
option.setAttribute('value', value);
|
||||
}
|
||||
}
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('row', '3');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SelectControl;
|
13
client/view/controls/text.js
Normal file
13
client/view/controls/text.js
Normal file
@ -0,0 +1,13 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class TextControl extends BaseControl {
|
||||
make_input() {
|
||||
this.input = frappe.ui.add('textarea', 'form-control', this.form_group);
|
||||
}
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('rows', '8');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TextControl;
|
108
client/view/form.js
Normal file
108
client/view/form.js
Normal file
@ -0,0 +1,108 @@
|
||||
const frappe = require('frappe-core');
|
||||
const controls = require('./controls');
|
||||
|
||||
class Form {
|
||||
constructor({doctype, parent, submit_label='Submit'}) {
|
||||
this.parent = parent;
|
||||
this.doctype = doctype;
|
||||
this.submit_label = submit_label;
|
||||
|
||||
this.controls = {};
|
||||
this.controls_list = [];
|
||||
|
||||
this.meta = frappe.get_meta(this.doctype);
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
if (this.body || !this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.body = frappe.ui.add('div', 'form-body', this.parent);
|
||||
this.make_actions();
|
||||
|
||||
this.form = frappe.ui.add('form', null, this.body);
|
||||
for(let df of this.meta.fields) {
|
||||
if (controls.get_control_class(df.fieldtype)) {
|
||||
let control = controls.make_control(df, this.form);
|
||||
this.controls_list.push(control);
|
||||
this.controls[df.fieldname] = control;
|
||||
}
|
||||
}
|
||||
this.make_submit();
|
||||
}
|
||||
|
||||
make_actions() {
|
||||
this.toolbar = frappe.ui.add('div', 'form-toolbar', this.body);
|
||||
this.actions = frappe.ui.make_dropdown('Actions', this.toolbar);
|
||||
|
||||
// delete
|
||||
this.actions.add_item('Delete', async () => {
|
||||
await this.doc.delete();
|
||||
this.show_alert('Deleted', 'success');
|
||||
});
|
||||
|
||||
this.actions.float_right();
|
||||
}
|
||||
|
||||
make_submit() {
|
||||
this.submit_btn = frappe.ui.add('button', 'btn btn-outline-primary',
|
||||
this.body);
|
||||
this.submit_btn.setAttribute('type', 'submit');
|
||||
this.submit_btn.textContent = this.submit_label;
|
||||
this.submit_btn.addEventListener('click', (event) => {
|
||||
this.submit();
|
||||
event.preventDefault();
|
||||
})
|
||||
}
|
||||
|
||||
show_alert(message, type) {
|
||||
this.clear_alert();
|
||||
this.alert = frappe.ui.add('div', `alert alert-${type}`, this.body);
|
||||
this.alert.textContent = message;
|
||||
}
|
||||
|
||||
clear_alert() {
|
||||
if (this.alert) {
|
||||
frappe.ui.remove(this.alert);
|
||||
this.alert = null;
|
||||
}
|
||||
}
|
||||
|
||||
async use(doc, is_new = false) {
|
||||
if (this.doc) {
|
||||
// clear handlers of outgoing doc
|
||||
this.doc.clear_handlers();
|
||||
}
|
||||
this.clear_alert();
|
||||
this.doc = doc;
|
||||
this.is_new = is_new;
|
||||
for (let control of this.controls_list) {
|
||||
control.bind(this.doc);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.is_new) {
|
||||
await this.doc.insert();
|
||||
} else {
|
||||
await this.doc.update();
|
||||
}
|
||||
await this.refresh();
|
||||
this.show_alert('Saved', 'success');
|
||||
} catch (e) {
|
||||
this.show_alert('Failed', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
for(let control of this.controls_list) {
|
||||
control.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {Form: Form};
|
124
client/view/list.js
Normal file
124
client/view/list.js
Normal file
@ -0,0 +1,124 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class ListView {
|
||||
constructor({doctype, parent, fields}) {
|
||||
this.doctype = doctype;
|
||||
this.parent = parent;
|
||||
this.fields = fields;
|
||||
|
||||
this.meta = frappe.get_meta(this.doctype);
|
||||
|
||||
this.start = 0;
|
||||
this.page_length = 20;
|
||||
|
||||
this.body = null;
|
||||
this.rows = [];
|
||||
this.data = [];
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.make_body();
|
||||
this.set_filters();
|
||||
|
||||
let data = await this.meta.get_list({
|
||||
filters: this.filters,
|
||||
start:this.start,
|
||||
limit:this.page_length + 1
|
||||
});
|
||||
|
||||
for (let i=0; i< Math.min(this.page_length, data.length); i++) {
|
||||
this.render_row(this.start + i, data[i]);
|
||||
}
|
||||
|
||||
if (this.start > 0) {
|
||||
this.data = this.data.concat(data);
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
this.clear_empty_rows();
|
||||
this.update_more(data.length > this.page_length);
|
||||
}
|
||||
|
||||
async append() {
|
||||
this.start += this.page_length;
|
||||
await this.run();
|
||||
}
|
||||
|
||||
set_filters() {
|
||||
this.filters = {};
|
||||
if (this.search_input.value) {
|
||||
this.filters.keywords = ['like', '%' + this.search_input.value + '%'];
|
||||
}
|
||||
}
|
||||
|
||||
make_body() {
|
||||
if (!this.body) {
|
||||
this.make_search();
|
||||
this.body = frappe.ui.add('div', 'list-body', this.parent);
|
||||
this.make_more_btn();
|
||||
}
|
||||
}
|
||||
|
||||
make_search() {
|
||||
this.search_input_group = frappe.ui.add('div', 'input-group list-search', this.parent);
|
||||
|
||||
this.search_input = frappe.ui.add('input', 'form-control', this.search_input_group);
|
||||
this.search_input.addEventListener('keypress', (event) => {
|
||||
if (event.keyCode===13) {
|
||||
this.run();
|
||||
}
|
||||
});
|
||||
|
||||
this.search_input_group_append = frappe.ui.add('div', 'input-group-append', this.search_input_group);
|
||||
this.search_button = frappe.ui.add('button', 'btn btn-secondary', this.search_input_group_append);
|
||||
this.search_button.textContent = 'Search';
|
||||
this.search_button.addEventListener('click', (event) => {
|
||||
this.run();
|
||||
});
|
||||
}
|
||||
|
||||
make_more_btn() {
|
||||
this.more_btn = frappe.ui.add('button', 'btn btn-secondary hide', this.parent);
|
||||
this.more_btn.textContent = 'More';
|
||||
this.more_btn.addEventListener('click', () => {
|
||||
this.append();
|
||||
})
|
||||
}
|
||||
|
||||
render_row(i, data) {
|
||||
let row = this.get_row(i);
|
||||
row.innerHTML = this.meta.get_row_html(data);
|
||||
row.style.display = 'block';
|
||||
}
|
||||
|
||||
get_row(i) {
|
||||
if (!this.rows[i]) {
|
||||
this.rows[i] = frappe.ui.add('div', 'list-row', this.body);
|
||||
}
|
||||
return this.rows[i];
|
||||
}
|
||||
|
||||
clear_empty_rows() {
|
||||
if (this.rows.length > this.data.length) {
|
||||
for (let i=this.data.length; i < this.rows.length; i++) {
|
||||
let row = this.get_row(i);
|
||||
row.innerHTML = '';
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_more(show) {
|
||||
if (show) {
|
||||
this.more_btn.classList.remove('hide');
|
||||
} else {
|
||||
this.more_btn.classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ListView: ListView
|
||||
};
|
45
client/view/page.js
Normal file
45
client/view/page.js
Normal file
@ -0,0 +1,45 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Page {
|
||||
constructor(title) {
|
||||
this.handlers = {};
|
||||
this.title = title;
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.body = frappe.ui.add('div', 'page hide', frappe.desk.main);
|
||||
}
|
||||
|
||||
hide() {
|
||||
frappe.ui.add_class(this.body, 'hide');
|
||||
|
||||
this.trigger('hide');
|
||||
}
|
||||
|
||||
show(params) {
|
||||
if (frappe.router.current_page) {
|
||||
frappe.router.current_page.hide();
|
||||
}
|
||||
frappe.ui.remove_class(this.body, 'hide');
|
||||
frappe.router.current_page = this;
|
||||
document.title = this.title;
|
||||
|
||||
this.trigger('show', params);
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
if (!this.handlers[event]) this.handlers.event = [];
|
||||
this.handlers[event].push(fn);
|
||||
}
|
||||
|
||||
trigger(event, params) {
|
||||
if (this.handlers[event]) {
|
||||
for (let handler of this.handlers[event]) {
|
||||
handler(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Page: Page };
|
@ -1,6 +0,0 @@
|
||||
const server = require('frappe-core/frappe/server');
|
||||
|
||||
server.start({
|
||||
backend: 'sqllite',
|
||||
connection_params: {db_path: 'test.db'}
|
||||
});
|
@ -1,130 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
const path = require('path');
|
||||
|
||||
class RESTClient {
|
||||
constructor({server, protocol='http'}) {
|
||||
this.server = server;
|
||||
this.protocol = protocol;
|
||||
|
||||
this.init_type_map();
|
||||
|
||||
this.json_headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
||||
}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
doc.doctype = doctype;
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.json_headers,
|
||||
body: JSON.stringify(doc)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async get(doctype, name) {
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.json_headers
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async get_all({doctype, fields, filters, start, limit, sort_by, order}) {
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`);
|
||||
|
||||
url = url + "?" + this.get_query_string({
|
||||
fields: JSON.stringify(fields),
|
||||
filters: JSON.stringify(filters),
|
||||
start: start,
|
||||
limit: limit,
|
||||
sort_by: sort_by,
|
||||
order: order
|
||||
});
|
||||
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.json_headers
|
||||
});
|
||||
return await response.json();
|
||||
|
||||
}
|
||||
|
||||
async update(doctype, doc) {
|
||||
doc.doctype = doctype;
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${doc.name}`);
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: this.json_headers,
|
||||
body: JSON.stringify(doc)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`);
|
||||
|
||||
let response = await frappe.fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: this.json_headers
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
get_query_string(params) {
|
||||
return Object.keys(params)
|
||||
.map(k => params[k] != null ? encodeURIComponent(k) + '=' + encodeURIComponent(params[k]) : null)
|
||||
.filter(v => v)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
init_type_map() {
|
||||
this.type_map = {
|
||||
'Currency': true
|
||||
,'Int': true
|
||||
,'Float': true
|
||||
,'Percent': true
|
||||
,'Check': true
|
||||
,'Small Text': true
|
||||
,'Long Text': true
|
||||
,'Code': true
|
||||
,'Text Editor': true
|
||||
,'Date': true
|
||||
,'Datetime': true
|
||||
,'Time': true
|
||||
,'Text': true
|
||||
,'Data': true
|
||||
,'Link': true
|
||||
,'Dynamic Link':true
|
||||
,'Password': true
|
||||
,'Select': true
|
||||
,'Read Only': true
|
||||
,'Attach': true
|
||||
,'Attach Image':true
|
||||
,'Signature': true
|
||||
,'Color': true
|
||||
,'Barcode': true
|
||||
,'Geolocation': true
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Database: RESTClient
|
||||
}
|
@ -1,243 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
|
||||
class sqliteDatabase {
|
||||
constructor({db_path}) {
|
||||
this.db_path = db_path;
|
||||
this.init_type_map();
|
||||
}
|
||||
|
||||
connect(db_path) {
|
||||
if (db_path) {
|
||||
this.db_path = db_path;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
this.conn = new sqlite3.Database(this.db_path, () => {
|
||||
// debug
|
||||
// this.conn.on('trace', (trace) => console.log(trace));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
for (let doctype in frappe.models.data.doctype) {
|
||||
if (await this.table_exists(doctype)) {
|
||||
await this.alter_table(doctype);
|
||||
} else {
|
||||
await this.create_table(doctype);
|
||||
}
|
||||
}
|
||||
await this.commit();
|
||||
}
|
||||
|
||||
async create_table(doctype) {
|
||||
let meta = frappe.get_meta(doctype);
|
||||
let columns = [];
|
||||
let values = [];
|
||||
|
||||
for (let df of this.get_fields(meta)) {
|
||||
if (this.type_map[df.fieldtype]) {
|
||||
columns.push(`${df.fieldname} ${this.type_map[df.fieldtype]} ${df.reqd ? "not null" : ""} ${df.default ? "default ?" : ""}`);
|
||||
if (df.default) {
|
||||
values.push(df.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = `CREATE TABLE IF NOT EXISTS ${frappe.slug(doctype)} (
|
||||
${columns.join(", ")})`;
|
||||
|
||||
return await this.run(query);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.conn.close();
|
||||
}
|
||||
|
||||
async alter_table(doctype) {
|
||||
// add columns
|
||||
|
||||
// change columns
|
||||
|
||||
}
|
||||
|
||||
get(doctype, name, fields='*') {
|
||||
if (fields instanceof Array) {
|
||||
fields = fields.join(", ");
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.get(`select ${fields} from ${frappe.slug(doctype)}
|
||||
where name = ?`, name,
|
||||
(err, row) => {
|
||||
resolve(row || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async insert(doctype, doc) {
|
||||
let placeholders = Object.keys(doc).map(d => '?').join(', ');
|
||||
return await this.run(`insert into ${frappe.slug(doctype)}
|
||||
(${Object.keys(doc).join(", ")})
|
||||
values (${placeholders})`, this.get_formatted_values(doc));
|
||||
}
|
||||
|
||||
async update(doctype, doc) {
|
||||
let assigns = Object.keys(doc).map(key => `${key} = ?`);
|
||||
let values = this.get_formatted_values(doc);
|
||||
values.push(doc.name);
|
||||
|
||||
return await this.run(`update ${frappe.slug(doctype)}
|
||||
set ${assigns.join(", ")} where name=?`, values);
|
||||
}
|
||||
|
||||
get_formatted_values(doc) {
|
||||
return Object.values(doc).map(value => {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
return await this.run(`delete from ${frappe.slug(doctype)} where name=?`, name);
|
||||
}
|
||||
|
||||
get_all({doctype, fields=['name'], filters, start, limit, order_by='modified', order='desc'} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let conditions = this.get_filter_conditions(filters);
|
||||
|
||||
this.conn.all(`select ${fields.join(", ")}
|
||||
from ${frappe.slug(doctype)}
|
||||
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
||||
${order_by ? ("order by " + order_by) : ""} ${order_by ? (order || "asc") : ""}
|
||||
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`, conditions.values,
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get_filter_conditions(filters) {
|
||||
// {"status": "Open"} => `status = "Open"`
|
||||
// {"status": "Open", "name": ["like", "apple%"]}
|
||||
// => `status="Open" and name like "apple%"
|
||||
let conditions = [];
|
||||
let values = [];
|
||||
for (let key in filters) {
|
||||
const value = filters[key];
|
||||
if (value instanceof Array) {
|
||||
conditions.push(`${key} ${value[0]} ?`);
|
||||
} else {
|
||||
conditions.push(`${key} = ?`);
|
||||
}
|
||||
values.push(value);
|
||||
}
|
||||
return {
|
||||
conditions: conditions.length ? conditions.join(" and ") : "",
|
||||
values: values
|
||||
};
|
||||
}
|
||||
|
||||
run(query, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.run(query, params, (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sql(query, params) {
|
||||
return new Promise((resolve) => {
|
||||
this.conn.all(query, params, (err, rows) => {
|
||||
resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async commit() {
|
||||
try {
|
||||
await this.run('commit');
|
||||
} catch (e) {
|
||||
if (e.errno !== 1) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async get_value(doctype, filters, fieldname='name') {
|
||||
if (typeof filters==='string') {
|
||||
filters = {name: filters};
|
||||
}
|
||||
|
||||
let row = await this.get_all({
|
||||
doctype:doctype,
|
||||
fields: [fieldname],
|
||||
filters: filters,
|
||||
start: 0,
|
||||
limit: 1});
|
||||
return row.length ? row[0][fieldname] : null;
|
||||
}
|
||||
|
||||
get_fields(meta) {
|
||||
// add standard fields
|
||||
let fields = frappe.model.standard_fields.slice();
|
||||
if (meta.istable) {
|
||||
fields = fields.concat(frappe.model.child_fields);
|
||||
}
|
||||
|
||||
// add model fields
|
||||
fields = fields.concat(meta.fields);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
async table_exists(table) {
|
||||
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
|
||||
return (name && name.length) ? true : false;
|
||||
}
|
||||
|
||||
init_type_map() {
|
||||
this.type_map = {
|
||||
'Currency': 'real'
|
||||
,'Int': 'integer'
|
||||
,'Float': 'real'
|
||||
,'Percent': 'real'
|
||||
,'Check': 'integer'
|
||||
,'Small Text': 'text'
|
||||
,'Long Text': 'text'
|
||||
,'Code': 'text'
|
||||
,'Text Editor': 'text'
|
||||
,'Date': 'text'
|
||||
,'Datetime': 'text'
|
||||
,'Time': 'text'
|
||||
,'Text': 'text'
|
||||
,'Data': 'text'
|
||||
,'Link': 'text'
|
||||
,'Dynamic Link':'text'
|
||||
,'Password': 'text'
|
||||
,'Select': 'text'
|
||||
,'Read Only': 'text'
|
||||
,'Attach': 'text'
|
||||
,'Attach Image':'text'
|
||||
,'Signature': 'text'
|
||||
,'Color': 'text'
|
||||
,'Barcode': 'text'
|
||||
,'Geolocation': 'text'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Database: sqliteDatabase };
|
@ -1,22 +0,0 @@
|
||||
const common = require('frappe-core/frappe/common');
|
||||
const Database = require('frappe-core/frappe/backends/rest_client').Database;
|
||||
const frappe = require('frappe-core');
|
||||
frappe.ui = require('./ui');
|
||||
frappe.view = require('./view');
|
||||
const Router = require('./view/router').Router;
|
||||
|
||||
module.exports = {
|
||||
async start({server, container}) {
|
||||
window.frappe = frappe;
|
||||
frappe.init();
|
||||
common.init_libs(frappe);
|
||||
|
||||
frappe.fetch = window.fetch.bind();
|
||||
frappe.db = await new Database({server: server});
|
||||
|
||||
frappe.view.init({container: container});
|
||||
frappe.router = new Router();
|
||||
await frappe.login();
|
||||
}
|
||||
};
|
||||
|
@ -1,89 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Dropdown {
|
||||
constructor({parent, label, btn_class = 'btn-secondary', items = []}) {
|
||||
Object.assign(this, arguments[0]);
|
||||
|
||||
this.dropdown_items = [];
|
||||
this.setup_background_click();
|
||||
this.make();
|
||||
|
||||
// init items
|
||||
if (this.items) {
|
||||
for (item of this.items) {
|
||||
this.add_item(item.label, item.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup_background_click() {
|
||||
if (!document.dropdown_setup) {
|
||||
frappe.dropdowns = [];
|
||||
// setup hiding all dropdowns on click
|
||||
document.addEventListener('click', (event) => {
|
||||
for (let d of frappe.dropdowns) {
|
||||
if (d.button !== event.target) {
|
||||
d.collapse();
|
||||
}
|
||||
}
|
||||
});
|
||||
document.dropdown_setup = true;
|
||||
}
|
||||
frappe.dropdowns.push(this);
|
||||
}
|
||||
|
||||
make() {
|
||||
this.dropdown = frappe.ui.add('div', 'dropdown', this.parent);
|
||||
this.make_button();
|
||||
this.dropdown_menu = frappe.ui.add('div', 'dropdown-menu', this.dropdown);
|
||||
}
|
||||
|
||||
make_button() {
|
||||
this.button = frappe.ui.add('button', 'btn ' + this.btn_class,
|
||||
this.dropdown);
|
||||
frappe.ui.add_class(this.button, 'dropdown-toggle');
|
||||
this.button.textContent = this.label;
|
||||
this.button.addEventListener('click', () => {
|
||||
this.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
expand() {
|
||||
this.dropdown.classList.add('show');
|
||||
this.dropdown_menu.classList.add('show');
|
||||
}
|
||||
|
||||
collapse() {
|
||||
this.dropdown.classList.remove('show');
|
||||
this.dropdown_menu.classList.remove('show');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.dropdown.classList.toggle('show');
|
||||
this.dropdown_menu.classList.toggle('show');
|
||||
}
|
||||
|
||||
add_item(label, action) {
|
||||
let item = frappe.ui.add('a', 'dropdown-item', this.dropdown_menu);
|
||||
item.textContent = label;
|
||||
if (typeof action === 'string') {
|
||||
item.src = action;
|
||||
item.addEventListener('click', () => {
|
||||
this.toggle();
|
||||
});
|
||||
} else {
|
||||
item.addEventListener('click', async () => {
|
||||
await action();
|
||||
this.toggle();
|
||||
});
|
||||
}
|
||||
this.dropdown_items.push(item);
|
||||
}
|
||||
|
||||
float_right() {
|
||||
frappe.ui.add_class(this.dropdown, 'float-right');
|
||||
frappe.ui.add_class(this.dropdown_menu, 'dropdown-menu-right');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dropdown;
|
@ -1,46 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
const Dropdown = require('./dropdown');
|
||||
|
||||
module.exports = {
|
||||
add(tag, className, parent) {
|
||||
let element = document.createElement(tag);
|
||||
if (className) {
|
||||
for (let c of className.split(' ')) {
|
||||
this.add_class(element, c);
|
||||
}
|
||||
}
|
||||
if (parent) {
|
||||
parent.appendChild(element);
|
||||
}
|
||||
return element;
|
||||
},
|
||||
|
||||
remove(element) {
|
||||
element.parentNode.removeChild(element);
|
||||
},
|
||||
|
||||
add_class(element, className) {
|
||||
if (element.classList) {
|
||||
element.classList.add(className);
|
||||
} else {
|
||||
element.className += " " + className;
|
||||
}
|
||||
},
|
||||
|
||||
remove_class(element, className) {
|
||||
if (element.classList) {
|
||||
element.classList.remove(className);
|
||||
} else {
|
||||
element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
||||
}
|
||||
},
|
||||
|
||||
toggle(element, default_display = '') {
|
||||
element.style.display = element.style.display === 'none' ? default_display : 'none';
|
||||
},
|
||||
|
||||
make_dropdown(label, parent, btn_class = 'btn-secondary') {
|
||||
return new Dropdown({parent: parent, label:label, btn_class:btn_class});
|
||||
}
|
||||
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class BaseControl {
|
||||
constructor(docfield, parent) {
|
||||
Object.assign(this, docfield);
|
||||
if (!this.fieldname) {
|
||||
this.fieldname = frappe.slug(this.label);
|
||||
}
|
||||
this.parent = parent;
|
||||
if (this.setup) {
|
||||
this.setup();
|
||||
}
|
||||
}
|
||||
|
||||
bind(doc) {
|
||||
this.doc = doc;
|
||||
|
||||
this.doc.add_handler(this.fieldname, () => {
|
||||
this.set_doc_value();
|
||||
});
|
||||
|
||||
this.set_doc_value();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.make();
|
||||
this.set_doc_value();
|
||||
}
|
||||
|
||||
set_doc_value() {
|
||||
if (this.doc) {
|
||||
this.set_input_value(this.doc.get(this.fieldname));
|
||||
}
|
||||
}
|
||||
|
||||
make() {
|
||||
if (!this.form_group) {
|
||||
this.make_form_group();
|
||||
this.make_label();
|
||||
this.make_input();
|
||||
this.set_input_name();
|
||||
this.make_description();
|
||||
this.bind_change_event();
|
||||
}
|
||||
}
|
||||
|
||||
make_form_group() {
|
||||
this.form_group = frappe.ui.add('div', 'form-group', this.parent);
|
||||
}
|
||||
|
||||
make_label() {
|
||||
this.label_element = frappe.ui.add('label', null, this.form_group);
|
||||
this.label_element.textContent = this.label;
|
||||
}
|
||||
|
||||
make_input() {
|
||||
this.input = frappe.ui.add('input', 'form-control', this.form_group);
|
||||
}
|
||||
|
||||
set_input_name() {
|
||||
this.input.setAttribute('name', this.fieldname);
|
||||
}
|
||||
|
||||
make_description() {
|
||||
if (this.description) {
|
||||
this.description_element = frappe.ui.add('small', 'form-text text-muted', this.form_group);
|
||||
this.description_element.textContent = this.description;
|
||||
}
|
||||
}
|
||||
|
||||
set_input_value(value) {
|
||||
if (value === undefined || value === null) {
|
||||
value = '';
|
||||
}
|
||||
this.input.value = value;
|
||||
}
|
||||
|
||||
async get_input_value() {
|
||||
return await this.parse(this.input.value);
|
||||
}
|
||||
|
||||
async parse(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
async validate(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
bind_change_event() {
|
||||
this.input.addEventListener('change', (e) => this.handle_change(e));
|
||||
}
|
||||
|
||||
async handle_change(e) {
|
||||
let value = await this.get_input_value();
|
||||
value = await this.validate(value);
|
||||
await this.doc.set(this.fieldname, value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseControl;
|
@ -1,18 +0,0 @@
|
||||
const control_classes = {
|
||||
Data: require('./data'),
|
||||
Text: require('./text'),
|
||||
Select: require('./select')
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
get_control_class(fieldtype) {
|
||||
return control_classes[fieldtype];
|
||||
},
|
||||
make_control(field, parent) {
|
||||
const control_class = this.get_control_class(field.fieldtype);
|
||||
let control = new control_class(field, parent);
|
||||
control.make();
|
||||
return control;
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class SelectControl extends BaseControl {
|
||||
make_input() {
|
||||
this.input = frappe.ui.add('select', 'form-control', this.form_group);
|
||||
|
||||
let options = this.options;
|
||||
if (typeof options==='string') {
|
||||
options = options.split('\n');
|
||||
}
|
||||
|
||||
for (let value of options) {
|
||||
let option = frappe.ui.add('option', null, this.input);
|
||||
option.textContent = value;
|
||||
option.setAttribute('value', value);
|
||||
}
|
||||
}
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('row', '3');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SelectControl;
|
@ -1,13 +0,0 @@
|
||||
const BaseControl = require('./base');
|
||||
|
||||
class TextControl extends BaseControl {
|
||||
make_input() {
|
||||
this.input = frappe.ui.add('textarea', 'form-control', this.form_group);
|
||||
}
|
||||
make() {
|
||||
super.make();
|
||||
this.input.setAttribute('rows', '8');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TextControl;
|
@ -1,108 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
const controls = require('./controls');
|
||||
|
||||
class Form {
|
||||
constructor({doctype, parent, submit_label='Submit'}) {
|
||||
this.parent = parent;
|
||||
this.doctype = doctype;
|
||||
this.submit_label = submit_label;
|
||||
|
||||
this.controls = {};
|
||||
this.controls_list = [];
|
||||
|
||||
this.meta = frappe.get_meta(this.doctype);
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
if (this.body || !this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.body = frappe.ui.add('div', 'form-body', this.parent);
|
||||
this.make_actions();
|
||||
|
||||
this.form = frappe.ui.add('form', null, this.body);
|
||||
for(let df of this.meta.fields) {
|
||||
if (controls.get_control_class(df.fieldtype)) {
|
||||
let control = controls.make_control(df, this.form);
|
||||
this.controls_list.push(control);
|
||||
this.controls[df.fieldname] = control;
|
||||
}
|
||||
}
|
||||
this.make_submit();
|
||||
}
|
||||
|
||||
make_actions() {
|
||||
this.toolbar = frappe.ui.add('div', 'form-toolbar', this.body);
|
||||
this.actions = frappe.ui.make_dropdown('Actions', this.toolbar);
|
||||
|
||||
// delete
|
||||
this.actions.add_item('Delete', async () => {
|
||||
await this.doc.delete();
|
||||
this.show_alert('Deleted', 'success');
|
||||
});
|
||||
|
||||
this.actions.float_right();
|
||||
}
|
||||
|
||||
make_submit() {
|
||||
this.submit_btn = frappe.ui.add('button', 'btn btn-outline-primary',
|
||||
this.body);
|
||||
this.submit_btn.setAttribute('type', 'submit');
|
||||
this.submit_btn.textContent = this.submit_label;
|
||||
this.submit_btn.addEventListener('click', (event) => {
|
||||
this.submit();
|
||||
event.preventDefault();
|
||||
})
|
||||
}
|
||||
|
||||
show_alert(message, type) {
|
||||
this.clear_alert();
|
||||
this.alert = frappe.ui.add('div', `alert alert-${type}`, this.body);
|
||||
this.alert.textContent = message;
|
||||
}
|
||||
|
||||
clear_alert() {
|
||||
if (this.alert) {
|
||||
frappe.ui.remove(this.alert);
|
||||
this.alert = null;
|
||||
}
|
||||
}
|
||||
|
||||
async use(doc, is_new = false) {
|
||||
if (this.doc) {
|
||||
// clear handlers of outgoing doc
|
||||
this.doc.clear_handlers();
|
||||
}
|
||||
this.clear_alert();
|
||||
this.doc = doc;
|
||||
this.is_new = is_new;
|
||||
for (let control of this.controls_list) {
|
||||
control.bind(this.doc);
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (this.is_new) {
|
||||
await this.doc.insert();
|
||||
} else {
|
||||
await this.doc.update();
|
||||
}
|
||||
await this.refresh();
|
||||
this.show_alert('Saved', 'success');
|
||||
} catch (e) {
|
||||
this.show_alert('Failed', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
for(let control of this.controls_list) {
|
||||
control.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {Form: Form};
|
@ -1,20 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
module.exports = {
|
||||
init({container, main, sidebar}) {
|
||||
frappe.container = container;
|
||||
|
||||
if (sidebar) {
|
||||
frappe.sidebar = sidebar;
|
||||
} else {
|
||||
frappe.sidebar = frappe.ui.add('div', 'sidebar', frappe.container);
|
||||
}
|
||||
|
||||
if (main) {
|
||||
frappe.main = main;
|
||||
} else {
|
||||
frappe.main = frappe.ui.add('div', 'main', frappe.container);
|
||||
}
|
||||
},
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class ListView {
|
||||
constructor({doctype, parent, fields}) {
|
||||
this.doctype = doctype;
|
||||
this.parent = parent;
|
||||
this.fields = fields;
|
||||
|
||||
this.meta = frappe.get_meta(this.doctype);
|
||||
|
||||
this.start = 0;
|
||||
this.page_length = 20;
|
||||
|
||||
this.body = null;
|
||||
this.rows = [];
|
||||
}
|
||||
|
||||
async run() {
|
||||
this.make_body();
|
||||
let data = await this.meta.get_list({
|
||||
start:this.start,
|
||||
limit:this.page_length
|
||||
});
|
||||
|
||||
for (let i=0; i< data.length; i++) {
|
||||
this.render_row(this.start + i, data[i]);
|
||||
}
|
||||
|
||||
this.clear_empty_rows(data.length);
|
||||
}
|
||||
|
||||
make_body() {
|
||||
if (!this.body) {
|
||||
this.body = frappe.ui.add('div', 'list-body', this.parent);
|
||||
}
|
||||
}
|
||||
|
||||
render_row(i, data) {
|
||||
let row = this.get_row(i);
|
||||
row.innerHTML = this.meta.get_row_html(data);
|
||||
}
|
||||
|
||||
get_row(i) {
|
||||
if (!this.rows[i]) {
|
||||
this.rows[i] = frappe.ui.add('div', 'list-row', this.body);
|
||||
}
|
||||
return this.rows[i];
|
||||
}
|
||||
|
||||
clear_empty_rows(start) {
|
||||
if (this.rows.length > start) {
|
||||
for (let i=start; i < this.rows.length; i++) {
|
||||
let row = this.get_row(i);
|
||||
row.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ListView: ListView
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Page {
|
||||
constructor(title) {
|
||||
this.handlers = {};
|
||||
this.title = title;
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.body = frappe.ui.add('div', 'page hide', frappe.main);
|
||||
}
|
||||
|
||||
hide() {
|
||||
frappe.ui.add_class(this.body, 'hide');
|
||||
|
||||
this.trigger('hide');
|
||||
}
|
||||
|
||||
show(params) {
|
||||
if (frappe.router.current_page) {
|
||||
frappe.router.current_page.hide();
|
||||
}
|
||||
frappe.ui.remove_class(this.body, 'hide');
|
||||
frappe.router.current_page = this;
|
||||
document.title = this.title;
|
||||
|
||||
this.trigger('show', params);
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
if (!this.handlers[event]) this.handlers.event = [];
|
||||
this.handlers[event].push(fn);
|
||||
}
|
||||
|
||||
trigger(event, params) {
|
||||
if (this.handlers[event]) {
|
||||
for (let handler of this.handlers[event]) {
|
||||
handler(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Page: Page };
|
@ -1,82 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Router {
|
||||
constructor() {
|
||||
this.current_page = null;
|
||||
this.routes = {};
|
||||
this.listen();
|
||||
}
|
||||
|
||||
add(route, handler) {
|
||||
let page = {handler: handler};
|
||||
|
||||
// '/todo/:name/:place'.match(/:([^/]+)/g);
|
||||
page.param_keys = route.match(/:([^/]+)/g);
|
||||
|
||||
if (page.param_keys) {
|
||||
// make expression
|
||||
// '/todo/:name/:place'.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
||||
page.expression = route.replace(/\/:([a-z1-9]+)/g, "\/([a-z0-9]+)");
|
||||
}
|
||||
|
||||
this.routes[route] = page;
|
||||
}
|
||||
|
||||
listen() {
|
||||
window.onhashchange = this.changed.bind(this);
|
||||
this.changed();
|
||||
}
|
||||
|
||||
async changed(event) {
|
||||
if (window.location.hash.length > 0) {
|
||||
const page_name = window.location.hash.substr(1);
|
||||
this.show(page_name);
|
||||
} else if (this.routes['default']) {
|
||||
this.show('default');
|
||||
}
|
||||
}
|
||||
|
||||
show(route) {
|
||||
if (!route) {
|
||||
route = 'default';
|
||||
}
|
||||
|
||||
if (route[0]==='#') {
|
||||
route = route.substr(1);
|
||||
}
|
||||
|
||||
let page = this.match(route);
|
||||
|
||||
if (page) {
|
||||
if (typeof page.handler==='function') {
|
||||
page.handler(page.params);
|
||||
} else {
|
||||
page.handler.show(page.params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match(route) {
|
||||
for(let key in this.routes) {
|
||||
let page = this.routes[key];
|
||||
|
||||
if (page.param_keys) {
|
||||
let matches = route.match(new RegExp(page.expression));
|
||||
if (matches && matches.length == page.param_keys.length + 1) {
|
||||
let params = {}
|
||||
for (let i=0; i < page.param_keys.length; i++) {
|
||||
params[page.param_keys[i].substr(1)] = matches[i + 1];
|
||||
}
|
||||
return {handler:page.handler, params: params};
|
||||
}
|
||||
|
||||
} else {
|
||||
if (key === route) {
|
||||
return {handler:page.handler};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Router: Router};
|
@ -1,69 +0,0 @@
|
||||
module.exports = {
|
||||
async init() {
|
||||
if (this._initialized) return;
|
||||
this.init_config();
|
||||
this.init_errors();
|
||||
this.init_globals();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
init_config() {
|
||||
this.config = {
|
||||
backend: 'sqlite',
|
||||
port: 8000
|
||||
};
|
||||
},
|
||||
|
||||
init_errors() {
|
||||
this.ValueError = class extends Error { };
|
||||
},
|
||||
|
||||
init_globals() {
|
||||
this.meta_cache = {};
|
||||
},
|
||||
|
||||
get_meta(doctype) {
|
||||
if (!this.meta_cache[doctype]) {
|
||||
this.meta_cache[doctype] = new (this.models.get_meta_class(doctype))(this.models.get('DocType', doctype));
|
||||
}
|
||||
return this.meta_cache[doctype];
|
||||
},
|
||||
|
||||
init_controller(doctype, module) {
|
||||
doctype = this.slug(doctype);
|
||||
this.models.controllers[doctype] = module[doctype];
|
||||
this.models.meta_classes[doctype] = module[doctype + '_meta'];
|
||||
},
|
||||
|
||||
async get_doc(data, name) {
|
||||
if (typeof data==='string' && typeof name==='string') {
|
||||
let controller_class = this.models.get_controller(data);
|
||||
var doc = new controller_class({doctype:data, name: name});
|
||||
await doc.load();
|
||||
} else {
|
||||
let controller_class = this.models.get_controller(data.doctype);
|
||||
var doc = new controller_class(data);
|
||||
}
|
||||
return doc;
|
||||
},
|
||||
|
||||
async insert(data) {
|
||||
const doc = await this.get_doc(data);
|
||||
return await doc.insert();
|
||||
},
|
||||
|
||||
login(user='guest', user_key) {
|
||||
this.session = new this._session.Session(user);
|
||||
if (user && user_key) {
|
||||
this.authenticate(user_key);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
};
|
@ -1,133 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Document {
|
||||
constructor(data) {
|
||||
this.handlers = {};
|
||||
this.setup();
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
setup() {
|
||||
// add handlers
|
||||
}
|
||||
|
||||
clear_handlers() {
|
||||
this.handlers = {};
|
||||
}
|
||||
|
||||
add_handler(key, method) {
|
||||
if (!this.handlers[key]) {
|
||||
this.handlers[key] = [];
|
||||
}
|
||||
this.handlers[key].push(method || key);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this[key];
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.validate_field(key, value);
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
set_name() {
|
||||
// assign a random name by default
|
||||
// override this to set a name
|
||||
if (!this.name) {
|
||||
this.name = Math.random().toString(36).substr(3);
|
||||
}
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (!this._meta) {
|
||||
this._meta = frappe.get_meta(this.doctype);
|
||||
}
|
||||
return this._meta;
|
||||
}
|
||||
|
||||
append(key, document) {
|
||||
if (!this[key]) {
|
||||
this[key] = [];
|
||||
}
|
||||
this[key].push(this.init_doc(document));
|
||||
}
|
||||
|
||||
init_doc(data) {
|
||||
if (data.prototype instanceof Document) {
|
||||
return data;
|
||||
} else {
|
||||
return new Document(d);
|
||||
}
|
||||
}
|
||||
|
||||
validate_field (key, value) {
|
||||
let df = this.meta.get_field(key);
|
||||
if (df.fieldtype=='Select') {
|
||||
this.meta.validate_select(df, value);
|
||||
}
|
||||
}
|
||||
|
||||
get_valid_dict() {
|
||||
let doc = {};
|
||||
for(let df of this.meta.get_valid_fields()) {
|
||||
doc[df.fieldname] = this.get(df.fieldname);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
set_standard_values() {
|
||||
let now = new Date();
|
||||
if (this.docstatus === null || this.docstatus === undefined) {
|
||||
this.docstatus = 0;
|
||||
}
|
||||
if (!this.owner) {
|
||||
this.owner = frappe.session.user;
|
||||
this.creation = now;
|
||||
}
|
||||
this.modified_by = frappe.session.user;
|
||||
this.modified = now;
|
||||
}
|
||||
|
||||
async load() {
|
||||
Object.assign(this, await frappe.db.get(this.doctype, this.name));
|
||||
}
|
||||
|
||||
async insert() {
|
||||
this.set_name();
|
||||
this.set_standard_values();
|
||||
await this.trigger('validate', 'before_insert');
|
||||
await frappe.db.insert(this.doctype, this.get_valid_dict());
|
||||
await this.trigger('after_insert', 'after_save');
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.trigger('before_delete');
|
||||
await frappe.db.delete(this.doctype, this.name);
|
||||
await this.trigger('after_delete');
|
||||
}
|
||||
|
||||
async trigger() {
|
||||
for(var key of arguments) {
|
||||
if (this.handlers[key]) {
|
||||
for (let method of this.handlers[key]) {
|
||||
if (typeof method === 'string') {
|
||||
await this[method]();
|
||||
} else {
|
||||
await method(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
this.set_standard_values();
|
||||
await this.trigger('validate', 'before_update');
|
||||
await frappe.db.update(this.doctype, this.get_valid_dict());
|
||||
await this.trigger('after_update', 'after_save');
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { Document: Document };
|
@ -1,36 +0,0 @@
|
||||
module.exports = {
|
||||
standard_fields: [
|
||||
{
|
||||
fieldname: 'name', fieldtype: 'Data', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'owner', fieldtype: 'Link', reqd: 1, options: 'User'
|
||||
},
|
||||
{
|
||||
fieldname: 'modified_by', fieldtype: 'Link', reqd: 1, options: 'User'
|
||||
},
|
||||
{
|
||||
fieldname: 'creation', fieldtype: 'Datetime', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'modified', fieldtype: 'Datetime', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'docstatus', fieldtype: 'Int', reqd: 1, default: 0
|
||||
}
|
||||
],
|
||||
child_fields: [
|
||||
{
|
||||
fieldname: 'idx', fieldtype: 'Int', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parent', fieldtype: 'Data', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parenttype', fieldtype: 'Link', reqd: 1, options: 'DocType'
|
||||
},
|
||||
{
|
||||
fieldname: 'parentfield', fieldtype: 'Data', reqd: 1
|
||||
}
|
||||
]
|
||||
};
|
@ -1,111 +0,0 @@
|
||||
const Document = require('./document').Document;
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Meta extends Document {
|
||||
constructor(data) {
|
||||
super(data);
|
||||
this.event_handlers = {};
|
||||
this.list_options = {
|
||||
fields: ['name', 'modified']
|
||||
};
|
||||
if (this.setup_meta) {
|
||||
this.setup_meta();
|
||||
}
|
||||
}
|
||||
|
||||
get_field(fieldname) {
|
||||
if (!this.field_map) {
|
||||
this.field_map = {};
|
||||
for (let df of this.fields) {
|
||||
this.field_map[df.fieldname] = df;
|
||||
}
|
||||
}
|
||||
return this.field_map[fieldname];
|
||||
}
|
||||
|
||||
on(key, fn) {
|
||||
if (!this.event_handlers[key]) {
|
||||
this.event_handlers[key] = [];
|
||||
}
|
||||
this.event_handlers[key].push(fn);
|
||||
}
|
||||
|
||||
async set(fieldname, value) {
|
||||
this[fieldname] = value;
|
||||
await this.trigger(fieldname);
|
||||
}
|
||||
|
||||
get(fieldname) {
|
||||
return this[fieldname];
|
||||
}
|
||||
|
||||
get_valid_fields() {
|
||||
if (!this._valid_fields) {
|
||||
this._valid_fields = [];
|
||||
|
||||
// standard fields
|
||||
for (let df of frappe.model.standard_fields) {
|
||||
this._valid_fields.push(df);
|
||||
}
|
||||
|
||||
// parent fields
|
||||
if (this.istable) {
|
||||
for (let df of frappe.model.child_fields) {
|
||||
this._valid_fields.push(df);
|
||||
}
|
||||
}
|
||||
|
||||
// doctype fields
|
||||
for (let df of this.fields) {
|
||||
if (frappe.db.type_map[df.fieldtype]) {
|
||||
this._valid_fields.push(df);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._valid_fields;
|
||||
}
|
||||
|
||||
validate_select(df, value) {
|
||||
let options = df.options;
|
||||
if (typeof options === 'string') {
|
||||
// values given as string
|
||||
options = df.options.split('\n');
|
||||
}
|
||||
if (!options.includes(value)) {
|
||||
throw new frappe.ValueError(`${value} must be one of ${options.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
async trigger(key, event = {}) {
|
||||
|
||||
Object.assign(event, {
|
||||
doc: this,
|
||||
name: key
|
||||
});
|
||||
|
||||
if (this.event_handlers[key]) {
|
||||
for (var handler of this.event_handlers[key]) {
|
||||
await handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collections
|
||||
async get_list({start, limit=20, filters}) {
|
||||
return await frappe.db.get_all({
|
||||
doctype: this.name,
|
||||
fields: this.list_options.fields,
|
||||
filters: filters,
|
||||
start: start,
|
||||
limit: limit
|
||||
});
|
||||
}
|
||||
|
||||
get_row_html(data) {
|
||||
return `<a href="/view/${this.name}/${data.name}">${data.name}</a>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Meta: Meta }
|
@ -1,25 +0,0 @@
|
||||
const process = require('process');
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Models {
|
||||
constructor() {
|
||||
this.data = {doctype: {}};
|
||||
this.controllers = {};
|
||||
this.meta_classes = {};
|
||||
}
|
||||
|
||||
get(doctype, name) {
|
||||
return this.data[frappe.slug(doctype)][frappe.slug(name)];
|
||||
}
|
||||
|
||||
get_controller(doctype) {
|
||||
return this.controllers[frappe.slug(doctype)];
|
||||
}
|
||||
|
||||
get_meta_class(doctype) {
|
||||
return this.meta_classes[frappe.slug(doctype)] || frappe.meta.Meta;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Models: Models }
|
@ -1,30 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class todo_meta extends frappe.meta.Meta {
|
||||
setup_meta() {
|
||||
Object.assign(this, require('./todo.json'));
|
||||
this.name = 'ToDo';
|
||||
this.list_options.fields = ['name', 'subject', 'status', 'description'];
|
||||
}
|
||||
|
||||
get_row_html(data) {
|
||||
return `<a href="#edit/todo/${data.name}">${data.subject}</a>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class todo extends frappe.document.Document {
|
||||
setup() {
|
||||
this.add_handler('validate');
|
||||
}
|
||||
validate() {
|
||||
if (!this.status) {
|
||||
this.status = 'Open';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
todo: todo,
|
||||
todo_meta: todo_meta
|
||||
};
|
@ -1,52 +0,0 @@
|
||||
const backends = {};
|
||||
backends.sqlite = require('frappe-core/frappe/backends/sqlite');
|
||||
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const frappe = require('frappe-core');
|
||||
const rest_api = require('./rest_api')
|
||||
const models = require('frappe-core/frappe/server/models');
|
||||
const common = require('frappe-core/frappe/common');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
module.exports = {
|
||||
async init() {
|
||||
await frappe.init();
|
||||
common.init_libs(frappe);
|
||||
await frappe.login();
|
||||
|
||||
// walk and find models
|
||||
models.init();
|
||||
|
||||
},
|
||||
|
||||
async init_db({backend, connection_params}) {
|
||||
frappe.db = await new backends[backend].Database(connection_params);
|
||||
await frappe.db.connect();
|
||||
await frappe.db.migrate();
|
||||
},
|
||||
|
||||
async start({backend, connection_params, static}) {
|
||||
await this.init();
|
||||
await this.init_db({backend:backend, connection_params:connection_params});
|
||||
// database
|
||||
|
||||
// app
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static('./'));
|
||||
|
||||
app.use(function (err, req, res, next) {
|
||||
console.error(err.stack);
|
||||
res.status(500).send('Something broke!');
|
||||
})
|
||||
// routes
|
||||
rest_api.setup(app);
|
||||
|
||||
// listen
|
||||
frappe.app = app;
|
||||
frappe.server = app.listen(frappe.config.port);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,54 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
const walk = require('walk');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
init() {
|
||||
const cwd = process.cwd();
|
||||
|
||||
const setup_model = (doctype, name, file_path) => {
|
||||
// add to frappe.models.data
|
||||
if (!frappe.models[doctype]) {
|
||||
frappe.models[doctype] = {};
|
||||
}
|
||||
frappe.models.data[doctype][name] = require(file_path);
|
||||
}
|
||||
|
||||
const setup_controller = (doctype, file_path) => {
|
||||
let _module = require(file_path);
|
||||
frappe.models.controllers[doctype] = _module[doctype];
|
||||
if (_module[doctype + '_meta']) {
|
||||
frappe.models.meta_classes[doctype] = _module[doctype + '_meta'];
|
||||
}
|
||||
}
|
||||
|
||||
// init for all apps
|
||||
if (!frappe.config.apps) {
|
||||
frappe.config.apps = [];
|
||||
}
|
||||
|
||||
frappe.config.apps.unshift('frappe-core');
|
||||
|
||||
// walk and sync
|
||||
for (let app_name of frappe.config.apps) {
|
||||
let start = path.resolve(require.resolve(app_name), '../models');
|
||||
walk.walkSync(start, {
|
||||
listeners: {
|
||||
file: (basepath, file_data, next) => {
|
||||
const doctype = path.basename(path.dirname(basepath));
|
||||
const name = path.basename(basepath);
|
||||
const file_path = path.resolve(basepath, file_data.name);
|
||||
if (file_data.name.endsWith('.json')) {
|
||||
setup_model(doctype, name, file_path)
|
||||
}
|
||||
if (doctype==='doctype' && file_data.name.endsWith('.js')) {
|
||||
setup_controller(path.basename(basepath), file_path);
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
module.exports = {
|
||||
setup(app) {
|
||||
// get list
|
||||
app.get('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
||||
let fields, filters;
|
||||
for (key of ['fields', 'filters']) {
|
||||
if (request.query[key]) {
|
||||
request.query[key] = JSON.parse(request.query[key]);
|
||||
}
|
||||
}
|
||||
|
||||
let data = await frappe.db.get_all({
|
||||
doctype: request.params.doctype,
|
||||
fields: request.query.fields || ['name', 'subject'],
|
||||
filters: request.query.filters,
|
||||
start: request.query.start || 0,
|
||||
limit: request.query.limit || 20,
|
||||
order_by: request.query.order_by,
|
||||
order: request.query.order
|
||||
});
|
||||
|
||||
return response.json(data);
|
||||
}));
|
||||
|
||||
// create
|
||||
app.post('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
||||
data = request.body;
|
||||
data.doctype = request.params.doctype;
|
||||
let doc = await frappe.get_doc(data);
|
||||
await doc.insert();
|
||||
await frappe.db.commit();
|
||||
return response.json(doc.get_valid_dict());
|
||||
}));
|
||||
|
||||
// update
|
||||
app.put('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||
data = request.body;
|
||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
||||
Object.assign(doc, data);
|
||||
await doc.update();
|
||||
await frappe.db.commit();
|
||||
return response.json(doc.get_valid_dict());
|
||||
}));
|
||||
|
||||
|
||||
// get document
|
||||
app.get('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
||||
return response.json(doc.get_valid_dict());
|
||||
}));
|
||||
|
||||
// delete
|
||||
app.delete('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name)
|
||||
await doc.delete();
|
||||
return response.json({});
|
||||
}));
|
||||
}
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Session {
|
||||
constructor(user, user_key) {
|
||||
this.user = user || 'guest';
|
||||
if (this.user !== 'guest') {
|
||||
this.login(user_key);
|
||||
}
|
||||
}
|
||||
|
||||
login(user_key) {
|
||||
// could be password, sessionid, otp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Session: Session };
|
@ -1,11 +0,0 @@
|
||||
const server = require('frappe-core/frappe/server');
|
||||
|
||||
module.exports = {
|
||||
async init_sqlite() {
|
||||
server.init()
|
||||
server.init_db({
|
||||
backend: 'sqlite',
|
||||
connection_params: {db_path: 'test.db'}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Controller', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should call controller method', async () => {
|
||||
let doc = await frappe.get_doc({
|
||||
doctype:'ToDo',
|
||||
subject: 'test'
|
||||
});
|
||||
doc.trigger('validate');
|
||||
assert.equal(doc.status, 'Open');
|
||||
});
|
||||
});
|
@ -1,49 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Database', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should insert and get values', async () => {
|
||||
await frappe.db.sql('delete from todo');
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 1'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 3'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 2'});
|
||||
|
||||
let subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject']})
|
||||
subjects = subjects.map(d => d.subject);
|
||||
|
||||
assert.ok(subjects.includes('testing 1'));
|
||||
assert.ok(subjects.includes('testing 2'));
|
||||
assert.ok(subjects.includes('testing 3'));
|
||||
});
|
||||
|
||||
it('should filter correct values', async () => {
|
||||
let subjects = null;
|
||||
|
||||
await frappe.db.sql('delete from todo');
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 1', status: 'Open'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 3', status: 'Open'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 2', status: 'Closed'});
|
||||
|
||||
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
||||
filters:{status: 'Open'}});
|
||||
subjects = subjects.map(d => d.subject);
|
||||
|
||||
assert.ok(subjects.includes('testing 1'));
|
||||
assert.ok(subjects.includes('testing 3'));
|
||||
assert.equal(subjects.includes('testing 2'), false);
|
||||
|
||||
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
||||
filters:{status: 'Closed'}});
|
||||
subjects = subjects.map(d => d.subject);
|
||||
|
||||
assert.equal(subjects.includes('testing 1'), false);
|
||||
assert.equal(subjects.includes('testing 3'), false);
|
||||
assert.ok(subjects.includes('testing 2'));
|
||||
|
||||
});
|
||||
});
|
@ -1,75 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Document', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should insert a doc', async () => {
|
||||
let doc1 = await test_doc();
|
||||
doc1.subject = 'insert subject 1';
|
||||
doc1.description = 'insert description 1';
|
||||
await doc1.insert();
|
||||
|
||||
// get it back from the db
|
||||
let doc2 = await frappe.get_doc(doc1.doctype, doc1.name);
|
||||
assert.equal(doc1.subject, doc2.subject);
|
||||
assert.equal(doc1.description, doc2.description);
|
||||
});
|
||||
|
||||
it('should update a doc', async () => {
|
||||
let doc = await test_doc();
|
||||
await doc.insert();
|
||||
|
||||
assert.notEqual(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
||||
|
||||
doc.subject = 'subject 2'
|
||||
await doc.update();
|
||||
|
||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
||||
})
|
||||
|
||||
it('should get a value', async () => {
|
||||
let doc = await test_doc();
|
||||
assert.equal(doc.get('subject'), 'testing 1');
|
||||
});
|
||||
|
||||
it('should set a value', async () => {
|
||||
let doc = await test_doc();
|
||||
doc.set('subject', 'testing 1')
|
||||
assert.equal(doc.get('subject'), 'testing 1');
|
||||
});
|
||||
|
||||
it('should not allow incorrect Select option', async () => {
|
||||
let doc = await test_doc();
|
||||
assert.throws(
|
||||
() => {
|
||||
doc.set('status', 'Illegal');
|
||||
},
|
||||
frappe.ValueError
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete a document', async () => {
|
||||
let doc = await test_doc();
|
||||
await doc.insert();
|
||||
|
||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), doc.name);
|
||||
|
||||
await doc.delete();
|
||||
|
||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
async function test_doc() {
|
||||
return await frappe.get_doc({
|
||||
doctype: 'ToDo',
|
||||
status: 'Open',
|
||||
subject: 'testing 1',
|
||||
description: 'test description 1'
|
||||
});
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Meta', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should get init from json file', () => {
|
||||
let todo = frappe.get_meta('ToDo');
|
||||
assert.equal(todo.issingle, 0);
|
||||
});
|
||||
|
||||
it('should get fields from meta', () => {
|
||||
let todo = frappe.get_meta('ToDo');
|
||||
let fields = todo.fields.map((df) => df.fieldname);
|
||||
assert.ok(fields.includes('subject'));
|
||||
assert.ok(fields.includes('description'));
|
||||
assert.ok(fields.includes('status'));
|
||||
});
|
||||
});
|
@ -1,14 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Models', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should get todo json', () => {
|
||||
let todo = frappe.models.get('DocType', 'ToDo');
|
||||
assert.equal(todo.issingle, 0);
|
||||
});
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const fetch = require('node-fetch');
|
||||
const helpers = require('./helpers');
|
||||
const { spawn } = require('child_process');
|
||||
const process = require('process');
|
||||
const Database = require('frappe-core/frappe/backends/rest_client').Database
|
||||
|
||||
// create a copy of frappe
|
||||
|
||||
var test_server;
|
||||
|
||||
describe('REST', () => {
|
||||
before(async function() {
|
||||
test_server = spawn('node', ['frappe/tests/test_server.js'], {
|
||||
stdio: [process.stdin, process.stdout, process.stderr, 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
await frappe.init();
|
||||
await frappe.login();
|
||||
|
||||
frappe.db = await new Database({server: 'localhost:8000'});
|
||||
frappe.fetch = fetch;
|
||||
|
||||
// wait for server to start
|
||||
await frappe.sleep(1);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
frappe.close();
|
||||
test_server.kill();
|
||||
});
|
||||
|
||||
it('should create a document', async () => {
|
||||
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
||||
await doc.insert();
|
||||
|
||||
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
||||
|
||||
assert.equal(doc.subject, doc1.subject);
|
||||
assert.equal(doc1.status, 'Open');
|
||||
});
|
||||
|
||||
it('should update a document', async () => {
|
||||
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
||||
await doc.insert();
|
||||
|
||||
doc.subject = 'subject changed';
|
||||
await doc.update();
|
||||
|
||||
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
||||
assert.equal(doc.subject, doc1.subject);
|
||||
});
|
||||
|
||||
it('should get multiple documents', async () => {
|
||||
await frappe.insert({doctype:'ToDo', subject:'all test 1'});
|
||||
await frappe.insert({doctype:'ToDo', subject:'all test 2'});
|
||||
|
||||
let data = await frappe.db.get_all({doctype:'ToDo'});
|
||||
let subjects = data.map(d => d.subject);
|
||||
assert.ok(subjects.includes('all test 1'));
|
||||
assert.ok(subjects.includes('all test 2'));
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
const server = require('frappe-core/frappe/server');
|
||||
|
||||
if (require.main === module) {
|
||||
server.start({backend: 'sqlite', connection_params: {db_path: 'test.db'}});
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
module.exports = {
|
||||
slug(text) {
|
||||
return text.toLowerCase().replace(/ /g, '_');
|
||||
},
|
||||
|
||||
async_handler(fn) {
|
||||
return (req, res, next) => Promise.resolve(fn(req, res, next))
|
||||
.catch(next);
|
||||
},
|
||||
|
||||
async sleep(seconds) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
});
|
||||
}
|
||||
}
|
69
index.js
Normal file
69
index.js
Normal file
@ -0,0 +1,69 @@
|
||||
module.exports = {
|
||||
async init() {
|
||||
if (this._initialized) return;
|
||||
this.init_config();
|
||||
this.init_errors();
|
||||
this.init_globals();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
init_config() {
|
||||
this.config = {
|
||||
backend: 'sqlite',
|
||||
port: 8000
|
||||
};
|
||||
},
|
||||
|
||||
init_errors() {
|
||||
this.ValueError = class extends Error { };
|
||||
},
|
||||
|
||||
init_globals() {
|
||||
this.meta_cache = {};
|
||||
},
|
||||
|
||||
get_meta(doctype) {
|
||||
if (!this.meta_cache[doctype]) {
|
||||
this.meta_cache[doctype] = new (this.models.get_meta_class(doctype))(this.models.get('DocType', doctype));
|
||||
}
|
||||
return this.meta_cache[doctype];
|
||||
},
|
||||
|
||||
init_controller(doctype, module) {
|
||||
doctype = this.slug(doctype);
|
||||
this.models.controllers[doctype] = module[doctype];
|
||||
this.models.meta_classes[doctype] = module[doctype + '_meta'];
|
||||
},
|
||||
|
||||
async get_doc(data, name) {
|
||||
if (typeof data==='string' && typeof name==='string') {
|
||||
let controller_class = this.models.get_controller(data);
|
||||
var doc = new controller_class({doctype:data, name: name});
|
||||
await doc.load();
|
||||
} else {
|
||||
let controller_class = this.models.get_controller(data.doctype);
|
||||
var doc = new controller_class(data);
|
||||
}
|
||||
return doc;
|
||||
},
|
||||
|
||||
async insert(data) {
|
||||
const doc = await this.get_doc(data);
|
||||
return await doc.insert();
|
||||
},
|
||||
|
||||
login(user='guest', user_key) {
|
||||
this.session = new this._session.Session(user);
|
||||
if (user && user_key) {
|
||||
this.authenticate(user_key);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
};
|
143
model/document.js
Normal file
143
model/document.js
Normal file
@ -0,0 +1,143 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Document {
|
||||
constructor(data) {
|
||||
this.handlers = {};
|
||||
this.setup();
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
setup() {
|
||||
// add handlers
|
||||
}
|
||||
|
||||
clear_handlers() {
|
||||
this.handlers = {};
|
||||
}
|
||||
|
||||
add_handler(key, method) {
|
||||
if (!this.handlers[key]) {
|
||||
this.handlers[key] = [];
|
||||
}
|
||||
this.handlers[key].push(method || key);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this[key];
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.validate_field(key, value);
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
set_name() {
|
||||
// assign a random name by default
|
||||
// override this to set a name
|
||||
if (!this.name) {
|
||||
this.name = Math.random().toString(36).substr(3);
|
||||
}
|
||||
}
|
||||
|
||||
set_keywords() {
|
||||
let keywords = [];
|
||||
for (let fieldname of this.meta.get_keyword_fields()) {
|
||||
keywords.push(this[fieldname]);
|
||||
}
|
||||
this.keywords = keywords.join(', ');
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (!this._meta) {
|
||||
this._meta = frappe.get_meta(this.doctype);
|
||||
}
|
||||
return this._meta;
|
||||
}
|
||||
|
||||
append(key, document) {
|
||||
if (!this[key]) {
|
||||
this[key] = [];
|
||||
}
|
||||
this[key].push(this.init_doc(document));
|
||||
}
|
||||
|
||||
init_doc(data) {
|
||||
if (data.prototype instanceof Document) {
|
||||
return data;
|
||||
} else {
|
||||
return new Document(d);
|
||||
}
|
||||
}
|
||||
|
||||
validate_field (key, value) {
|
||||
let df = this.meta.get_field(key);
|
||||
if (df.fieldtype=='Select') {
|
||||
this.meta.validate_select(df, value);
|
||||
}
|
||||
}
|
||||
|
||||
get_valid_dict() {
|
||||
let doc = {};
|
||||
for(let df of this.meta.get_valid_fields()) {
|
||||
doc[df.fieldname] = this.get(df.fieldname);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
set_standard_values() {
|
||||
let now = new Date();
|
||||
if (this.docstatus === null || this.docstatus === undefined) {
|
||||
this.docstatus = 0;
|
||||
}
|
||||
if (!this.owner) {
|
||||
this.owner = frappe.session.user;
|
||||
this.creation = now;
|
||||
}
|
||||
this.modified_by = frappe.session.user;
|
||||
this.modified = now;
|
||||
}
|
||||
|
||||
async load() {
|
||||
Object.assign(this, await frappe.db.get(this.doctype, this.name));
|
||||
}
|
||||
|
||||
async insert() {
|
||||
this.set_name();
|
||||
this.set_standard_values();
|
||||
this.set_keywords();
|
||||
await this.trigger('validate', 'before_insert');
|
||||
await frappe.db.insert(this.doctype, this.get_valid_dict());
|
||||
await this.trigger('after_insert', 'after_save');
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.trigger('before_delete');
|
||||
await frappe.db.delete(this.doctype, this.name);
|
||||
await this.trigger('after_delete');
|
||||
}
|
||||
|
||||
async trigger() {
|
||||
for(var key of arguments) {
|
||||
if (this.handlers[key]) {
|
||||
for (let method of this.handlers[key]) {
|
||||
if (typeof method === 'string') {
|
||||
await this[method]();
|
||||
} else {
|
||||
await method(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
this.set_standard_values();
|
||||
this.set_keywords();
|
||||
await this.trigger('validate', 'before_update');
|
||||
await frappe.db.update(this.doctype, this.get_valid_dict());
|
||||
await this.trigger('after_update', 'after_save');
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { Document: Document };
|
39
model/index.js
Normal file
39
model/index.js
Normal file
@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
standard_fields: [
|
||||
{
|
||||
fieldname: 'name', fieldtype: 'Data', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'owner', fieldtype: 'Link', reqd: 1, options: 'User'
|
||||
},
|
||||
{
|
||||
fieldname: 'modified_by', fieldtype: 'Link', reqd: 1, options: 'User'
|
||||
},
|
||||
{
|
||||
fieldname: 'creation', fieldtype: 'Datetime', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'modified', fieldtype: 'Datetime', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'keywords', fieldtype: 'Text'
|
||||
},
|
||||
{
|
||||
fieldname: 'docstatus', fieldtype: 'Int', reqd: 1, default: 0
|
||||
}
|
||||
],
|
||||
child_fields: [
|
||||
{
|
||||
fieldname: 'idx', fieldtype: 'Int', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parent', fieldtype: 'Data', reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'parenttype', fieldtype: 'Link', reqd: 1, options: 'DocType'
|
||||
},
|
||||
{
|
||||
fieldname: 'parentfield', fieldtype: 'Data', reqd: 1
|
||||
}
|
||||
]
|
||||
};
|
115
model/meta.js
Normal file
115
model/meta.js
Normal file
@ -0,0 +1,115 @@
|
||||
const Document = require('./document').Document;
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Meta extends Document {
|
||||
constructor(data) {
|
||||
super(data);
|
||||
this.event_handlers = {};
|
||||
this.list_options = {
|
||||
fields: ['name', 'modified']
|
||||
};
|
||||
if (this.setup_meta) {
|
||||
this.setup_meta();
|
||||
}
|
||||
}
|
||||
|
||||
get_field(fieldname) {
|
||||
if (!this.field_map) {
|
||||
this.field_map = {};
|
||||
for (let df of this.fields) {
|
||||
this.field_map[df.fieldname] = df;
|
||||
}
|
||||
}
|
||||
return this.field_map[fieldname];
|
||||
}
|
||||
|
||||
on(key, fn) {
|
||||
if (!this.event_handlers[key]) {
|
||||
this.event_handlers[key] = [];
|
||||
}
|
||||
this.event_handlers[key].push(fn);
|
||||
}
|
||||
|
||||
async set(fieldname, value) {
|
||||
this[fieldname] = value;
|
||||
await this.trigger(fieldname);
|
||||
}
|
||||
|
||||
get(fieldname) {
|
||||
return this[fieldname];
|
||||
}
|
||||
|
||||
get_valid_fields() {
|
||||
if (!this._valid_fields) {
|
||||
this._valid_fields = [];
|
||||
|
||||
// standard fields
|
||||
for (let df of frappe.model.standard_fields) {
|
||||
this._valid_fields.push(df);
|
||||
}
|
||||
|
||||
// parent fields
|
||||
if (this.istable) {
|
||||
for (let df of frappe.model.child_fields) {
|
||||
this._valid_fields.push(df);
|
||||
}
|
||||
}
|
||||
|
||||
// doctype fields
|
||||
for (let df of this.fields) {
|
||||
if (frappe.db.type_map[df.fieldtype]) {
|
||||
this._valid_fields.push(df);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._valid_fields;
|
||||
}
|
||||
|
||||
get_keyword_fields() {
|
||||
return this.keyword_fields || this.meta.fields.filter(df => df.reqd).map(df => df.fieldname);
|
||||
}
|
||||
|
||||
validate_select(df, value) {
|
||||
let options = df.options;
|
||||
if (typeof options === 'string') {
|
||||
// values given as string
|
||||
options = df.options.split('\n');
|
||||
}
|
||||
if (!options.includes(value)) {
|
||||
throw new frappe.ValueError(`${value} must be one of ${options.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
async trigger(key, event = {}) {
|
||||
|
||||
Object.assign(event, {
|
||||
doc: this,
|
||||
name: key
|
||||
});
|
||||
|
||||
if (this.event_handlers[key]) {
|
||||
for (var handler of this.event_handlers[key]) {
|
||||
await handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collections
|
||||
async get_list({start, limit=20, filters}) {
|
||||
return await frappe.db.get_all({
|
||||
doctype: this.name,
|
||||
fields: this.list_options.fields,
|
||||
filters: filters,
|
||||
start: start,
|
||||
limit: limit
|
||||
});
|
||||
}
|
||||
|
||||
get_row_html(data) {
|
||||
return `<a href="/view/${this.name}/${data.name}">${data.name}</a>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Meta: Meta }
|
25
model/models.js
Normal file
25
model/models.js
Normal file
@ -0,0 +1,25 @@
|
||||
const process = require('process');
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Models {
|
||||
constructor() {
|
||||
this.data = {doctype: {}};
|
||||
this.controllers = {};
|
||||
this.meta_classes = {};
|
||||
}
|
||||
|
||||
get(doctype, name) {
|
||||
return this.data[frappe.slug(doctype)][frappe.slug(name)];
|
||||
}
|
||||
|
||||
get_controller(doctype) {
|
||||
return this.controllers[frappe.slug(doctype)];
|
||||
}
|
||||
|
||||
get_meta_class(doctype) {
|
||||
return this.meta_classes[frappe.slug(doctype)] || frappe.meta.Meta;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Models: Models }
|
30
models/doctype/todo/todo.js
Normal file
30
models/doctype/todo/todo.js
Normal file
@ -0,0 +1,30 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class todo_meta extends frappe.meta.Meta {
|
||||
setup_meta() {
|
||||
Object.assign(this, require('./todo.json'));
|
||||
this.name = 'ToDo';
|
||||
this.list_options.fields = ['name', 'subject', 'status', 'description'];
|
||||
}
|
||||
|
||||
get_row_html(data) {
|
||||
return `<a href="#edit/todo/${data.name}">${data.subject}</a>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class todo extends frappe.document.Document {
|
||||
setup() {
|
||||
this.add_handler('validate');
|
||||
}
|
||||
validate() {
|
||||
if (!this.status) {
|
||||
this.status = 'Open';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
todo: todo,
|
||||
todo_meta: todo_meta
|
||||
};
|
@ -3,6 +3,10 @@
|
||||
"name": "ToDo",
|
||||
"doctype": "DocType",
|
||||
"issingle": 0,
|
||||
"keyword_fields": [
|
||||
"subject",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "subject",
|
@ -2,10 +2,10 @@
|
||||
"name": "frappe-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Frappe Core",
|
||||
"main": "frappe/index.js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha frappe/tests",
|
||||
"start": "nodemon frappe/app.js"
|
||||
"test": "mocha tests",
|
||||
"start": "nodemon app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
|
52
server/index.js
Normal file
52
server/index.js
Normal file
@ -0,0 +1,52 @@
|
||||
const backends = {};
|
||||
backends.sqlite = require('frappe-core/backends/sqlite');
|
||||
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const frappe = require('frappe-core');
|
||||
const rest_api = require('./rest_api')
|
||||
const models = require('frappe-core/server/models');
|
||||
const common = require('frappe-core/common');
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
module.exports = {
|
||||
async init() {
|
||||
await frappe.init();
|
||||
common.init_libs(frappe);
|
||||
await frappe.login();
|
||||
|
||||
// walk and find models
|
||||
models.init();
|
||||
|
||||
},
|
||||
|
||||
async init_db({backend, connection_params}) {
|
||||
frappe.db = await new backends[backend].Database(connection_params);
|
||||
await frappe.db.connect();
|
||||
await frappe.db.migrate();
|
||||
},
|
||||
|
||||
async start({backend, connection_params, static}) {
|
||||
await this.init();
|
||||
await this.init_db({backend:backend, connection_params:connection_params});
|
||||
// database
|
||||
|
||||
// app
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static('./'));
|
||||
|
||||
app.use(function (err, req, res, next) {
|
||||
console.error(err.stack);
|
||||
res.status(500).send('Something broke!');
|
||||
})
|
||||
// routes
|
||||
rest_api.setup(app);
|
||||
|
||||
// listen
|
||||
frappe.app = app;
|
||||
frappe.server = app.listen(frappe.config.port);
|
||||
|
||||
}
|
||||
}
|
||||
|
54
server/models.js
Normal file
54
server/models.js
Normal file
@ -0,0 +1,54 @@
|
||||
const frappe = require('frappe-core');
|
||||
const walk = require('walk');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
init() {
|
||||
const cwd = process.cwd();
|
||||
|
||||
const setup_model = (doctype, name, file_path) => {
|
||||
// add to frappe.models.data
|
||||
if (!frappe.models[doctype]) {
|
||||
frappe.models[doctype] = {};
|
||||
}
|
||||
frappe.models.data[doctype][name] = require(file_path);
|
||||
}
|
||||
|
||||
const setup_controller = (doctype, file_path) => {
|
||||
let _module = require(file_path);
|
||||
frappe.models.controllers[doctype] = _module[doctype];
|
||||
if (_module[doctype + '_meta']) {
|
||||
frappe.models.meta_classes[doctype] = _module[doctype + '_meta'];
|
||||
}
|
||||
}
|
||||
|
||||
// init for all apps
|
||||
if (!frappe.config.apps) {
|
||||
frappe.config.apps = [];
|
||||
}
|
||||
|
||||
frappe.config.apps.unshift('frappe-core');
|
||||
|
||||
// walk and sync
|
||||
for (let app_name of frappe.config.apps) {
|
||||
let start = path.resolve(require.resolve(app_name), '../models');
|
||||
walk.walkSync(start, {
|
||||
listeners: {
|
||||
file: (basepath, file_data, next) => {
|
||||
const doctype = path.basename(path.dirname(basepath));
|
||||
const name = path.basename(basepath);
|
||||
const file_path = path.resolve(basepath, file_data.name);
|
||||
if (file_data.name.endsWith('.json')) {
|
||||
setup_model(doctype, name, file_path)
|
||||
}
|
||||
if (doctype==='doctype' && file_data.name.endsWith('.js')) {
|
||||
setup_controller(path.basename(basepath), file_path);
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
61
server/rest_api.js
Normal file
61
server/rest_api.js
Normal file
@ -0,0 +1,61 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
module.exports = {
|
||||
setup(app) {
|
||||
// get list
|
||||
app.get('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
||||
let fields, filters;
|
||||
for (key of ['fields', 'filters']) {
|
||||
if (request.query[key]) {
|
||||
request.query[key] = JSON.parse(request.query[key]);
|
||||
}
|
||||
}
|
||||
|
||||
let data = await frappe.db.get_all({
|
||||
doctype: request.params.doctype,
|
||||
fields: request.query.fields || ['name', 'subject'],
|
||||
filters: request.query.filters,
|
||||
start: request.query.start || 0,
|
||||
limit: request.query.limit || 20,
|
||||
order_by: request.query.order_by,
|
||||
order: request.query.order
|
||||
});
|
||||
|
||||
return response.json(data);
|
||||
}));
|
||||
|
||||
// create
|
||||
app.post('/api/resource/:doctype', frappe.async_handler(async function(request, response) {
|
||||
data = request.body;
|
||||
data.doctype = request.params.doctype;
|
||||
let doc = await frappe.get_doc(data);
|
||||
await doc.insert();
|
||||
await frappe.db.commit();
|
||||
return response.json(doc.get_valid_dict());
|
||||
}));
|
||||
|
||||
// update
|
||||
app.put('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||
data = request.body;
|
||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
||||
Object.assign(doc, data);
|
||||
await doc.update();
|
||||
await frappe.db.commit();
|
||||
return response.json(doc.get_valid_dict());
|
||||
}));
|
||||
|
||||
|
||||
// get document
|
||||
app.get('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name);
|
||||
return response.json(doc.get_valid_dict());
|
||||
}));
|
||||
|
||||
// delete
|
||||
app.delete('/api/resource/:doctype/:name', frappe.async_handler(async function(request, response) {
|
||||
let doc = await frappe.get_doc(request.params.doctype, request.params.name)
|
||||
await doc.delete();
|
||||
return response.json({});
|
||||
}));
|
||||
}
|
||||
};
|
17
session.js
Normal file
17
session.js
Normal file
@ -0,0 +1,17 @@
|
||||
const frappe = require('frappe-core');
|
||||
|
||||
class Session {
|
||||
constructor(user, user_key) {
|
||||
this.user = user || 'guest';
|
||||
if (this.user !== 'guest') {
|
||||
this.login(user_key);
|
||||
}
|
||||
}
|
||||
|
||||
login(user_key) {
|
||||
// could be password, sessionid, otp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { Session: Session };
|
11
tests/helpers.js
Normal file
11
tests/helpers.js
Normal file
@ -0,0 +1,11 @@
|
||||
const server = require('frappe-core/server');
|
||||
|
||||
module.exports = {
|
||||
async init_sqlite() {
|
||||
server.init()
|
||||
server.init_db({
|
||||
backend: 'sqlite',
|
||||
connection_params: {db_path: 'test.db'}
|
||||
});
|
||||
}
|
||||
}
|
18
tests/test_controller.js
Normal file
18
tests/test_controller.js
Normal file
@ -0,0 +1,18 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Controller', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should call controller method', async () => {
|
||||
let doc = await frappe.get_doc({
|
||||
doctype:'ToDo',
|
||||
subject: 'test'
|
||||
});
|
||||
doc.trigger('validate');
|
||||
assert.equal(doc.status, 'Open');
|
||||
});
|
||||
});
|
49
tests/test_database.js
Normal file
49
tests/test_database.js
Normal file
@ -0,0 +1,49 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Database', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should insert and get values', async () => {
|
||||
await frappe.db.sql('delete from todo');
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 1'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 3'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 2'});
|
||||
|
||||
let subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject']})
|
||||
subjects = subjects.map(d => d.subject);
|
||||
|
||||
assert.ok(subjects.includes('testing 1'));
|
||||
assert.ok(subjects.includes('testing 2'));
|
||||
assert.ok(subjects.includes('testing 3'));
|
||||
});
|
||||
|
||||
it('should filter correct values', async () => {
|
||||
let subjects = null;
|
||||
|
||||
await frappe.db.sql('delete from todo');
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 1', status: 'Open'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 3', status: 'Open'});
|
||||
await frappe.insert({doctype:'ToDo', subject: 'testing 2', status: 'Closed'});
|
||||
|
||||
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
||||
filters:{status: 'Open'}});
|
||||
subjects = subjects.map(d => d.subject);
|
||||
|
||||
assert.ok(subjects.includes('testing 1'));
|
||||
assert.ok(subjects.includes('testing 3'));
|
||||
assert.equal(subjects.includes('testing 2'), false);
|
||||
|
||||
subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'],
|
||||
filters:{status: 'Closed'}});
|
||||
subjects = subjects.map(d => d.subject);
|
||||
|
||||
assert.equal(subjects.includes('testing 1'), false);
|
||||
assert.equal(subjects.includes('testing 3'), false);
|
||||
assert.ok(subjects.includes('testing 2'));
|
||||
|
||||
});
|
||||
});
|
75
tests/test_document.js
Normal file
75
tests/test_document.js
Normal file
@ -0,0 +1,75 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Document', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should insert a doc', async () => {
|
||||
let doc1 = await test_doc();
|
||||
doc1.subject = 'insert subject 1';
|
||||
doc1.description = 'insert description 1';
|
||||
await doc1.insert();
|
||||
|
||||
// get it back from the db
|
||||
let doc2 = await frappe.get_doc(doc1.doctype, doc1.name);
|
||||
assert.equal(doc1.subject, doc2.subject);
|
||||
assert.equal(doc1.description, doc2.description);
|
||||
});
|
||||
|
||||
it('should update a doc', async () => {
|
||||
let doc = await test_doc();
|
||||
await doc.insert();
|
||||
|
||||
assert.notEqual(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
||||
|
||||
doc.subject = 'subject 2'
|
||||
await doc.update();
|
||||
|
||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
|
||||
})
|
||||
|
||||
it('should get a value', async () => {
|
||||
let doc = await test_doc();
|
||||
assert.equal(doc.get('subject'), 'testing 1');
|
||||
});
|
||||
|
||||
it('should set a value', async () => {
|
||||
let doc = await test_doc();
|
||||
doc.set('subject', 'testing 1')
|
||||
assert.equal(doc.get('subject'), 'testing 1');
|
||||
});
|
||||
|
||||
it('should not allow incorrect Select option', async () => {
|
||||
let doc = await test_doc();
|
||||
assert.throws(
|
||||
() => {
|
||||
doc.set('status', 'Illegal');
|
||||
},
|
||||
frappe.ValueError
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete a document', async () => {
|
||||
let doc = await test_doc();
|
||||
await doc.insert();
|
||||
|
||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), doc.name);
|
||||
|
||||
await doc.delete();
|
||||
|
||||
assert.equal(await frappe.db.get_value(doc.doctype, doc.name), null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
async function test_doc() {
|
||||
return await frappe.get_doc({
|
||||
doctype: 'ToDo',
|
||||
status: 'Open',
|
||||
subject: 'testing 1',
|
||||
description: 'test description 1'
|
||||
});
|
||||
}
|
22
tests/test_meta.js
Normal file
22
tests/test_meta.js
Normal file
@ -0,0 +1,22 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Meta', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should get init from json file', () => {
|
||||
let todo = frappe.get_meta('ToDo');
|
||||
assert.equal(todo.issingle, 0);
|
||||
});
|
||||
|
||||
it('should get fields from meta', () => {
|
||||
let todo = frappe.get_meta('ToDo');
|
||||
let fields = todo.fields.map((df) => df.fieldname);
|
||||
assert.ok(fields.includes('subject'));
|
||||
assert.ok(fields.includes('description'));
|
||||
assert.ok(fields.includes('status'));
|
||||
});
|
||||
});
|
14
tests/test_models.js
Normal file
14
tests/test_models.js
Normal file
@ -0,0 +1,14 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('Models', () => {
|
||||
before(async function() {
|
||||
await helpers.init_sqlite();
|
||||
});
|
||||
|
||||
it('should get todo json', () => {
|
||||
let todo = frappe.models.get('DocType', 'ToDo');
|
||||
assert.equal(todo.issingle, 0);
|
||||
});
|
||||
});
|
66
tests/test_rest_api.js
Normal file
66
tests/test_rest_api.js
Normal file
@ -0,0 +1,66 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappe-core');
|
||||
const fetch = require('node-fetch');
|
||||
const helpers = require('./helpers');
|
||||
const { spawn } = require('child_process');
|
||||
const process = require('process');
|
||||
const Database = require('frappe-core/backends/rest_client').Database
|
||||
|
||||
// create a copy of frappe
|
||||
|
||||
var test_server;
|
||||
|
||||
describe('REST', () => {
|
||||
before(async function() {
|
||||
test_server = spawn('node', ['tests/test_server.js'], {
|
||||
stdio: [process.stdin, process.stdout, process.stderr, 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
await frappe.init();
|
||||
await frappe.login();
|
||||
|
||||
frappe.db = await new Database({server: 'localhost:8000'});
|
||||
frappe.fetch = fetch;
|
||||
|
||||
// wait for server to start
|
||||
await frappe.sleep(1);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
frappe.close();
|
||||
test_server.kill();
|
||||
});
|
||||
|
||||
it('should create a document', async () => {
|
||||
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
||||
await doc.insert();
|
||||
|
||||
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
||||
|
||||
assert.equal(doc.subject, doc1.subject);
|
||||
assert.equal(doc1.status, 'Open');
|
||||
});
|
||||
|
||||
it('should update a document', async () => {
|
||||
let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'});
|
||||
await doc.insert();
|
||||
|
||||
doc.subject = 'subject changed';
|
||||
await doc.update();
|
||||
|
||||
let doc1 = await frappe.get_doc('ToDo', doc.name);
|
||||
assert.equal(doc.subject, doc1.subject);
|
||||
});
|
||||
|
||||
it('should get multiple documents', async () => {
|
||||
await frappe.insert({doctype:'ToDo', subject:'all test 1'});
|
||||
await frappe.insert({doctype:'ToDo', subject:'all test 2'});
|
||||
|
||||
let data = await frappe.db.get_all({doctype:'ToDo'});
|
||||
let subjects = data.map(d => d.subject);
|
||||
assert.ok(subjects.includes('all test 1'));
|
||||
assert.ok(subjects.includes('all test 2'));
|
||||
});
|
||||
|
||||
|
||||
});
|
5
tests/test_server.js
Normal file
5
tests/test_server.js
Normal file
@ -0,0 +1,5 @@
|
||||
const server = require('frappe-core/server');
|
||||
|
||||
if (require.main === module) {
|
||||
server.start({backend: 'sqlite', connection_params: {db_path: 'test.db'}});
|
||||
}
|
16
utils/index.js
Normal file
16
utils/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
slug(text) {
|
||||
return text.toLowerCase().replace(/ /g, '_');
|
||||
},
|
||||
|
||||
async_handler(fn) {
|
||||
return (req, res, next) => Promise.resolve(fn(req, res, next))
|
||||
.catch(next);
|
||||
},
|
||||
|
||||
async sleep(seconds) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user