2
0
mirror of https://github.com/frappe/books.git synced 2024-11-14 01:14:03 +00:00
books/backends/database.js

501 lines
14 KiB
JavaScript
Raw Normal View History

const frappe = require('frappejs');
2018-02-19 16:41:10 +00:00
const Observable = require('frappejs/utils/observable');
2018-02-19 16:41:10 +00:00
module.exports = class Database extends Observable {
constructor() {
2018-02-19 16:41:10 +00:00
super();
this.initTypeMap();
}
async connect() {
// this.conn
}
close() {
this.conn.close();
}
async migrate() {
for (let doctype in frappe.models) {
// check if controller module
let meta = frappe.getMeta(doctype);
if (!meta.isSingle) {
if (await this.tableExists(doctype)) {
await this.alterTable(doctype);
} else {
await this.createTable(doctype);
}
}
}
await this.commit();
}
2018-02-20 09:53:38 +00:00
async createTable(doctype, newName=null) {
let meta = frappe.getMeta(doctype);
let columns = [];
2018-02-20 09:53:38 +00:00
let indexes = [];
2018-02-12 12:01:31 +00:00
for (let field of meta.getValidFields({ withChildren: false })) {
if (this.typeMap[field.fieldtype]) {
2018-02-20 09:53:38 +00:00
this.updateColumnDefinition(field, columns, indexes);
}
}
2018-02-20 09:53:38 +00:00
return await this.runCreateTableQuery(newName || doctype, columns, indexes);
}
async tableExists(table) {
// return true if table exists
}
2018-02-20 09:53:38 +00:00
async runCreateTableQuery(doctype, columns, indexes) {
// override
}
2018-02-20 09:53:38 +00:00
updateColumnDefinition(field, columns, indexes) {
// return `${df.fieldname} ${this.typeMap[df.fieldtype]} ${ ? "PRIMARY KEY" : ""} ${df.required && !df.default ? "NOT NULL" : ""} ${df.default ? `DEFAULT ${df.default}` : ""}`
}
async alterTable(doctype) {
// get columns
2018-03-05 16:45:21 +00:00
let diff = await this.getColumnDiff(doctype);
2018-02-20 09:53:38 +00:00
let newForeignKeys = await this.getNewForeignKeys(doctype);
2018-03-05 16:45:21 +00:00
if (diff.added.length) {
await this.addColumns(doctype, diff.added);
2018-02-20 09:53:38 +00:00
}
2018-03-05 16:45:21 +00:00
if (diff.removed.length) {
await this.removeColumns(doctype, diff.removed);
}
2018-02-20 09:53:38 +00:00
if (newForeignKeys.length) {
2018-02-23 16:17:55 +00:00
await this.addForeignKeys(doctype, newForeignKeys);
2018-02-20 09:53:38 +00:00
}
}
2018-03-05 16:45:21 +00:00
async getColumnDiff(doctype) {
const tableColumns = await this.getTableColumns(doctype);
const validFields = frappe.getMeta(doctype).getValidFields({ withChildren: false });
const diff = { added: [], removed: [] };
for (let field of validFields) {
if (!tableColumns.includes(field.fieldname) && this.typeMap[field.fieldtype]) {
2018-03-05 16:45:21 +00:00
diff.added.push(field);
}
}
const validFieldNames = validFields.map(field => field.fieldname);
for (let column of tableColumns) {
if (!validFieldNames.includes(column)) {
diff.removed.push(column);
}
}
2018-03-05 16:45:21 +00:00
return diff;
2018-02-20 09:53:38 +00:00
}
2018-03-05 16:45:21 +00:00
async addColumns(doctype, added) {
for (let field of added) {
2018-02-20 09:53:38 +00:00
await this.runAddColumnQuery(doctype, field);
}
}
2018-03-05 16:45:21 +00:00
async removeColumns(doctype, removed) {
for (let column of removed) {
await this.runRemoveColumnQuery(doctype, column);
}
}
2018-02-20 09:53:38 +00:00
async getNewForeignKeys(doctype) {
let foreignKeys = await this.getForeignKeys(doctype);
let newForeignKeys = [];
let meta = frappe.getMeta(doctype);
for (let field of meta.getValidFields({ withChildren: false})) {
if (field.fieldtype==='Link' && !foreignKeys.includes(field.fieldname)) {
newForeignKeys.push(field);
}
}
return newForeignKeys;
}
async addForeignKeys(doctype, newForeignKeys) {
for (let field of newForeignKeys) {
this.addForeignKey(doctype, field);
}
}
async getForeignKey(doctype, field) {
}
async getTableColumns(doctype) {
return [];
}
2018-02-20 09:53:38 +00:00
async runAddColumnQuery(doctype, field) {
// alter table {doctype} add column ({column_def});
}
2018-02-12 12:01:31 +00:00
async get(doctype, name=null, fields = '*') {
let meta = frappe.getMeta(doctype);
let doc;
if (meta.isSingle) {
doc = await this.getSingle(doctype);
doc.name = doctype;
} else {
if (!name) {
throw new frappe.errors.ValueError('name is mandatory');
2018-02-12 12:01:31 +00:00
}
doc = await this.getOne(doctype, name, fields);
}
await this.loadChildren(doc, meta);
return doc;
}
2018-02-12 12:01:31 +00:00
async loadChildren(doc, meta) {
// load children
2018-02-12 12:01:31 +00:00
let tableFields = meta.getTableFields();
for (let field of tableFields) {
doc[field.fieldname] = await this.getAll({
doctype: field.childtype,
fields: ["*"],
filters: { parent: doc.name },
2018-04-26 10:19:43 +00:00
orderBy: 'idx',
order: 'asc'
});
}
2018-02-12 12:01:31 +00:00
}
async getSingle(doctype) {
let values = await this.getAll({
doctype: 'SingleValue',
2018-02-12 12:01:31 +00:00
fields: ['fieldname', 'value'],
filters: { parent: doctype },
2018-04-26 10:19:43 +00:00
orderBy: 'fieldname',
2018-02-12 12:01:31 +00:00
order: 'asc'
});
let doc = {};
for (let row of values) {
doc[row.fieldname] = row.value;
}
return doc;
}
async getOne(doctype, name, fields = '*') {
// select {fields} form {doctype} where name = ?
}
prepareFields(fields) {
if (fields instanceof Array) {
fields = fields.join(", ");
}
return fields;
}
2018-02-19 16:41:10 +00:00
triggerChange(doctype, name) {
2018-03-07 10:37:58 +00:00
this.trigger(`change:${doctype}`, {name:name}, 500);
this.trigger(`change`, {doctype:name, name:name}, 500);
2018-02-19 16:41:10 +00:00
}
async insert(doctype, doc) {
2018-02-12 12:01:31 +00:00
let meta = frappe.getMeta(doctype);
// insert parent
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.insertOne(doctype, doc);
}
// insert children
2018-02-12 12:01:31 +00:00
await this.insertChildren(meta, doc, doctype);
2018-02-19 16:41:10 +00:00
this.triggerChange(doctype, doc.name);
2018-02-12 12:01:31 +00:00
return doc;
}
2018-02-19 16:41:10 +00:00
2018-02-12 12:01:31 +00:00
async insertChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
let idx = 0;
for (let child of (doc[field.fieldname] || [])) {
this.prepareChild(doctype, doc.name, child, field, idx);
await this.insertOne(field.childtype, child);
idx++;
}
}
}
async insertOne(doctype, doc) {
// insert into {doctype} ({fields}) values ({values})
}
async update(doctype, doc) {
2018-02-12 12:01:31 +00:00
let meta = frappe.getMeta(doctype);
// update parent
2018-02-12 12:01:31 +00:00
if (meta.isSingle) {
await this.updateSingle(meta, doc, doctype);
} else {
await this.updateOne(doctype, doc);
}
// insert or update children
2018-02-12 12:01:31 +00:00
await this.updateChildren(meta, doc, doctype);
2018-02-19 16:41:10 +00:00
this.triggerChange(doctype, doc.name);
2018-02-12 12:01:31 +00:00
return doc;
}
2018-02-12 12:01:31 +00:00
async updateChildren(meta, doc, doctype) {
let tableFields = meta.getTableFields();
for (let field of tableFields) {
// first key is "parent" - for SQL params
let added = [doc.name];
for (let child of (doc[field.fieldname] || [])) {
this.prepareChild(doctype, doc.name, child, field, added.length - 1);
if (await this.exists(field.childtype, child.name)) {
await this.updateOne(field.childtype, child);
2018-02-12 12:01:31 +00:00
}
else {
await this.insertOne(field.childtype, child);
}
added.push(child.name);
}
await this.runDeleteOtherChildren(field, added);
}
}
async updateOne(doctype, doc) {
// update {doctype} set {field=value} where name=?
}
async runDeleteOtherChildren(field, added) {
// delete from doctype where parent = ? and name not in (?, ?, ?)
}
2018-02-12 12:01:31 +00:00
async updateSingle(meta, doc, doctype) {
await this.deleteSingleValues();
for (let field of meta.getValidFields({withChildren: false})) {
let value = doc[field.fieldname];
if (value) {
let singleValue = frappe.newDoc({
doctype: 'SingleValue',
2018-02-12 12:01:31 +00:00
parent: doctype,
fieldname: field.fieldname,
value: value
})
await singleValue.insert();
}
}
}
async deleteSingleValues(name) {
// await frappe.db.run('delete from SingleValue where parent=?', name)
2018-02-12 12:01:31 +00:00
}
prepareChild(parenttype, parent, child, field, idx) {
if (!child.name) {
2018-03-05 16:45:21 +00:00
child.name = frappe.getRandomString();
}
child.parent = parent;
child.parenttype = parenttype;
child.parentfield = field.fieldname;
child.idx = idx;
}
getKeys(doctype) {
2018-02-12 12:01:31 +00:00
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
}
getFormattedValues(fields, doc) {
let values = fields.map(field => {
let value = doc[field.fieldname];
if (value instanceof Date) {
2018-02-14 12:50:56 +00:00
if (field.fieldtype==='Date') {
// date
return value.toISOString().substr(0, 10);
} else {
// datetime
return value.toISOString();
}
2018-03-07 10:37:58 +00:00
} else if (field.fieldtype === 'Link' && !value) {
// empty value must be null to satisfy
// foreign key constraint
return null;
} else {
return value;
}
});
return values;
}
2018-02-08 09:38:47 +00:00
async deleteMany(doctype, names) {
for (const name of names) {
await this.delete(doctype, name);
}
}
async delete(doctype, name) {
await this.deleteOne(doctype, name);
// delete children
let tableFields = frappe.getMeta(doctype).getTableFields();
for (let field of tableFields) {
await this.deleteChildren(field.childtype, name);
}
2018-02-19 16:41:10 +00:00
this.triggerChange(doctype, name);
}
async deleteOne(doctype, name) {
// delete from {doctype} where name = ?
}
async deleteChildren(parenttype, parent) {
// delete from {parenttype} where parent = ?
}
async exists(doctype, name) {
2018-02-08 09:38:47 +00:00
return (await this.getValue(doctype, name)) ? true : false;
}
2018-02-08 09:38:47 +00:00
async getValue(doctype, filters, fieldname = 'name') {
if (typeof filters === 'string') {
filters = { name: filters };
}
let row = await this.getAll({
doctype: doctype,
fields: [fieldname],
filters: filters,
start: 0,
limit: 1,
2018-04-26 10:19:43 +00:00
orderBy: 'name',
order: 'asc'
});
return row.length ? row[0][fieldname] : null;
}
2018-04-26 10:19:43 +00:00
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) {
// select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit}
}
getFilterConditions(filters) {
// {"status": "Open"} => `status = "Open"`
// {"status": "Open", "name": ["like", "apple%"]}
// => `status="Open" and name like "apple%"
// {"date": [">=", "2017-09-09", "<=", "2017-11-01"]}
// => `date >= 2017-09-09 and date <= 2017-11-01`
let filtersArray = [];
for (let key in filters) {
let value = filters[key];
let field = key;
let operator = '=';
let comparisonValue = value;
if (Array.isArray(value)) {
operator = value[0];
comparisonValue = value[1];
operator = operator.toLowerCase();
if (operator === 'includes') {
operator = 'like';
2018-03-09 13:00:51 +00:00
}
if (['like', 'includes'].includes(operator) && !comparisonValue.includes('%')) {
comparisonValue = `%${comparisonValue}%`;
}
}
filtersArray.push([field, operator, comparisonValue]);
if (Array.isArray(value) && value.length > 2) {
// multiple conditions
let operator = value[2];
let comparisonValue = value[3];
filtersArray.push([field, operator, comparisonValue]);
}
}
let conditions = filtersArray.map(filter => {
const [field, operator, comparisonValue] = filter;
let placeholder = Array.isArray(comparisonValue) ?
comparisonValue.map(v => '?').join(', ') :
'?';
2018-04-24 07:56:28 +00:00
return `ifnull(${field}, '') ${operator} (${placeholder})`;
});
let values = filtersArray.reduce((acc, filter) => {
const comparisonValue = filter[2];
if (Array.isArray(comparisonValue)) {
acc = acc.concat(comparisonValue);
} else {
acc.push(comparisonValue);
}
return acc;
}, []);
return {
conditions: conditions.length ? conditions.join(" and ") : "",
values
};
}
async run(query, params) {
// run query
}
async sql(query, params) {
// run sql
}
async commit() {
// commit
}
initTypeMap() {
this.typeMap = {
2018-03-30 16:56:30 +00:00
'Autocomplete': 'text'
, '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'
2018-03-27 04:20:42 +00:00
, 'DynamicLink': 'text'
, 'Password': 'text'
, 'Select': 'text'
, 'Read Only': 'text'
2018-03-29 18:51:24 +00:00
, 'File': 'text'
, 'Attach': 'text'
, 'Attach Image': 'text'
, 'Signature': 'text'
, 'Color': 'text'
, 'Barcode': 'text'
, 'Geolocation': 'text'
}
}
}