2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

first commit

This commit is contained in:
Rushabh Mehta 2018-01-01 14:57:59 +05:30
commit 2ed02ea846
20 changed files with 4889 additions and 0 deletions

8
.eslintrc Normal file
View File

@ -0,0 +1,8 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parser": "babel-eslint"
}

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
node_modules
.DS_Store
Thumbs.db
test.db
*.log
/dist
/temp
# ignore everything in 'app' folder what had been generated from 'src' folder
/app/app.js
/app/background.js
/app/**/*.map

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# Frappe Core
Core libs for Frappe Framework JS
### Examples
const frappe = require('frappe-core);
# init database
frappe.init();
# make a new todo
let todo = frappe.get_doc({doctype: 'ToDo', subject: 'something'})
todo.insert()
# get all todos
let total_open = 0;
for (let d of frappe.get_all('ToDo')) {
todo = frappe.get_doc('ToDo', d.name);
if (todo.status == 'Open') total_open += 1;
}

60
frappe/index.js Normal file
View File

@ -0,0 +1,60 @@
const path = require('path')
module.exports = {
init(db_path, user, user_key) {
this.db_path = db_path || 'test.db';
if (this._initialized) return;
this.init_config();
this.init_errors();
this.init_globals();
this.init_libs();
this.init_db();
this._initialized = true;
this.login(user, user_key);
},
init_config() {
this.config = {};
},
init_errors() {
this.ValueError = class extends Error { };
},
init_globals() {
this.meta_cache = {};
},
init_libs() {
this.utils = require('./utils');
Object.assign(this, this.utils);
let models = require('./model/models');
this.models = new models.Models();
this.model = require('./model');
this.document = require('./model/document');
this.meta = require('./model/meta');
this.session_lib = require('./session');
},
init_db() {
let database = require('./model/database');
this.db = new database.Database(this.db_path);
},
get_meta(doctype) {
if (!this.meta_cache[doctype]) {
this.meta_cache[doctype] = new this.meta.Meta(this.models.get('DocType', doctype));
}
return this.meta_cache[doctype];
},
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});
doc.load();
} else {
let controller_class = this.models.get_controller(data.doctype);
var doc = new controller_class(data);
}
return doc;
},
login(user, user_key) {
this.session = new this.session_lib.Session(user);
if (user && user_key) {
this.login(user_key);
}
}
};

124
frappe/model/database.js Normal file
View File

@ -0,0 +1,124 @@
const fs = require('fs');
const path = require('path');
const SQL = require('sql.js');
const frappe = require('frappe-core');
class Database {
constructor(db_path) {
this.db_file_name = db_path;
this.connect();
}
connect() {
if (this.db_path) {
const filebuffer = fs.readFileSync(this.db_path);
this._conn = new SQL.Database(filebuffer);
} else {
this._conn = new SQL.Database();
}
}
write() {
if (this.db_path) {
let data = this._conn.export();
fs.writeFileSync(this.db_path, new Buffer(data));
}
}
close() {
this.write();
this._conn.close();
}
create_db() {
// Create a database.
let db = new SQL.Database();
let query = SCHEMA;
let result = db.exec(query);
if (Object.keys(result).length === 0 &&
typeof result.constructor === 'function') {
return db;
} else {
return null;
}
}
create_table(doctype) {
let meta = frappe.get_meta(doctype);
let columns = [];
// add standard fields
let fields = frappe.model.standard_fields.slice();
if (meta.istable) {
fields = fields.concat(model.child_fields);
}
// add model fields
fields = fields.concat(meta.fields);
for (let df of fields) {
if (frappe.model.type_map[df.fieldtype]) {
columns.push(`${df.fieldname} ${frappe.model.type_map[df.fieldtype]} ${df.reqd ? "not null" : ""} ${df.default ? ("default " + frappe.utils.sqlescape(df.default)) : ""}`);
}
}
const query = `CREATE TABLE IF NOT EXISTS ${frappe.slug(doctype)} (
${columns.join(", ")})`;
return this.sql(query);
}
get(doctype, name) {
let doc = frappe.db.sql(`select * from ${frappe.slug(doctype)} where name = ${frappe.db.escape(name)}`);
return doc ? doc[0] : {};
}
insert(doctype, doc) {
this.sql(`insert into ${frappe.slug(doctype)}
(${Object.keys(doc).join(", ")})
values (${Object.values(doc).map(d => frappe.db.escape(d)).join(", ")})`);
}
update(doctype, doc) {
let assigns = [];
for (let key in doc) {
assigns.push(`${key} = ${this.escape(doc[key])}`);
}
this.sql(`update ${frappe.slug(doctype)}
set ${assigns.join(", ")}`);
}
sql(query, opts={}) {
//console.log(query);
const result = frappe.db._conn.exec(query);
if (result.length > 0) {
if (opts.as_list)
return result[0];
else
return sql_result_to_obj(result[0]);
}
return null;
}
get_value(doctype, name, fieldname='name') {
let value = this.sql(`select ${fieldname} from ${frappe.slug(doctype)}
where name=${this.escape(name)}`);
return value.length ? value[0][fieldname] : null;
}
escape(value) {
return frappe.utils.sqlescape(value);
}
}
function sql_result_to_obj(result) {
const columns = result.columns;
return result.values.map(row => {
return columns.reduce((res, col, i) => {
res[col] = row[i];
return res;
}, {});
})
}
module.exports = { Database: Database };

109
frappe/model/document.js Normal file
View File

@ -0,0 +1,109 @@
const frappe = require('frappe-core');
class Document {
constructor(data) {
Object.assign(this, data);
}
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);
}
}
validate() { }
before_insert() { }
after_insert() { }
before_update() { }
after_update() { }
after_save() { }
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;
}
load() {
Object.assign(this, frappe.db.get(this.doctype, this.name));
return this;
}
insert() {
this.set_name();
this.set_standard_values();
this.validate();
this.before_insert();
frappe.db.insert(this.doctype, this.get_valid_dict())
this.after_insert();
this.after_save();
return this;
}
update() {
this.set_standard_values();
this.validate();
this.before_insert();
frappe.db.update(this.doctype, this.get_valid_dict());
this.after_update();
this.after_save();
return this;
}
};
module.exports = { Document: Document };

63
frappe/model/index.js Normal file
View File

@ -0,0 +1,63 @@
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
}
],
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'
}
};

70
frappe/model/meta.js Normal file
View File

@ -0,0 +1,70 @@
const Document = require('./document').Document;
const frappe = require('frappe-core');
class Meta extends Document {
constructor(data) {
super(data);
this.event_handlers = {};
}
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);
}
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.model.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(", ")}`);
}
}
trigger(key) {
}
}
module.exports = { Meta: Meta }

68
frappe/model/models.js Normal file
View File

@ -0,0 +1,68 @@
const walk = require('walk');
const path = require('path');
const process = require('process');
const frappe = require('frappe-core');
class Models {
constructor() {
this.data = {};
this.controllers = {};
this.setup_path_map();
}
get(doctype, name) {
if (!this.data[doctype]) {
this.data[doctype] = {};
}
if (!this.data[doctype][name]) {
this.data[doctype][name] = require(
this.path_map[frappe.slug(doctype)][frappe.slug(name)]);
}
return this.data[doctype][name];
}
get_controller(doctype) {
doctype = frappe.slug(doctype);
if (!this.controllers[doctype]) {
this.controllers[doctype] = require(this.controller_map[doctype])[doctype];
}
return this.controllers[doctype];
}
setup_path_map() {
const cwd = process.cwd();
this.path_map = {};
this.controller_map = {};
if (!frappe.config.apps) {
frappe.config.apps = [];
}
frappe.config.apps.unshift('frappe-core');
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')) {
if (!this.path_map[doctype]) {
this.path_map[doctype] = {};
}
this.path_map[doctype][name] = file_path;
}
if (doctype==='doctype' && file_data.name.endsWith('.js')) {
this.controller_map[name] = file_path;
}
next();
}
}
});
}
}
}
module.exports = { Models: Models }

View File

@ -0,0 +1,11 @@
const frappe = require('frappe-core');
class todo extends frappe.document.Document {
validate() {
if (!this.status) {
this.status = 'Open';
}
}
}
module.exports = { todo: todo };

View File

@ -0,0 +1,30 @@
{
"autoname": "hash",
"name": "ToDo",
"doctype": "DocType",
"issingle": 0,
"fields": [
{
"fieldname": "subject",
"label": "Subject",
"fieldtype": "Data",
"reqd": 1
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
},
{
"fieldname": "status",
"label": "Status",
"fieldtype": "Select",
"options": [
"Open",
"Closed"
],
"default": "Open",
"reqd": 1
}
]
}

17
frappe/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 };

View File

@ -0,0 +1,18 @@
const assert = require('assert');
const frappe = require('frappe-core');
describe('Controller', () => {
before(function() {
frappe.init();
frappe.db.create_table('ToDo');
});
it('should call controller method', () => {
let doc = frappe.get_doc({
doctype:'ToDo',
subject: 'test'
});
doc.validate();
assert.equal(doc.status, 'Open');
});
});

View File

@ -0,0 +1,13 @@
const assert = require('assert');
const frappe = require('frappe-core');
describe('Document', () => {
before(function() {
frappe.init();
});
// it('should create a table', () => {
// frappe.db.create_table('ToDo');
// frappe.db.write();
// });
});

View File

@ -0,0 +1,62 @@
const assert = require('assert');
const frappe = require('frappe-core');
describe('Document', () => {
before(function() {
frappe.init();
frappe.db.create_table('ToDo');
});
it('should insert a doc', () => {
let doc1 = test_doc();
doc1.subject = 'insert subject 1';
doc1.description = 'insert description 1';
doc1.insert();
// get it back from the db
let doc2 = frappe.get_doc(doc1.doctype, doc1.name);
assert.equal(doc1.subject, doc2.subject);
assert.equal(doc1.description, doc2.description);
});
it('should update a doc', () => {
let doc = test_doc().insert();
assert.notEqual(frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
doc.subject = 'subject 2'
doc.update();
assert.equal(frappe.db.get_value(doc.doctype, doc.name, 'subject'), 'subject 2');
})
it('should get a value', () => {
assert.equal(test_doc().get('subject'), 'testing 1');
});
it('should set a value', () => {
let doc = test_doc();
doc.set('subject', 'testing 1')
assert.equal(doc.get('subject'), 'testing 1');
});
it('should not allow incorrect Select option', () => {
let doc = test_doc();
assert.throws(
() => {
doc.set('status', 'Illegal');
},
frappe.ValueError
);
});
});
const test_doc = () => {
return frappe.get_doc({
doctype: 'ToDo',
status: 'Open',
subject: 'testing 1',
description: 'test description 1'
});
}

21
frappe/tests/test_meta.js Normal file
View File

@ -0,0 +1,21 @@
const assert = require('assert');
const frappe = require('frappe-core');
describe('Meta', () => {
before(function() {
frappe.init();
});
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

@ -0,0 +1,13 @@
const assert = require('assert');
const frappe = require('frappe-core');
describe('Models', () => {
before(function() {
frappe.init();
});
it('should get todo json', () => {
let todo = frappe.models.get('DocType', 'ToDo');
assert.equal(todo.issingle, 0);
});
});

23
frappe/utils.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
slug(text) {
return text.toLowerCase().replace(/ /g, '_');
},
sqlescape(value) {
if (value===null || value===undefined) {
// null
return 'null';
} else if (value instanceof Date) {
// date
return `'${value.toISOString()}'`;
} else if (typeof value==='string') {
// text
return "'" + value.replace(/'/g, '\'').replace(/"/g, '\"') + "'";
} else {
// number
return value + '';
}
}
}

4114
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "frappe-core",
"version": "1.0.0",
"description": "Frappe Core",
"main": "frappe/index.js",
"scripts": {
"test": "mocha frappe/tests"
},
"dependencies": {
"sql.js": "^0.4.0",
"walk": "^2.3.9"
},
"repository": {
"type": "git",
"url": "git+https://github.com/frappe/frappe-js.git"
},
"author": "Frappe",
"license": "MIT",
"bugs": {
"url": "https://github.com/frappe/frappe-js/issues"
},
"homepage": "https://github.com/frappe/frappe-js#readme",
"devDependencies": {
"babel-eslint": "^8.0.1",
"eslint": "^4.9.0",
"mocha": "^4.0.1",
"webpack": "^3.8.1"
}
}