2022-01-20 20:57:29 +00:00
|
|
|
const frappe = require('frappe');
|
|
|
|
const Observable = require('frappe/utils/observable');
|
|
|
|
const CacheManager = require('frappe/utils/cacheManager');
|
2019-12-09 19:57:26 +00:00
|
|
|
const Knex = require('knex');
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-02-19 16:41:10 +00:00
|
|
|
module.exports = class Database extends Observable {
|
2018-08-18 15:54:17 +00:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.initTypeMap();
|
2019-12-09 19:57:26 +00:00
|
|
|
this.connectionParams = {};
|
2019-12-18 18:21:45 +00:00
|
|
|
this.cache = new CacheManager();
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
connect() {
|
|
|
|
this.knex = Knex(this.connectionParams);
|
2021-11-30 09:20:54 +00:00
|
|
|
this.knex.on('query-error', (error) => {
|
2019-12-09 19:57:26 +00:00
|
|
|
error.type = this.getError(error);
|
|
|
|
});
|
2021-11-30 09:20:54 +00:00
|
|
|
this.executePostDbConnect();
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
2019-12-09 19:57:26 +00:00
|
|
|
//
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async migrate() {
|
|
|
|
for (let doctype in frappe.models) {
|
|
|
|
// check if controller module
|
|
|
|
let meta = frappe.getMeta(doctype);
|
2019-10-05 21:47:09 +00:00
|
|
|
let baseDoctype = meta.getBaseDocType();
|
2018-08-18 15:54:17 +00:00
|
|
|
if (!meta.isSingle) {
|
2019-10-05 21:47:09 +00:00
|
|
|
if (await this.tableExists(baseDoctype)) {
|
|
|
|
await this.alterTable(baseDoctype);
|
2018-02-12 12:01:31 +00:00
|
|
|
} else {
|
2019-10-05 21:47:09 +00:00
|
|
|
await this.createTable(baseDoctype);
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
await this.commit();
|
2019-12-18 18:22:47 +00:00
|
|
|
await this.initializeSingles();
|
|
|
|
}
|
|
|
|
|
|
|
|
async initializeSingles() {
|
|
|
|
let singleDoctypes = frappe
|
2021-11-30 09:20:54 +00:00
|
|
|
.getModels((model) => model.isSingle)
|
|
|
|
.map((model) => model.name);
|
2019-12-18 18:22:47 +00:00
|
|
|
|
|
|
|
for (let doctype of singleDoctypes) {
|
|
|
|
if (await this.singleExists(doctype)) {
|
2021-11-30 09:20:54 +00:00
|
|
|
const singleValues = await this.getSingleFieldsToInsert(doctype);
|
|
|
|
singleValues.forEach(({ fieldname, value }) => {
|
|
|
|
let singleValue = frappe.newDoc({
|
|
|
|
doctype: 'SingleValue',
|
|
|
|
parent: doctype,
|
|
|
|
fieldname,
|
|
|
|
value,
|
|
|
|
});
|
|
|
|
singleValue.insert();
|
|
|
|
});
|
2019-12-18 18:22:47 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let meta = frappe.getMeta(doctype);
|
2021-11-30 09:20:54 +00:00
|
|
|
if (meta.fields.every((df) => df.default == null)) {
|
2019-12-18 18:22:47 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let defaultValues = meta.fields.reduce((doc, df) => {
|
|
|
|
if (df.default != null) {
|
|
|
|
doc[df.fieldname] = df.default;
|
|
|
|
}
|
|
|
|
return doc;
|
|
|
|
}, {});
|
|
|
|
await this.updateSingle(doctype, defaultValues);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async singleExists(doctype) {
|
|
|
|
let res = await this.knex('SingleValue')
|
|
|
|
.count('parent as count')
|
|
|
|
.where('parent', doctype)
|
|
|
|
.first();
|
|
|
|
return res.count > 0;
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2021-11-30 09:20:54 +00:00
|
|
|
async getSingleFieldsToInsert(doctype) {
|
|
|
|
const existingFields = (
|
|
|
|
await frappe.db
|
|
|
|
.knex('SingleValue')
|
|
|
|
.where({ parent: doctype })
|
|
|
|
.select('fieldname')
|
|
|
|
).map(({ fieldname }) => fieldname);
|
|
|
|
|
|
|
|
return frappe
|
|
|
|
.getMeta(doctype)
|
|
|
|
.fields.map(({ fieldname, default: value }) => ({
|
|
|
|
fieldname,
|
|
|
|
value,
|
|
|
|
}))
|
|
|
|
.filter(
|
|
|
|
({ fieldname, value }) =>
|
|
|
|
!existingFields.includes(fieldname) && value !== undefined
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
tableExists(table) {
|
|
|
|
return this.knex.schema.hasTable(table);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-12 12:01:31 +00:00
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
async createTable(doctype, tableName = null) {
|
|
|
|
let fields = this.getValidFields(doctype);
|
|
|
|
return await this.runCreateTableQuery(tableName || doctype, fields);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
runCreateTableQuery(doctype, fields) {
|
2021-11-30 09:20:54 +00:00
|
|
|
return this.knex.schema.createTable(doctype, (table) => {
|
2019-12-09 19:57:26 +00:00
|
|
|
for (let field of fields) {
|
|
|
|
this.buildColumnForTable(table, field);
|
|
|
|
}
|
|
|
|
});
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-12 12:01:31 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async alterTable(doctype) {
|
|
|
|
// get columns
|
|
|
|
let diff = await this.getColumnDiff(doctype);
|
|
|
|
let newForeignKeys = await this.getNewForeignKeys(doctype);
|
2018-02-19 16:41:10 +00:00
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
return this.knex.schema
|
2021-11-30 09:20:54 +00:00
|
|
|
.table(doctype, (table) => {
|
2019-12-09 19:57:26 +00:00
|
|
|
if (diff.added.length) {
|
|
|
|
for (let field of diff.added) {
|
|
|
|
this.buildColumnForTable(table, field);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (diff.removed.length) {
|
|
|
|
this.removeColumns(doctype, diff.removed);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
if (newForeignKeys.length) {
|
|
|
|
return this.addForeignKeys(doctype, newForeignKeys);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
buildColumnForTable(table, field) {
|
|
|
|
let columnType = this.getColumnType(field);
|
2021-11-08 09:44:02 +00:00
|
|
|
if (!columnType) {
|
|
|
|
// In case columnType is "Table"
|
|
|
|
// childTable links are handled using the childTable's "parent" field
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
let column = table[columnType](field.fieldname);
|
|
|
|
|
|
|
|
// primary key
|
|
|
|
if (field.fieldname === 'name') {
|
|
|
|
column.primary();
|
|
|
|
}
|
|
|
|
|
|
|
|
// default value
|
2021-11-08 09:44:02 +00:00
|
|
|
if (!!field.default && !(field.default instanceof Function)) {
|
2019-12-09 19:57:26 +00:00
|
|
|
column.defaultTo(field.default);
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
// required
|
2021-12-29 06:13:39 +00:00
|
|
|
if (
|
|
|
|
(!!field.required && !(field.required instanceof Function)) ||
|
|
|
|
field.fieldtype === 'Currency'
|
|
|
|
) {
|
2019-12-09 19:57:26 +00:00
|
|
|
column.notNullable();
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
// link
|
2019-12-12 14:53:24 +00:00
|
|
|
if (field.fieldtype === 'Link' && field.target) {
|
2019-12-09 19:57:26 +00:00
|
|
|
let meta = frappe.getMeta(field.target);
|
2019-12-12 14:53:24 +00:00
|
|
|
table
|
2019-12-09 19:57:26 +00:00
|
|
|
.foreign(field.fieldname)
|
|
|
|
.references('name')
|
|
|
|
.inTable(meta.getBaseDocType())
|
|
|
|
.onUpdate('CASCADE')
|
|
|
|
.onDelete('RESTRICT');
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async getColumnDiff(doctype) {
|
|
|
|
const tableColumns = await this.getTableColumns(doctype);
|
2019-12-09 19:57:26 +00:00
|
|
|
const validFields = this.getValidFields(doctype);
|
2018-08-18 15:54:17 +00:00
|
|
|
const diff = { added: [], removed: [] };
|
2018-02-12 12:01:31 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
for (let field of validFields) {
|
2019-12-09 19:57:26 +00:00
|
|
|
if (
|
|
|
|
!tableColumns.includes(field.fieldname) &&
|
|
|
|
this.getColumnType(field)
|
|
|
|
) {
|
2018-08-18 15:54:17 +00:00
|
|
|
diff.added.push(field);
|
|
|
|
}
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2021-11-30 09:20:54 +00:00
|
|
|
const validFieldNames = validFields.map((field) => field.fieldname);
|
2018-08-18 15:54:17 +00:00
|
|
|
for (let column of tableColumns) {
|
|
|
|
if (!validFieldNames.includes(column)) {
|
|
|
|
diff.removed.push(column);
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
return diff;
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async removeColumns(doctype, removed) {
|
|
|
|
for (let column of removed) {
|
|
|
|
await this.runRemoveColumnQuery(doctype, column);
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-12 12:01:31 +00:00
|
|
|
|
2018-08-18 15:54:17 +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 })) {
|
2019-12-09 19:57:26 +00:00
|
|
|
if (
|
|
|
|
field.fieldtype === 'Link' &&
|
|
|
|
!foreignKeys.includes(field.fieldname)
|
|
|
|
) {
|
2018-08-18 15:54:17 +00:00
|
|
|
newForeignKeys.push(field);
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
return newForeignKeys;
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async addForeignKeys(doctype, newForeignKeys) {
|
|
|
|
for (let field of newForeignKeys) {
|
|
|
|
this.addForeignKey(doctype, field);
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
async getForeignKeys(doctype, field) {
|
2019-12-11 12:27:25 +00:00
|
|
|
return [];
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 09:38:47 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async getTableColumns(doctype) {
|
|
|
|
return [];
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-08-18 15:54:17 +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');
|
|
|
|
}
|
|
|
|
doc = await this.getOne(doctype, name, fields);
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2019-12-11 12:27:25 +00:00
|
|
|
if (!doc) {
|
|
|
|
return;
|
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
await this.loadChildren(doc, meta);
|
|
|
|
return doc;
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async loadChildren(doc, meta) {
|
|
|
|
// load children
|
|
|
|
let tableFields = meta.getTableFields();
|
|
|
|
for (let field of tableFields) {
|
|
|
|
doc[field.fieldname] = await this.getAll({
|
|
|
|
doctype: field.childtype,
|
2019-12-09 19:57:26 +00:00
|
|
|
fields: ['*'],
|
2018-08-18 15:54:17 +00:00
|
|
|
filters: { parent: doc.name },
|
|
|
|
orderBy: 'idx',
|
2021-11-30 09:20:54 +00:00
|
|
|
order: 'asc',
|
2018-08-18 15:54:17 +00:00
|
|
|
});
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async getSingle(doctype) {
|
|
|
|
let values = await this.getAll({
|
|
|
|
doctype: 'SingleValue',
|
|
|
|
fields: ['fieldname', 'value'],
|
|
|
|
filters: { parent: doctype },
|
|
|
|
orderBy: 'fieldname',
|
2021-11-30 09:20:54 +00:00
|
|
|
order: 'asc',
|
2018-08-18 15:54:17 +00:00
|
|
|
});
|
|
|
|
let doc = {};
|
|
|
|
for (let row of values) {
|
|
|
|
doc[row.fieldname] = row.value;
|
|
|
|
}
|
|
|
|
return doc;
|
|
|
|
}
|
2018-02-08 06:46:38 +00:00
|
|
|
|
2021-11-30 09:20:54 +00:00
|
|
|
/**
|
|
|
|
* Get list of values from the singles table.
|
|
|
|
* @param {...string | Object} fieldnames list of fieldnames to get the values of
|
|
|
|
* @returns {Array<Object>} array of {parent, value, fieldname}.
|
|
|
|
* @example
|
|
|
|
* Database.getSingleValues('internalPrecision');
|
|
|
|
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
|
|
|
* @example
|
|
|
|
* Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'});
|
|
|
|
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
|
|
|
*/
|
|
|
|
async getSingleValues(...fieldnames) {
|
|
|
|
fieldnames = fieldnames.map((fieldname) => {
|
|
|
|
if (typeof fieldname === 'string') {
|
|
|
|
return { fieldname };
|
|
|
|
}
|
|
|
|
return fieldname;
|
|
|
|
});
|
|
|
|
|
|
|
|
let builder = frappe.db.knex('SingleValue');
|
|
|
|
builder = builder.where(fieldnames[0]);
|
|
|
|
|
|
|
|
fieldnames.slice(1).forEach(({ fieldname, parent }) => {
|
|
|
|
if (typeof parent === 'undefined') {
|
|
|
|
builder = builder.orWhere({ fieldname });
|
|
|
|
} else {
|
|
|
|
builder = builder.orWhere({ fieldname, parent });
|
|
|
|
}
|
|
|
|
});
|
2021-12-23 07:28:17 +00:00
|
|
|
|
2022-01-17 05:43:42 +00:00
|
|
|
let values = [];
|
|
|
|
try {
|
|
|
|
values = await builder.select('fieldname', 'value', 'parent');
|
|
|
|
} catch (error) {
|
|
|
|
if (error.message.includes('no such table')) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}
|
2021-12-23 07:28:17 +00:00
|
|
|
|
|
|
|
return values.map((value) => {
|
|
|
|
const fields = frappe.getMeta(value.parent).fields;
|
|
|
|
return this.getDocFormattedDoc(fields, values);
|
|
|
|
});
|
2021-11-30 09:20:54 +00:00
|
|
|
}
|
|
|
|
|
2021-12-23 07:28:17 +00:00
|
|
|
async getOne(doctype, name, fields = '*') {
|
2019-12-09 19:57:26 +00:00
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
let baseDoctype = meta.getBaseDocType();
|
2018-08-18 15:54:17 +00:00
|
|
|
|
2021-12-23 07:28:17 +00:00
|
|
|
const doc = await this.knex
|
2019-12-09 19:57:26 +00:00
|
|
|
.select(fields)
|
|
|
|
.from(baseDoctype)
|
|
|
|
.where('name', name)
|
|
|
|
.first();
|
2021-12-23 07:28:17 +00:00
|
|
|
|
|
|
|
if (!doc) {
|
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.getDocFormattedDoc(meta.fields, doc);
|
|
|
|
}
|
|
|
|
|
|
|
|
getDocFormattedDoc(fields, doc) {
|
|
|
|
// format for usage, not going into the db
|
|
|
|
const docFields = Object.keys(doc);
|
|
|
|
const filteredFields = fields.filter(({ fieldname }) =>
|
|
|
|
docFields.includes(fieldname)
|
|
|
|
);
|
|
|
|
|
|
|
|
const formattedValues = filteredFields.reduce((d, field) => {
|
|
|
|
const { fieldname } = field;
|
|
|
|
d[fieldname] = this.getDocFormattedValues(field, doc[fieldname]);
|
|
|
|
return d;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
return Object.assign(doc, formattedValues);
|
|
|
|
}
|
|
|
|
|
|
|
|
getDocFormattedValues(field, value) {
|
|
|
|
// format for usage, not going into the db
|
2021-12-29 05:53:16 +00:00
|
|
|
try {
|
|
|
|
if (field.fieldtype === 'Currency') {
|
|
|
|
return frappe.pesa(value);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${
|
|
|
|
field.fieldname
|
|
|
|
}', label: '${field.label}'`;
|
|
|
|
throw err;
|
2021-12-23 07:28:17 +00:00
|
|
|
}
|
|
|
|
return value;
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
triggerChange(doctype, name) {
|
2018-11-09 19:07:14 +00:00
|
|
|
this.trigger(`change:${doctype}`, { name }, 500);
|
|
|
|
this.trigger(`change`, { doctype, name }, 500);
|
2019-10-05 21:47:09 +00:00
|
|
|
// also trigger change for basedOn doctype
|
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
if (meta.basedOn) {
|
|
|
|
this.triggerChange(meta.basedOn, name);
|
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async insert(doctype, doc) {
|
|
|
|
let meta = frappe.getMeta(doctype);
|
2019-10-05 21:47:09 +00:00
|
|
|
let baseDoctype = meta.getBaseDocType();
|
|
|
|
doc = this.applyBaseDocTypeFilters(doctype, doc);
|
2018-08-18 15:54:17 +00:00
|
|
|
|
|
|
|
// insert parent
|
|
|
|
if (meta.isSingle) {
|
2019-12-18 18:22:47 +00:00
|
|
|
await this.updateSingle(doctype, doc);
|
2018-08-18 15:54:17 +00:00
|
|
|
} else {
|
2019-10-05 21:47:09 +00:00
|
|
|
await this.insertOne(baseDoctype, doc);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// insert children
|
2019-10-05 21:47:09 +00:00
|
|
|
await this.insertChildren(meta, doc, baseDoctype);
|
2018-08-18 15:54:17 +00:00
|
|
|
|
|
|
|
this.triggerChange(doctype, doc.name);
|
|
|
|
|
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
|
|
|
|
async insertChildren(meta, doc, doctype) {
|
|
|
|
let tableFields = meta.getTableFields();
|
|
|
|
for (let field of tableFields) {
|
|
|
|
let idx = 0;
|
2019-12-09 19:57:26 +00:00
|
|
|
for (let child of doc[field.fieldname] || []) {
|
2018-08-18 15:54:17 +00:00
|
|
|
this.prepareChild(doctype, doc.name, child, field, idx);
|
|
|
|
await this.insertOne(field.childtype, child);
|
|
|
|
idx++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
insertOne(doctype, doc) {
|
|
|
|
let fields = this.getValidFields(doctype);
|
|
|
|
|
|
|
|
if (!doc.name) {
|
|
|
|
doc.name = frappe.getRandomString();
|
|
|
|
}
|
|
|
|
|
|
|
|
let formattedDoc = this.getFormattedDoc(fields, doc);
|
|
|
|
return this.knex(doctype).insert(formattedDoc);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async update(doctype, doc) {
|
|
|
|
let meta = frappe.getMeta(doctype);
|
2019-10-05 21:47:09 +00:00
|
|
|
let baseDoctype = meta.getBaseDocType();
|
|
|
|
doc = this.applyBaseDocTypeFilters(doctype, doc);
|
2018-08-18 15:54:17 +00:00
|
|
|
|
|
|
|
// update parent
|
|
|
|
if (meta.isSingle) {
|
2019-12-18 18:22:47 +00:00
|
|
|
await this.updateSingle(doctype, doc);
|
2018-08-18 15:54:17 +00:00
|
|
|
} else {
|
2019-10-05 21:47:09 +00:00
|
|
|
await this.updateOne(baseDoctype, doc);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// insert or update children
|
2019-10-05 21:47:09 +00:00
|
|
|
await this.updateChildren(meta, doc, baseDoctype);
|
2018-08-18 15:54:17 +00:00
|
|
|
|
|
|
|
this.triggerChange(doctype, doc.name);
|
|
|
|
|
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateChildren(meta, doc, doctype) {
|
|
|
|
let tableFields = meta.getTableFields();
|
|
|
|
for (let field of tableFields) {
|
2019-12-09 19:57:26 +00:00
|
|
|
let added = [];
|
|
|
|
for (let child of doc[field.fieldname] || []) {
|
|
|
|
this.prepareChild(doctype, doc.name, child, field, added.length);
|
2018-08-18 15:54:17 +00:00
|
|
|
if (await this.exists(field.childtype, child.name)) {
|
|
|
|
await this.updateOne(field.childtype, child);
|
2019-12-09 19:57:26 +00:00
|
|
|
} else {
|
2018-08-18 15:54:17 +00:00
|
|
|
await this.insertOne(field.childtype, child);
|
|
|
|
}
|
|
|
|
added.push(child.name);
|
|
|
|
}
|
2019-12-09 19:57:26 +00:00
|
|
|
await this.runDeleteOtherChildren(field, doc.name, added);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
updateOne(doctype, doc) {
|
|
|
|
let validFields = this.getValidFields(doctype);
|
2021-11-30 09:20:54 +00:00
|
|
|
let fieldsToUpdate = Object.keys(doc).filter((f) => f !== 'name');
|
|
|
|
let fields = validFields.filter((df) =>
|
2019-12-11 12:27:25 +00:00
|
|
|
fieldsToUpdate.includes(df.fieldname)
|
|
|
|
);
|
2019-12-09 19:57:26 +00:00
|
|
|
let formattedDoc = this.getFormattedDoc(fields, doc);
|
|
|
|
|
|
|
|
return this.knex(doctype)
|
|
|
|
.where('name', doc.name)
|
2019-12-18 18:21:45 +00:00
|
|
|
.update(formattedDoc)
|
|
|
|
.then(() => {
|
|
|
|
let cacheKey = `${doctype}:${doc.name}`;
|
|
|
|
if (this.cache.hexists(cacheKey)) {
|
|
|
|
for (let fieldname in formattedDoc) {
|
|
|
|
let value = formattedDoc[fieldname];
|
|
|
|
this.cache.hset(cacheKey, fieldname, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
runDeleteOtherChildren(field, parent, added) {
|
|
|
|
// delete other children
|
|
|
|
return this.knex(field.childtype)
|
|
|
|
.where('parent', parent)
|
|
|
|
.andWhere('name', 'not in', added)
|
|
|
|
.delete();
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-12-18 18:22:47 +00:00
|
|
|
async updateSingle(doctype, doc) {
|
|
|
|
let meta = frappe.getMeta(doctype);
|
2019-01-12 10:32:46 +00:00
|
|
|
await this.deleteSingleValues(doctype);
|
2018-08-18 15:54:17 +00:00
|
|
|
for (let field of meta.getValidFields({ withChildren: false })) {
|
|
|
|
let value = doc[field.fieldname];
|
2019-12-18 18:22:47 +00:00
|
|
|
if (value != null) {
|
2018-08-18 15:54:17 +00:00
|
|
|
let singleValue = frappe.newDoc({
|
|
|
|
doctype: 'SingleValue',
|
|
|
|
parent: doctype,
|
|
|
|
fieldname: field.fieldname,
|
2021-11-30 09:20:54 +00:00
|
|
|
value: value,
|
2019-12-09 19:57:26 +00:00
|
|
|
});
|
2018-08-18 15:54:17 +00:00
|
|
|
await singleValue.insert();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
deleteSingleValues(name) {
|
2021-11-30 09:20:54 +00:00
|
|
|
return this.knex('SingleValue').where('parent', name).delete();
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-10-05 21:46:14 +00:00
|
|
|
async rename(doctype, oldName, newName) {
|
2019-12-09 19:57:26 +00:00
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
let baseDoctype = meta.getBaseDocType();
|
|
|
|
await this.knex(baseDoctype)
|
|
|
|
.update({ name: newName })
|
2019-12-18 18:21:45 +00:00
|
|
|
.where('name', oldName)
|
|
|
|
.then(() => {
|
|
|
|
this.clearValueCache(doctype, oldName);
|
|
|
|
});
|
2019-12-09 19:57:26 +00:00
|
|
|
await frappe.db.commit();
|
2019-12-11 12:27:25 +00:00
|
|
|
|
|
|
|
this.triggerChange(doctype, newName);
|
2019-10-05 21:46:14 +00:00
|
|
|
}
|
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
prepareChild(parenttype, parent, child, field, idx) {
|
|
|
|
if (!child.name) {
|
|
|
|
child.name = frappe.getRandomString();
|
|
|
|
}
|
|
|
|
child.parent = parent;
|
|
|
|
child.parenttype = parenttype;
|
|
|
|
child.parentfield = field.fieldname;
|
|
|
|
child.idx = idx;
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
getValidFields(doctype) {
|
2018-08-18 15:54:17 +00:00
|
|
|
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
getFormattedDoc(fields, doc) {
|
2021-12-23 07:28:17 +00:00
|
|
|
// format for storage, going into the db
|
2019-12-09 19:57:26 +00:00
|
|
|
let formattedDoc = {};
|
2021-11-30 09:20:54 +00:00
|
|
|
fields.map((field) => {
|
2018-08-18 15:54:17 +00:00
|
|
|
let value = doc[field.fieldname];
|
2019-12-09 19:57:26 +00:00
|
|
|
formattedDoc[field.fieldname] = this.getFormattedValue(field, value);
|
2018-08-18 15:54:17 +00:00
|
|
|
});
|
2019-12-09 19:57:26 +00:00
|
|
|
return formattedDoc;
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getFormattedValue(field, value) {
|
2021-12-23 07:28:17 +00:00
|
|
|
// format for storage, going into the db
|
|
|
|
const type = typeof value;
|
|
|
|
if (field.fieldtype === 'Currency') {
|
|
|
|
let currency = value;
|
|
|
|
|
|
|
|
if (type === 'number' || type === 'string') {
|
|
|
|
currency = frappe.pesa(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
const currencyValue = currency.store;
|
|
|
|
if (typeof currencyValue !== 'string') {
|
|
|
|
throw new Error(
|
|
|
|
`invalid currencyValue '${currencyValue}' of type '${typeof currencyValue}' on converting from '${value}' of type '${type}'`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return currencyValue;
|
|
|
|
}
|
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
if (value instanceof Date) {
|
|
|
|
if (field.fieldtype === 'Date') {
|
|
|
|
// date
|
|
|
|
return value.toISOString().substr(0, 10);
|
|
|
|
} else {
|
|
|
|
// datetime
|
|
|
|
return value.toISOString();
|
|
|
|
}
|
|
|
|
} else if (field.fieldtype === 'Link' && !value) {
|
|
|
|
// empty value must be null to satisfy
|
|
|
|
// foreign key constraint
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-05 21:47:09 +00:00
|
|
|
applyBaseDocTypeFilters(doctype, doc) {
|
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
if (meta.filters) {
|
|
|
|
for (let fieldname in meta.filters) {
|
|
|
|
let value = meta.filters[fieldname];
|
|
|
|
if (typeof value !== 'object') {
|
|
|
|
doc[fieldname] = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
|
2018-08-18 15:54:17 +00:00
|
|
|
async deleteMany(doctype, names) {
|
|
|
|
for (const name of names) {
|
|
|
|
await this.delete(doctype, name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete(doctype, name) {
|
2019-10-05 21:47:09 +00:00
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
let baseDoctype = meta.getBaseDocType();
|
|
|
|
await this.deleteOne(baseDoctype, name);
|
2018-08-18 15:54:17 +00:00
|
|
|
|
|
|
|
// delete children
|
|
|
|
let tableFields = frappe.getMeta(doctype).getTableFields();
|
|
|
|
for (let field of tableFields) {
|
|
|
|
await this.deleteChildren(field.childtype, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.triggerChange(doctype, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
async deleteOne(doctype, name) {
|
2019-12-09 19:57:26 +00:00
|
|
|
return this.knex(doctype)
|
|
|
|
.where('name', name)
|
2019-12-18 18:21:45 +00:00
|
|
|
.delete()
|
|
|
|
.then(() => {
|
|
|
|
this.clearValueCache(doctype, name);
|
|
|
|
});
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
deleteChildren(parenttype, parent) {
|
2021-11-30 09:20:54 +00:00
|
|
|
return this.knex(parenttype).where('parent', parent).delete();
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async exists(doctype, name) {
|
|
|
|
return (await this.getValue(doctype, name)) ? true : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getValue(doctype, filters, fieldname = 'name') {
|
2019-10-05 21:47:09 +00:00
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
let baseDoctype = meta.getBaseDocType();
|
2018-08-18 15:54:17 +00:00
|
|
|
if (typeof filters === 'string') {
|
|
|
|
filters = { name: filters };
|
|
|
|
}
|
2019-10-05 21:47:09 +00:00
|
|
|
if (meta.filters) {
|
|
|
|
Object.assign(filters, meta.filters);
|
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
|
|
|
|
let row = await this.getAll({
|
2019-10-05 21:47:09 +00:00
|
|
|
doctype: baseDoctype,
|
2018-08-18 15:54:17 +00:00
|
|
|
fields: [fieldname],
|
|
|
|
filters: filters,
|
|
|
|
start: 0,
|
|
|
|
limit: 1,
|
|
|
|
orderBy: 'name',
|
2021-11-30 09:20:54 +00:00
|
|
|
order: 'asc',
|
2018-08-18 15:54:17 +00:00
|
|
|
});
|
|
|
|
return row.length ? row[0][fieldname] : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async setValue(doctype, name, fieldname, value) {
|
|
|
|
return await this.setValues(doctype, name, {
|
2021-11-30 09:20:54 +00:00
|
|
|
[fieldname]: value,
|
2018-08-18 15:54:17 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async setValues(doctype, name, fieldValuePair) {
|
2019-12-09 19:57:26 +00:00
|
|
|
let doc = Object.assign({}, fieldValuePair, { name });
|
|
|
|
return this.updateOne(doctype, doc);
|
|
|
|
}
|
|
|
|
|
2019-12-18 18:21:45 +00:00
|
|
|
async getCachedValue(doctype, name, fieldname) {
|
|
|
|
let value = this.cache.hget(`${doctype}:${name}`, fieldname);
|
|
|
|
if (value == null) {
|
|
|
|
value = await this.getValue(doctype, name, fieldname);
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2021-12-23 07:28:17 +00:00
|
|
|
async getAll({
|
2019-12-09 19:57:26 +00:00
|
|
|
doctype,
|
|
|
|
fields,
|
|
|
|
filters,
|
|
|
|
start,
|
|
|
|
limit,
|
|
|
|
groupBy,
|
|
|
|
orderBy = 'creation',
|
2021-11-30 09:20:54 +00:00
|
|
|
order = 'desc',
|
2019-12-09 19:57:26 +00:00
|
|
|
} = {}) {
|
|
|
|
let meta = frappe.getMeta(doctype);
|
|
|
|
let baseDoctype = meta.getBaseDocType();
|
|
|
|
if (!fields) {
|
|
|
|
fields = meta.getKeywordFields();
|
|
|
|
fields.push('name');
|
|
|
|
}
|
|
|
|
if (typeof fields === 'string') {
|
|
|
|
fields = [fields];
|
|
|
|
}
|
|
|
|
if (meta.filters) {
|
|
|
|
filters = Object.assign({}, filters, meta.filters);
|
|
|
|
}
|
|
|
|
|
|
|
|
let builder = this.knex.select(fields).from(baseDoctype);
|
|
|
|
|
|
|
|
this.applyFiltersToBuilder(builder, filters);
|
|
|
|
|
|
|
|
if (orderBy) {
|
|
|
|
builder.orderBy(orderBy, order);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (groupBy) {
|
|
|
|
builder.groupBy(groupBy);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (start) {
|
|
|
|
builder.offset(start);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (limit) {
|
|
|
|
builder.limit(limit);
|
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
|
2021-12-23 07:28:17 +00:00
|
|
|
const docs = await builder;
|
|
|
|
return docs.map((doc) => this.getDocFormattedDoc(meta.fields, doc));
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
applyFiltersToBuilder(builder, filters) {
|
2018-08-18 15:54:17 +00:00
|
|
|
// {"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 = [];
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
for (let field in filters) {
|
|
|
|
let value = filters[field];
|
2018-08-18 15:54:17 +00:00
|
|
|
let operator = '=';
|
|
|
|
let comparisonValue = value;
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
operator = value[0];
|
|
|
|
comparisonValue = value[1];
|
|
|
|
operator = operator.toLowerCase();
|
|
|
|
|
|
|
|
if (operator === 'includes') {
|
|
|
|
operator = 'like';
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-04-19 14:31:17 +00:00
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
if (operator === 'like' && !comparisonValue.includes('%')) {
|
2018-08-18 15:54:17 +00:00
|
|
|
comparisonValue = `%${comparisonValue}%`;
|
2018-02-08 06:46:38 +00:00
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-30 09:20:54 +00:00
|
|
|
filtersArray.map((filter) => {
|
2018-08-18 15:54:17 +00:00
|
|
|
const [field, operator, comparisonValue] = filter;
|
2019-12-11 12:27:25 +00:00
|
|
|
if (operator === '=') {
|
|
|
|
builder.where(field, comparisonValue);
|
|
|
|
} else {
|
|
|
|
builder.where(field, operator, comparisonValue);
|
|
|
|
}
|
2018-08-18 15:54:17 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
run(query, params) {
|
2018-08-18 15:54:17 +00:00
|
|
|
// run query
|
2019-12-09 19:57:26 +00:00
|
|
|
return this.sql(query, params);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
sql(query, params) {
|
2018-08-18 15:54:17 +00:00
|
|
|
// run sql
|
2019-12-09 19:57:26 +00:00
|
|
|
return this.knex.raw(query, params);
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async commit() {
|
2019-12-09 19:57:26 +00:00
|
|
|
try {
|
|
|
|
await this.sql('commit');
|
|
|
|
} catch (e) {
|
|
|
|
if (e.type !== frappe.errors.CannotCommitError) {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-18 18:21:45 +00:00
|
|
|
clearValueCache(doctype, name) {
|
|
|
|
let cacheKey = `${doctype}:${name}`;
|
|
|
|
this.cache.hclear(cacheKey);
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:57:26 +00:00
|
|
|
getColumnType(field) {
|
|
|
|
return this.typeMap[field.fieldtype];
|
|
|
|
}
|
|
|
|
|
|
|
|
getError(err) {
|
|
|
|
return frappe.errors.DatabaseError;
|
2018-08-18 15:54:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
initTypeMap() {
|
2019-12-09 19:57:26 +00:00
|
|
|
this.typeMap = {};
|
|
|
|
}
|
2021-11-30 09:20:54 +00:00
|
|
|
|
|
|
|
executePostDbConnect() {
|
|
|
|
frappe.initializeMoneyMaker();
|
|
|
|
}
|
2019-12-09 19:57:26 +00:00
|
|
|
};
|