2
0
mirror of https://github.com/frappe/books.git synced 2024-09-20 03:29:00 +00:00

collapse frappe folder and start desk

This commit is contained in:
Rushabh Mehta 2018-01-12 17:55:07 +05:30
parent f89c6e778e
commit e3a94dc997
90 changed files with 2026 additions and 1889 deletions

6
app.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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});
}
}

View 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;

View File

@ -1,10 +1,10 @@
const BaseControl = require('./base'); const BaseControl = require('./base');
class DataControl extends BaseControl { class DataControl extends BaseControl {
make() { make() {
super.make(); super.make();
this.input.setAttribute('type', 'text'); this.input.setAttribute('type', 'text');
} }
}; };
module.exports = DataControl; module.exports = DataControl;

View 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;
}
}

View 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;

View 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
View 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
View 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
View 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 };

View File

@ -7,12 +7,12 @@ const _session = require('../session');
module.exports = { module.exports = {
init_libs(frappe) { init_libs(frappe) {
Object.assign(frappe, utils); Object.assign(frappe, utils);
frappe.model = model; frappe.model = model;
frappe.models = new models.Models(); frappe.models = new models.Models();
frappe.document = _document; frappe.document = _document;
frappe.meta = meta; frappe.meta = meta;
frappe._session = _session; frappe._session = _session;
} }
} }

View File

@ -1,6 +0,0 @@
const server = require('frappe-core/frappe/server');
server.start({
backend: 'sqllite',
connection_params: {db_path: 'test.db'}
});

View File

@ -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
}

View File

@ -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 };

View File

@ -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();
}
};

View File

@ -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;

View File

@ -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});
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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};

View File

@ -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);
}
},
}

View File

@ -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
};

View File

@ -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 };

View File

@ -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};

View File

@ -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();
}
}
};

View File

@ -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 };

View File

@ -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
}
]
};

View File

@ -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 }

View File

@ -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 }

View File

@ -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
};

View File

@ -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);
}
}

View File

@ -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();
}
}
});
}
}
}

View File

@ -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({});
}));
}
};

View File

@ -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 };

View File

@ -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'}
});
}
}

View File

@ -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');
});
});

View File

@ -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'));
});
});

View File

@ -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'
});
}

View File

@ -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'));
});
});

View File

@ -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);
});
});

View File

@ -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'));
});
});

View File

@ -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'}});
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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 }

View 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
};

View File

@ -3,6 +3,10 @@
"name": "ToDo", "name": "ToDo",
"doctype": "DocType", "doctype": "DocType",
"issingle": 0, "issingle": 0,
"keyword_fields": [
"subject",
"description"
],
"fields": [ "fields": [
{ {
"fieldname": "subject", "fieldname": "subject",

View File

@ -2,10 +2,10 @@
"name": "frappe-core", "name": "frappe-core",
"version": "1.0.0", "version": "1.0.0",
"description": "Frappe Core", "description": "Frappe Core",
"main": "frappe/index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "mocha frappe/tests", "test": "mocha tests",
"start": "nodemon frappe/app.js" "start": "nodemon app.js"
}, },
"dependencies": { "dependencies": {
"body-parser": "^1.18.2", "body-parser": "^1.18.2",

52
server/index.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
});
}
}