mirror of
https://github.com/frappe/books.git
synced 2024-12-23 19:39:07 +00:00
File (#80)
File doctype - Upload files using multer - uploadFiles API in http.js - Link File to doctype with name and field - Refactor File component for fullpaths - frappe.db.setValue(s) API
This commit is contained in:
parent
1aabf7ef40
commit
375325d917
@ -2,499 +2,513 @@ const frappe = require('frappejs');
|
|||||||
const Observable = require('frappejs/utils/observable');
|
const Observable = require('frappejs/utils/observable');
|
||||||
|
|
||||||
module.exports = class Database extends Observable {
|
module.exports = class Database extends Observable {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.initTypeMap();
|
this.initTypeMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
// this.conn
|
// this.conn
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.conn.close();
|
this.conn.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate() {
|
async migrate() {
|
||||||
for (let doctype in frappe.models) {
|
for (let doctype in frappe.models) {
|
||||||
// check if controller module
|
// check if controller module
|
||||||
let meta = frappe.getMeta(doctype);
|
let meta = frappe.getMeta(doctype);
|
||||||
if (!meta.isSingle) {
|
if (!meta.isSingle) {
|
||||||
if (await this.tableExists(doctype)) {
|
if (await this.tableExists(doctype)) {
|
||||||
await this.alterTable(doctype);
|
await this.alterTable(doctype);
|
||||||
} else {
|
|
||||||
await this.createTable(doctype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTable(doctype, newName=null) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let columns = [];
|
|
||||||
let indexes = [];
|
|
||||||
|
|
||||||
for (let field of meta.getValidFields({ withChildren: false })) {
|
|
||||||
if (this.typeMap[field.fieldtype]) {
|
|
||||||
this.updateColumnDefinition(field, columns, indexes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.runCreateTableQuery(newName || doctype, columns, indexes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async tableExists(table) {
|
|
||||||
// return true if table exists
|
|
||||||
}
|
|
||||||
|
|
||||||
async runCreateTableQuery(doctype, columns, indexes) {
|
|
||||||
// override
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
let diff = await this.getColumnDiff(doctype);
|
|
||||||
let newForeignKeys = await this.getNewForeignKeys(doctype);
|
|
||||||
|
|
||||||
if (diff.added.length) {
|
|
||||||
await this.addColumns(doctype, diff.added);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diff.removed.length) {
|
|
||||||
await this.removeColumns(doctype, diff.removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newForeignKeys.length) {
|
|
||||||
await this.addForeignKeys(doctype, newForeignKeys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]) {
|
|
||||||
diff.added.push(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validFieldNames = validFields.map(field => field.fieldname);
|
|
||||||
for (let column of tableColumns) {
|
|
||||||
if (!validFieldNames.includes(column)) {
|
|
||||||
diff.removed.push(column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addColumns(doctype, added) {
|
|
||||||
for (let field of added) {
|
|
||||||
await this.runAddColumnQuery(doctype, field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeColumns(doctype, removed) {
|
|
||||||
for (let column of removed) {
|
|
||||||
await this.runRemoveColumnQuery(doctype, column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async runAddColumnQuery(doctype, field) {
|
|
||||||
// alter table {doctype} add column ({column_def});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
if (!name) {
|
await this.createTable(doctype);
|
||||||
throw new frappe.errors.ValueError('name is mandatory');
|
|
||||||
}
|
|
||||||
doc = await this.getOne(doctype, name, fields);
|
|
||||||
}
|
}
|
||||||
await this.loadChildren(doc, meta);
|
}
|
||||||
return doc;
|
}
|
||||||
|
await this.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTable(doctype, newName = null) {
|
||||||
|
let meta = frappe.getMeta(doctype);
|
||||||
|
let columns = [];
|
||||||
|
let indexes = [];
|
||||||
|
|
||||||
|
for (let field of meta.getValidFields({ withChildren: false })) {
|
||||||
|
if (this.typeMap[field.fieldtype]) {
|
||||||
|
this.updateColumnDefinition(field, columns, indexes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadChildren(doc, meta) {
|
return await this.runCreateTableQuery(newName || doctype, columns, indexes);
|
||||||
// load children
|
}
|
||||||
let tableFields = meta.getTableFields();
|
|
||||||
for (let field of tableFields) {
|
async tableExists(table) {
|
||||||
doc[field.fieldname] = await this.getAll({
|
// return true if table exists
|
||||||
doctype: field.childtype,
|
}
|
||||||
fields: ["*"],
|
|
||||||
filters: { parent: doc.name },
|
async runCreateTableQuery(doctype, columns, indexes) {
|
||||||
orderBy: 'idx',
|
// override
|
||||||
order: 'asc'
|
}
|
||||||
});
|
|
||||||
|
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
|
||||||
|
let diff = await this.getColumnDiff(doctype);
|
||||||
|
let newForeignKeys = await this.getNewForeignKeys(doctype);
|
||||||
|
|
||||||
|
if (diff.added.length) {
|
||||||
|
await this.addColumns(doctype, diff.added);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff.removed.length) {
|
||||||
|
await this.removeColumns(doctype, diff.removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newForeignKeys.length) {
|
||||||
|
await this.addForeignKeys(doctype, newForeignKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
diff.added.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validFieldNames = validFields.map(field => field.fieldname);
|
||||||
|
for (let column of tableColumns) {
|
||||||
|
if (!validFieldNames.includes(column)) {
|
||||||
|
diff.removed.push(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addColumns(doctype, added) {
|
||||||
|
for (let field of added) {
|
||||||
|
await this.runAddColumnQuery(doctype, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeColumns(doctype, removed) {
|
||||||
|
for (let column of removed) {
|
||||||
|
await this.runRemoveColumnQuery(doctype, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async runAddColumnQuery(doctype, field) {
|
||||||
|
// alter table {doctype} add column ({column_def});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
await this.loadChildren(doc, meta);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadChildren(doc, meta) {
|
||||||
|
// load children
|
||||||
|
let tableFields = meta.getTableFields();
|
||||||
|
for (let field of tableFields) {
|
||||||
|
doc[field.fieldname] = await this.getAll({
|
||||||
|
doctype: field.childtype,
|
||||||
|
fields: ["*"],
|
||||||
|
filters: { parent: doc.name },
|
||||||
|
orderBy: 'idx',
|
||||||
|
order: 'asc'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSingle(doctype) {
|
||||||
|
let values = await this.getAll({
|
||||||
|
doctype: 'SingleValue',
|
||||||
|
fields: ['fieldname', 'value'],
|
||||||
|
filters: { parent: doctype },
|
||||||
|
orderBy: 'fieldname',
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerChange(doctype, name) {
|
||||||
|
this.trigger(`change:${doctype}`, { name: name }, 500);
|
||||||
|
this.trigger(`change`, { doctype: name, name: name }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(doctype, doc) {
|
||||||
|
let meta = frappe.getMeta(doctype);
|
||||||
|
|
||||||
|
// insert parent
|
||||||
|
if (meta.isSingle) {
|
||||||
|
await this.updateSingle(meta, doc, doctype);
|
||||||
|
} else {
|
||||||
|
await this.insertOne(doctype, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert children
|
||||||
|
await this.insertChildren(meta, doc, doctype);
|
||||||
|
|
||||||
|
this.triggerChange(doctype, doc.name);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let meta = frappe.getMeta(doctype);
|
||||||
|
|
||||||
|
// update parent
|
||||||
|
if (meta.isSingle) {
|
||||||
|
await this.updateSingle(meta, doc, doctype);
|
||||||
|
} else {
|
||||||
|
await this.updateOne(doctype, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert or update children
|
||||||
|
await this.updateChildren(meta, doc, doctype);
|
||||||
|
|
||||||
|
this.triggerChange(doctype, doc.name);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
|
await this.insertOne(field.childtype, child);
|
||||||
async getSingle(doctype) {
|
|
||||||
let values = await this.getAll({
|
|
||||||
doctype: 'SingleValue',
|
|
||||||
fields: ['fieldname', 'value'],
|
|
||||||
filters: { parent: doctype },
|
|
||||||
orderBy: 'fieldname',
|
|
||||||
order: 'asc'
|
|
||||||
});
|
|
||||||
let doc = {};
|
|
||||||
for (let row of values) {
|
|
||||||
doc[row.fieldname] = row.value;
|
|
||||||
}
|
}
|
||||||
return doc;
|
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 (?, ?, ?)
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
parent: doctype,
|
||||||
|
fieldname: field.fieldname,
|
||||||
|
value: value
|
||||||
|
})
|
||||||
|
await singleValue.insert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSingleValues(name) {
|
||||||
|
// await frappe.db.run('delete from SingleValue where parent=?', name)
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeys(doctype) {
|
||||||
|
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormattedValues(fields, doc) {
|
||||||
|
let values = fields.map(field => {
|
||||||
|
let value = doc[field.fieldname];
|
||||||
|
return this.getFormattedValue(field, value);
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormattedValue(field, value) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOne(doctype, name, fields = '*') {
|
this.triggerChange(doctype, name);
|
||||||
// select {fields} form {doctype} where name = ?
|
}
|
||||||
|
|
||||||
|
async deleteOne(doctype, name) {
|
||||||
|
// delete from {doctype} where name = ?
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteChildren(parenttype, parent) {
|
||||||
|
// delete from {parenttype} where parent = ?
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(doctype, name) {
|
||||||
|
return (await this.getValue(doctype, name)) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValue(doctype, filters, fieldname = 'name') {
|
||||||
|
if (typeof filters === 'string') {
|
||||||
|
filters = { name: filters };
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareFields(fields) {
|
let row = await this.getAll({
|
||||||
if (fields instanceof Array) {
|
doctype: doctype,
|
||||||
fields = fields.join(", ");
|
fields: [fieldname],
|
||||||
}
|
filters: filters,
|
||||||
return fields;
|
start: 0,
|
||||||
}
|
limit: 1,
|
||||||
|
orderBy: 'name',
|
||||||
|
order: 'asc'
|
||||||
|
});
|
||||||
|
return row.length ? row[0][fieldname] : null;
|
||||||
|
}
|
||||||
|
|
||||||
triggerChange(doctype, name) {
|
async setValue(doctype, name, fieldname, value) {
|
||||||
this.trigger(`change:${doctype}`, {name:name}, 500);
|
return await this.setValues(doctype, name, {
|
||||||
this.trigger(`change`, {doctype:name, name:name}, 500);
|
[fieldname]: value
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async insert(doctype, doc) {
|
async setValues(doctype, name, fieldValuePair) {
|
||||||
let meta = frappe.getMeta(doctype);
|
//
|
||||||
|
}
|
||||||
|
|
||||||
// insert parent
|
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', order = 'desc' } = {}) {
|
||||||
if (meta.isSingle) {
|
// select {fields} from {doctype} where {filters} order by {orderBy} {order} limit {start} {limit}
|
||||||
await this.updateSingle(meta, doc, doctype);
|
}
|
||||||
} else {
|
|
||||||
await this.insertOne(doctype, doc);
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert children
|
if (['like', 'includes'].includes(operator) && !comparisonValue.includes('%')) {
|
||||||
await this.insertChildren(meta, doc, doctype);
|
comparisonValue = `%${comparisonValue}%`;
|
||||||
|
|
||||||
this.triggerChange(doctype, doc.name);
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertOne(doctype, doc) {
|
let conditions = filtersArray.map(filter => {
|
||||||
// insert into {doctype} ({fields}) values ({values})
|
const [field, operator, comparisonValue] = filter;
|
||||||
}
|
|
||||||
|
let placeholder = Array.isArray(comparisonValue) ?
|
||||||
async update(doctype, doc) {
|
comparisonValue.map(v => '?').join(', ') :
|
||||||
let meta = frappe.getMeta(doctype);
|
'?';
|
||||||
|
|
||||||
// update parent
|
return `ifnull(${field}, '') ${operator} (${placeholder})`;
|
||||||
if (meta.isSingle) {
|
});
|
||||||
await this.updateSingle(meta, doc, doctype);
|
|
||||||
} else {
|
let values = filtersArray.reduce((acc, filter) => {
|
||||||
await this.updateOne(doctype, doc);
|
const comparisonValue = filter[2];
|
||||||
}
|
if (Array.isArray(comparisonValue)) {
|
||||||
|
acc = acc.concat(comparisonValue);
|
||||||
// insert or update children
|
} else {
|
||||||
await this.updateChildren(meta, doc, doctype);
|
acc.push(comparisonValue);
|
||||||
|
}
|
||||||
this.triggerChange(doctype, doc.name);
|
return acc;
|
||||||
|
}, []);
|
||||||
return doc;
|
|
||||||
}
|
return {
|
||||||
|
conditions: conditions.length ? conditions.join(" and ") : "",
|
||||||
async updateChildren(meta, doc, doctype) {
|
values
|
||||||
let tableFields = meta.getTableFields();
|
};
|
||||||
for (let field of tableFields) {
|
}
|
||||||
// first key is "parent" - for SQL params
|
|
||||||
let added = [doc.name];
|
async run(query, params) {
|
||||||
for (let child of (doc[field.fieldname] || [])) {
|
// run query
|
||||||
this.prepareChild(doctype, doc.name, child, field, added.length - 1);
|
}
|
||||||
if (await this.exists(field.childtype, child.name)) {
|
|
||||||
await this.updateOne(field.childtype, child);
|
async sql(query, params) {
|
||||||
}
|
// run sql
|
||||||
else {
|
}
|
||||||
await this.insertOne(field.childtype, child);
|
|
||||||
}
|
async commit() {
|
||||||
added.push(child.name);
|
// commit
|
||||||
}
|
}
|
||||||
await this.runDeleteOtherChildren(field, added);
|
|
||||||
}
|
initTypeMap() {
|
||||||
}
|
this.typeMap = {
|
||||||
|
'Autocomplete': 'text'
|
||||||
async updateOne(doctype, doc) {
|
, 'Currency': 'real'
|
||||||
// update {doctype} set {field=value} where name=?
|
, 'Int': 'integer'
|
||||||
}
|
, 'Float': 'real'
|
||||||
|
, 'Percent': 'real'
|
||||||
async runDeleteOtherChildren(field, added) {
|
, 'Check': 'integer'
|
||||||
// delete from doctype where parent = ? and name not in (?, ?, ?)
|
, 'Small Text': 'text'
|
||||||
}
|
, 'Long Text': 'text'
|
||||||
|
, 'Code': 'text'
|
||||||
async updateSingle(meta, doc, doctype) {
|
, 'Text Editor': 'text'
|
||||||
await this.deleteSingleValues();
|
, 'Date': 'text'
|
||||||
for (let field of meta.getValidFields({withChildren: false})) {
|
, 'Datetime': 'text'
|
||||||
let value = doc[field.fieldname];
|
, 'Time': 'text'
|
||||||
if (value) {
|
, 'Text': 'text'
|
||||||
let singleValue = frappe.newDoc({
|
, 'Data': 'text'
|
||||||
doctype: 'SingleValue',
|
, 'Link': 'text'
|
||||||
parent: doctype,
|
, 'DynamicLink': 'text'
|
||||||
fieldname: field.fieldname,
|
, 'Password': 'text'
|
||||||
value: value
|
, 'Select': 'text'
|
||||||
})
|
, 'Read Only': 'text'
|
||||||
await singleValue.insert();
|
, 'File': 'text'
|
||||||
}
|
, 'Attach': 'text'
|
||||||
}
|
, 'Attach Image': 'text'
|
||||||
}
|
, 'Signature': 'text'
|
||||||
|
, 'Color': 'text'
|
||||||
async deleteSingleValues(name) {
|
, 'Barcode': 'text'
|
||||||
// await frappe.db.run('delete from SingleValue where parent=?', name)
|
, 'Geolocation': 'text'
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeys(doctype) {
|
|
||||||
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedValues(fields, doc) {
|
|
||||||
let values = fields.map(field => {
|
|
||||||
let value = doc[field.fieldname];
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return (await this.getValue(doctype, name)) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
orderBy: 'name',
|
|
||||||
order: 'asc'
|
|
||||||
});
|
|
||||||
return row.length ? row[0][fieldname] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
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(', ') :
|
|
||||||
'?';
|
|
||||||
|
|
||||||
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 = {
|
|
||||||
'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'
|
|
||||||
, 'DynamicLink': 'text'
|
|
||||||
, 'Password': 'text'
|
|
||||||
, 'Select': 'text'
|
|
||||||
, 'Read Only': 'text'
|
|
||||||
, 'File': 'text'
|
|
||||||
, 'Attach': 'text'
|
|
||||||
, 'Attach Image': 'text'
|
|
||||||
, 'Signature': 'text'
|
|
||||||
, 'Color': 'text'
|
|
||||||
, 'Barcode': 'text'
|
|
||||||
, 'Geolocation': 'text'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
333
backends/http.js
333
backends/http.js
@ -2,157 +2,220 @@ const frappe = require('frappejs');
|
|||||||
const Observable = require('frappejs/utils/observable');
|
const Observable = require('frappejs/utils/observable');
|
||||||
|
|
||||||
module.exports = class HTTPClient extends Observable {
|
module.exports = class HTTPClient extends Observable {
|
||||||
constructor({ server, protocol = 'http' }) {
|
constructor({ server, protocol = 'http' }) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.protocol = protocol;
|
this.protocol = protocol;
|
||||||
frappe.config.serverURL = this.getURL();
|
frappe.config.serverURL = this.getURL();
|
||||||
|
|
||||||
// if the backend is http, then always client!
|
// if the backend is http, then always client!
|
||||||
frappe.isServer = false;
|
frappe.isServer = false;
|
||||||
|
|
||||||
this.initTypeMap();
|
this.initTypeMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(doctype, doc) {
|
||||||
|
doc.doctype = doctype;
|
||||||
|
let filesToUpload = this.getFilesToUpload(doc);
|
||||||
|
let url = this.getURL('/api/resource', doctype);
|
||||||
|
|
||||||
|
const responseDoc = await this.fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(doc)
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
|
||||||
|
|
||||||
|
return responseDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(doctype, name) {
|
||||||
|
name = encodeURIComponent(name);
|
||||||
|
let url = this.getURL('/api/resource', doctype, name);
|
||||||
|
return await this.fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.getHeaders()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll({ doctype, fields, filters, start, limit, sortBy, order }) {
|
||||||
|
let url = this.getURL('/api/resource', doctype);
|
||||||
|
|
||||||
|
url = url + '?' + frappe.getQueryString({
|
||||||
|
fields: JSON.stringify(fields),
|
||||||
|
filters: JSON.stringify(filters),
|
||||||
|
start: start,
|
||||||
|
limit: limit,
|
||||||
|
sortBy: sortBy,
|
||||||
|
order: order
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(doctype, doc) {
|
||||||
|
doc.doctype = doctype;
|
||||||
|
let filesToUpload = this.getFilesToUpload(doc);
|
||||||
|
let url = this.getURL('/api/resource', doctype, doc.name);
|
||||||
|
|
||||||
|
const responseDoc = await this.fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(doc)
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.uploadFilesAndUpdateDoc(filesToUpload, doctype, responseDoc);
|
||||||
|
|
||||||
|
return responseDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(doctype, name) {
|
||||||
|
let url = this.getURL('/api/resource', doctype, name);
|
||||||
|
|
||||||
|
return await this.fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMany(doctype, names) {
|
||||||
|
let url = this.getURL('/api/resource', doctype);
|
||||||
|
|
||||||
|
return await this.fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify(names)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(doctype, name) {
|
||||||
|
return (await this.getValue(doctype, name, 'name')) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValue(doctype, name, fieldname) {
|
||||||
|
let url = this.getURL('/api/resource', doctype, name, fieldname);
|
||||||
|
|
||||||
|
return (await this.fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
})).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(url, args) {
|
||||||
|
args.headers = this.getHeaders();
|
||||||
|
let response = await frappe.fetch(url, args);
|
||||||
|
let data = await response.json();
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw Error(data.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
getFilesToUpload(doc) {
|
||||||
|
const meta = frappe.getMeta(doc.doctype);
|
||||||
|
const fileFields = meta.getFieldsWith({ fieldtype: 'File' });
|
||||||
|
const filesToUpload = [];
|
||||||
|
|
||||||
async insert(doctype, doc) {
|
if (fileFields.length > 0) {
|
||||||
doc.doctype = doctype;
|
fileFields.forEach(df => {
|
||||||
let url = this.getURL('/api/resource', doctype);
|
const files = doc[df.fieldname] || [];
|
||||||
return await this.fetch(url, {
|
if (files.length) {
|
||||||
method: 'POST',
|
filesToUpload.push({
|
||||||
body: JSON.stringify(doc)
|
fieldname: df.fieldname,
|
||||||
})
|
files: files
|
||||||
}
|
})
|
||||||
|
|
||||||
async get(doctype, name) {
|
|
||||||
let url = this.getURL('/api/resource', doctype, name);
|
|
||||||
return await this.fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: this.getHeaders()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll({ doctype, fields, filters, start, limit, sort_by, order }) {
|
|
||||||
let url = this.getURL('/api/resource', doctype);
|
|
||||||
|
|
||||||
url = url + "?" + frappe.getQueryString({
|
|
||||||
fields: JSON.stringify(fields),
|
|
||||||
filters: JSON.stringify(filters),
|
|
||||||
start: start,
|
|
||||||
limit: limit,
|
|
||||||
sort_by: sort_by,
|
|
||||||
order: order
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(doctype, doc) {
|
|
||||||
doc.doctype = doctype;
|
|
||||||
let url = this.getURL('/api/resource', doctype, doc.name);
|
|
||||||
|
|
||||||
return await this.fetch(url, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(doc)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(doctype, name) {
|
|
||||||
let url = this.getURL('/api/resource', doctype, name);
|
|
||||||
|
|
||||||
return await this.fetch(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteMany(doctype, names) {
|
|
||||||
let url = this.getURL('/api/resource', doctype);
|
|
||||||
|
|
||||||
return await this.fetch(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
body: JSON.stringify(names)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(doctype, name) {
|
|
||||||
return (await this.getValue(doctype, name, 'name')) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getValue(doctype, name, fieldname) {
|
|
||||||
let url = this.getURL('/api/resource', doctype, name, fieldname);
|
|
||||||
|
|
||||||
return (await this.fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
})).value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch(url, args) {
|
|
||||||
args.headers = this.getHeaders();
|
|
||||||
let response = await frappe.fetch(url, args);
|
|
||||||
let data = await response.json();
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw Error(data.error);
|
|
||||||
}
|
}
|
||||||
|
delete doc[df.fieldname];
|
||||||
return data;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getURL(...parts) {
|
return filesToUpload;
|
||||||
return this.protocol + '://' + this.server + (parts || []).join('/');
|
}
|
||||||
|
|
||||||
|
async uploadFilesAndUpdateDoc(filesToUpload, doctype, doc) {
|
||||||
|
if (filesToUpload.length > 0) {
|
||||||
|
// upload files
|
||||||
|
for (const fileToUpload of filesToUpload) {
|
||||||
|
const files = await this.uploadFiles(fileToUpload.files, doctype, doc.name, fileToUpload.fieldname);
|
||||||
|
doc[fileToUpload.fieldname] = files[0].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFiles(fileList, doctype, name, fieldname) {
|
||||||
|
let url = this.getURL('/api/upload', doctype, name, fieldname);
|
||||||
|
|
||||||
|
let formData = new FormData();
|
||||||
|
for (const file of fileList) {
|
||||||
|
formData.append('files', file, file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaders() {
|
let response = await frappe.fetch(url, {
|
||||||
const headers = {
|
method: 'POST',
|
||||||
'Accept': 'application/json',
|
body: formData
|
||||||
'Content-Type': 'application/json'
|
});
|
||||||
};
|
|
||||||
if (frappe.session && frappe.session.token) {
|
|
||||||
headers.token = frappe.session.token;
|
|
||||||
};
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
initTypeMap() {
|
const data = await response.json();
|
||||||
this.typeMap = {
|
if (response.status !== 200) {
|
||||||
'Autocomplete': true
|
throw Error(data.error);
|
||||||
, '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
|
|
||||||
, 'DynamicLink': true
|
|
||||||
, 'Password': true
|
|
||||||
, 'Select': true
|
|
||||||
, 'Read Only': true
|
|
||||||
, 'File': true
|
|
||||||
, 'Attach': true
|
|
||||||
, 'Attach Image': true
|
|
||||||
, 'Signature': true
|
|
||||||
, 'Color': true
|
|
||||||
, 'Barcode': true
|
|
||||||
, 'Geolocation': true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
getURL(...parts) {
|
||||||
|
return this.protocol + '://' + this.server + (parts || []).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeaders() {
|
||||||
|
const headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
if (frappe.session && frappe.session.token) {
|
||||||
|
headers.token = frappe.session.token;
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
initTypeMap() {
|
||||||
|
this.typeMap = {
|
||||||
|
'Autocomplete': true
|
||||||
|
, '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
|
||||||
|
, 'DynamicLink': true
|
||||||
|
, 'Password': true
|
||||||
|
, 'Select': true
|
||||||
|
, 'Read Only': true
|
||||||
|
, 'File': true
|
||||||
|
, 'Attach': true
|
||||||
|
, 'Attach Image': true
|
||||||
|
, 'Signature': true
|
||||||
|
, 'Color': true
|
||||||
|
, 'Barcode': true
|
||||||
|
, 'Geolocation': true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -4,244 +4,268 @@ const Database = require('./database');
|
|||||||
const debug = false;
|
const debug = false;
|
||||||
|
|
||||||
module.exports = class sqliteDatabase extends Database {
|
module.exports = class sqliteDatabase extends Database {
|
||||||
constructor({ dbPath }) {
|
constructor({ dbPath }) {
|
||||||
super();
|
super();
|
||||||
this.dbPath = dbPath;
|
this.dbPath = dbPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(dbPath) {
|
connect(dbPath) {
|
||||||
if (dbPath) {
|
if (dbPath) {
|
||||||
this.dbPath = dbPath;
|
this.dbPath = dbPath;
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.conn = new sqlite3.Database(this.dbPath, () => {
|
||||||
|
if (debug) {
|
||||||
|
this.conn.on('trace', (trace) => console.log(trace));
|
||||||
}
|
}
|
||||||
return new Promise(resolve => {
|
this.run('PRAGMA foreign_keys=ON').then(resolve);
|
||||||
this.conn = new sqlite3.Database(this.dbPath, () => {
|
});
|
||||||
if (debug) {
|
});
|
||||||
this.conn.on('trace', (trace) => console.log(trace));
|
}
|
||||||
}
|
|
||||||
this.run('PRAGMA foreign_keys=ON').then(resolve);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async tableExists(table) {
|
async tableExists(table) {
|
||||||
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
|
const name = await this.sql(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`);
|
||||||
return (name && name.length) ? true : false;
|
return (name && name.length) ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addForeignKeys(doctype, newForeignKeys) {
|
async addForeignKeys(doctype, newForeignKeys) {
|
||||||
await this.run('PRAGMA foreign_keys=OFF');
|
await this.run('PRAGMA foreign_keys=OFF');
|
||||||
await this.run('BEGIN TRANSACTION');
|
await this.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
const tempName = 'TEMP' + doctype
|
const tempName = 'TEMP' + doctype
|
||||||
|
|
||||||
// create temp table
|
// create temp table
|
||||||
await this.createTable(doctype, tempName);
|
await this.createTable(doctype, tempName);
|
||||||
|
|
||||||
const columns = (await this.getTableColumns(tempName)).join(', ');
|
const columns = (await this.getTableColumns(tempName)).join(', ');
|
||||||
|
|
||||||
// copy from old to new table
|
// copy from old to new table
|
||||||
await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`);
|
await this.run(`INSERT INTO ${tempName} (${columns}) SELECT ${columns} from ${doctype}`);
|
||||||
|
|
||||||
// drop old table
|
// drop old table
|
||||||
await this.run(`DROP TABLE ${doctype}`);
|
await this.run(`DROP TABLE ${doctype}`);
|
||||||
|
|
||||||
// rename new table
|
// rename new table
|
||||||
await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`);
|
await this.run(`ALTER TABLE ${tempName} RENAME TO ${doctype}`);
|
||||||
|
|
||||||
await this.run('COMMIT');
|
await this.run('COMMIT');
|
||||||
await this.run('PRAGMA foreign_keys=ON');
|
await this.run('PRAGMA foreign_keys=ON');
|
||||||
}
|
}
|
||||||
|
|
||||||
removeColumns() {
|
removeColumns() {
|
||||||
// pass
|
// pass
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCreateTableQuery(doctype, columns, indexes) {
|
async runCreateTableQuery(doctype, columns, indexes) {
|
||||||
const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
|
const query = `CREATE TABLE IF NOT EXISTS ${doctype} (
|
||||||
${columns.join(", ")} ${indexes.length ? (", " + indexes.join(", ")) : ''})`;
|
${columns.join(", ")} ${indexes.length ? (", " + indexes.join(", ")) : ''})`;
|
||||||
|
|
||||||
return await this.run(query);
|
return await this.run(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColumnDefinition(field, columns, indexes) {
|
||||||
|
let def = this.getColumnDefinition(field);
|
||||||
|
|
||||||
|
columns.push(def);
|
||||||
|
|
||||||
|
if (field.fieldtype === 'Link' && field.target) {
|
||||||
|
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateColumnDefinition(field, columns, indexes) {
|
getColumnDefinition(field) {
|
||||||
let def = this.getColumnDefinition(field);
|
let def = [
|
||||||
|
field.fieldname,
|
||||||
|
this.typeMap[field.fieldtype],
|
||||||
|
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
|
||||||
|
field.required ? 'NOT NULL' : '',
|
||||||
|
field.default ? `DEFAULT ${field.default}` : ''
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
columns.push(def);
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
if (field.fieldtype==='Link' && field.target) {
|
async getTableColumns(doctype) {
|
||||||
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
|
return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
getColumnDefinition(field) {
|
async getForeignKeys(doctype) {
|
||||||
let def = [
|
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(d => d.from);
|
||||||
field.fieldname,
|
}
|
||||||
this.typeMap[field.fieldtype],
|
|
||||||
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
|
|
||||||
field.required ? 'NOT NULL' : '',
|
|
||||||
field.default ? `DEFAULT ${field.default}` : ''
|
|
||||||
].join(' ');
|
|
||||||
|
|
||||||
return def;
|
async runAddColumnQuery(doctype, field, values) {
|
||||||
}
|
await this.run(`ALTER TABLE ${doctype} ADD COLUMN ${this.getColumnDefinition(field)}`, values);
|
||||||
|
}
|
||||||
|
|
||||||
async getTableColumns(doctype) {
|
getOne(doctype, name, fields = '*') {
|
||||||
return (await this.sql(`PRAGMA table_info(${doctype})`)).map(d => d.name);
|
fields = this.prepareFields(fields);
|
||||||
}
|
return new Promise((resolve, reject) => {
|
||||||
|
this.conn.get(`select ${fields} from ${doctype}
|
||||||
async getForeignKeys(doctype) {
|
|
||||||
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(d => d.from);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runAddColumnQuery(doctype, field, values) {
|
|
||||||
await this.run(`ALTER TABLE ${doctype} ADD COLUMN ${this.getColumnDefinition(field)}`, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
getOne(doctype, name, fields = '*') {
|
|
||||||
fields = this.prepareFields(fields);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.conn.get(`select ${fields} from ${doctype}
|
|
||||||
where name = ?`, name,
|
where name = ?`, name,
|
||||||
(err, row) => {
|
(err, row) => {
|
||||||
resolve(row || {});
|
resolve(row || {});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertOne(doctype, doc) {
|
||||||
|
let fields = this.getKeys(doctype);
|
||||||
|
let placeholders = fields.map(d => '?').join(', ');
|
||||||
|
|
||||||
|
if (!doc.name) {
|
||||||
|
doc.name = frappe.getRandomString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertOne(doctype, doc) {
|
return await this.run(`insert into ${doctype}
|
||||||
let fields = this.getKeys(doctype);
|
|
||||||
let placeholders = fields.map(d => '?').join(', ');
|
|
||||||
|
|
||||||
if (!doc.name) {
|
|
||||||
doc.name = frappe.getRandomString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.run(`insert into ${doctype}
|
|
||||||
(${fields.map(field => field.fieldname).join(", ")})
|
(${fields.map(field => field.fieldname).join(", ")})
|
||||||
values (${placeholders})`, this.getFormattedValues(fields, doc));
|
values (${placeholders})`, this.getFormattedValues(fields, doc));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateOne(doctype, doc) {
|
async updateOne(doctype, doc) {
|
||||||
let fields = this.getKeys(doctype);
|
let fields = this.getKeys(doctype);
|
||||||
let assigns = fields.map(field => `${field.fieldname} = ?`);
|
let assigns = fields.map(field => `${field.fieldname} = ?`);
|
||||||
let values = this.getFormattedValues(fields, doc);
|
let values = this.getFormattedValues(fields, doc);
|
||||||
|
|
||||||
// additional name for where clause
|
// additional name for where clause
|
||||||
values.push(doc.name);
|
values.push(doc.name);
|
||||||
|
|
||||||
return await this.run(`update ${doctype}
|
return await this.run(`update ${doctype}
|
||||||
set ${assigns.join(", ")} where name=?`, values);
|
set ${assigns.join(", ")} where name=?`, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runDeleteOtherChildren(field, added) {
|
async runDeleteOtherChildren(field, added) {
|
||||||
// delete other children
|
// delete other children
|
||||||
// `delete from doctype where parent = ? and name not in (?, ?, ?)}`
|
// `delete from doctype where parent = ? and name not in (?, ?, ?)}`
|
||||||
await this.run(`delete from ${field.childtype}
|
await this.run(`delete from ${field.childtype}
|
||||||
where
|
where
|
||||||
parent = ? and
|
parent = ? and
|
||||||
name not in (${added.slice(1).map(d => '?').join(', ')})`, added);
|
name not in (${added.slice(1).map(d => '?').join(', ')})`, added);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOne(doctype, name) {
|
||||||
|
return await this.run(`delete from ${doctype} where name=?`, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteChildren(parenttype, parent) {
|
||||||
|
await this.run(`delete from ${parenttype} where parent=?`, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSingleValues(name) {
|
||||||
|
await frappe.db.run('delete from SingleValue where parent=?', name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async setValues(doctype, name, fieldValuePair) {
|
||||||
|
const meta = frappe.getMeta(doctype);
|
||||||
|
const validFields = this.getKeys(doctype);
|
||||||
|
const validFieldnames = validFields.map(df => df.fieldname);
|
||||||
|
const fieldsToUpdate = Object.keys(fieldValuePair)
|
||||||
|
.filter(fieldname => validFieldnames.includes(fieldname))
|
||||||
|
|
||||||
|
// assignment part of query
|
||||||
|
const assigns = fieldsToUpdate.map(fieldname => `${fieldname} = ?`);
|
||||||
|
|
||||||
|
// values
|
||||||
|
const values = fieldsToUpdate.map(fieldname => {
|
||||||
|
const field = meta.getField(fieldname);
|
||||||
|
const value = fieldValuePair[fieldname];
|
||||||
|
return this.getFormattedValue(field, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// additional name for where clause
|
||||||
|
values.push(name);
|
||||||
|
|
||||||
|
return await this.run(`update ${doctype}
|
||||||
|
set ${assigns.join(', ')} where name=?`, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
|
||||||
|
if (!fields) {
|
||||||
|
fields = frappe.getMeta(doctype).getKeywordFields();
|
||||||
|
}
|
||||||
|
if (typeof fields === 'string') {
|
||||||
|
fields = [fields];
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteOne(doctype, name) {
|
return new Promise((resolve, reject) => {
|
||||||
return await this.run(`delete from ${doctype} where name=?`, name);
|
let conditions = this.getFilterConditions(filters);
|
||||||
}
|
let query = `select ${fields.join(", ")}
|
||||||
|
|
||||||
async deleteChildren(parenttype, parent) {
|
|
||||||
await this.run(`delete from ${parenttype} where parent=?`, parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSingleValues(name) {
|
|
||||||
await frappe.db.run('delete from SingleValue where parent=?', name)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
|
|
||||||
if (!fields) {
|
|
||||||
fields = frappe.getMeta(doctype).getKeywordFields();
|
|
||||||
}
|
|
||||||
if (typeof fields === 'string') {
|
|
||||||
fields = [fields];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let conditions = this.getFilterConditions(filters);
|
|
||||||
let query = `select ${fields.join(", ")}
|
|
||||||
from ${doctype}
|
from ${doctype}
|
||||||
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
||||||
${groupBy ? ("group by " + groupBy.join(', ')) : ""}
|
${groupBy ? ("group by " + groupBy.join(', ')) : ""}
|
||||||
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}
|
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}
|
||||||
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`;
|
${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`;
|
||||||
|
|
||||||
this.conn.all(query, conditions.values,
|
this.conn.all(query, conditions.values,
|
||||||
(err, rows) => {
|
(err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve(rows);
|
resolve(rows);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
run(query, params) {
|
run(query, params) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.conn.run(query, params, (err) => {
|
this.conn.run(query, params, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initTypeMap() {
|
sql(query, params) {
|
||||||
this.typeMap = {
|
return new Promise((resolve) => {
|
||||||
'Autocomplete': 'text'
|
this.conn.all(query, params, (err, rows) => {
|
||||||
, 'Currency': 'real'
|
resolve(rows);
|
||||||
, 'Int': 'integer'
|
});
|
||||||
, 'Float': 'real'
|
});
|
||||||
, 'Percent': 'real'
|
}
|
||||||
, 'Check': 'integer'
|
|
||||||
, 'Small Text': 'text'
|
async commit() {
|
||||||
, 'Long Text': 'text'
|
try {
|
||||||
, 'Code': 'text'
|
await this.run('commit');
|
||||||
, 'Text Editor': 'text'
|
} catch (e) {
|
||||||
, 'Date': 'text'
|
if (e.errno !== 1) {
|
||||||
, 'Datetime': 'text'
|
throw e;
|
||||||
, 'Time': 'text'
|
}
|
||||||
, 'Text': 'text'
|
|
||||||
, 'Data': 'text'
|
|
||||||
, 'Link': 'text'
|
|
||||||
, 'DynamicLink': 'text'
|
|
||||||
, 'Password': 'text'
|
|
||||||
, 'Select': 'text'
|
|
||||||
, 'Read Only': 'text'
|
|
||||||
, 'File': 'text'
|
|
||||||
, 'Attach': 'text'
|
|
||||||
, 'Attach Image': 'text'
|
|
||||||
, 'Signature': 'text'
|
|
||||||
, 'Color': 'text'
|
|
||||||
, 'Barcode': 'text'
|
|
||||||
, 'Geolocation': 'text'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initTypeMap() {
|
||||||
|
this.typeMap = {
|
||||||
|
'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'
|
||||||
|
, 'DynamicLink': 'text'
|
||||||
|
, 'Password': 'text'
|
||||||
|
, 'Select': 'text'
|
||||||
|
, 'Read Only': 'text'
|
||||||
|
, 'File': 'text'
|
||||||
|
, 'Attach': 'text'
|
||||||
|
, 'Attach Image': 'text'
|
||||||
|
, 'Signature': 'text'
|
||||||
|
, 'Color': 'text'
|
||||||
|
, 'Barcode': 'text'
|
||||||
|
, 'Geolocation': 'text'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,25 @@ module.exports = class BaseMeta extends BaseDocument {
|
|||||||
return this._field_map[fieldname];
|
return this._field_map[fieldname];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fields filtered by filters
|
||||||
|
* @param {Object} filters
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* meta = frappe.getMeta('ToDo')
|
||||||
|
* dataFields = meta.getFieldsWith({ fieldtype: 'Data' })
|
||||||
|
*/
|
||||||
|
getFieldsWith(filters) {
|
||||||
|
return this.fields.filter(df => {
|
||||||
|
let match = true;
|
||||||
|
for (const key in filters) {
|
||||||
|
const value = filters[key];
|
||||||
|
match = df[key] === value;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getLabel(fieldname) {
|
getLabel(fieldname) {
|
||||||
return this.getField(fieldname).label;
|
return this.getField(fieldname).label;
|
||||||
}
|
}
|
||||||
|
67
models/doctype/File/File.js
Normal file
67
models/doctype/File/File.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
module.exports = {
|
||||||
|
name: 'File',
|
||||||
|
doctype: 'DocType',
|
||||||
|
isSingle: 0,
|
||||||
|
keywordFields: [
|
||||||
|
'name',
|
||||||
|
'filename'
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: 'name',
|
||||||
|
label: 'File Path',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
required: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'filename',
|
||||||
|
label: 'File Name',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
required: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'mimetype',
|
||||||
|
label: 'MIME Type',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'size',
|
||||||
|
label: 'File Size',
|
||||||
|
fieldtype: 'Int',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'referenceDoctype',
|
||||||
|
label: 'Reference DocType',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'referenceName',
|
||||||
|
label: 'Reference Name',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: 'referenceField',
|
||||||
|
label: 'Reference Field',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: [
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{ fields: ['filename'] },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{ fields: ['mimetype'] },
|
||||||
|
{ fields: ['size'] },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{ fields: ['referenceDoctype'] },
|
||||||
|
{ fields: ['referenceName'] },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
@ -1,66 +1,66 @@
|
|||||||
const indicatorColor = require('frappejs/ui/constants/indicators');
|
const { BLUE, GREEN } = require('frappejs/ui/constants/indicators');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "ToDo",
|
name: 'ToDo',
|
||||||
label: "To Do",
|
label: 'To Do',
|
||||||
naming: "autoincrement",
|
naming: 'autoincrement',
|
||||||
pageSettings: {
|
pageSettings: {
|
||||||
hideTitle: true
|
hideTitle: true
|
||||||
|
},
|
||||||
|
isSingle: 0,
|
||||||
|
keywordFields: [
|
||||||
|
'subject',
|
||||||
|
'description'
|
||||||
|
],
|
||||||
|
titleField: 'subject',
|
||||||
|
indicators: {
|
||||||
|
key: 'status',
|
||||||
|
colors: {
|
||||||
|
Open: BLUE,
|
||||||
|
Closed: GREEN
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: 'subject',
|
||||||
|
label: 'Subject',
|
||||||
|
fieldtype: 'Data',
|
||||||
|
required: 1
|
||||||
},
|
},
|
||||||
"isSingle": 0,
|
{
|
||||||
"keywordFields": [
|
fieldname: 'status',
|
||||||
"subject",
|
label: 'Status',
|
||||||
"description"
|
fieldtype: 'Select',
|
||||||
],
|
options: [
|
||||||
titleField: 'subject',
|
'Open',
|
||||||
indicators: {
|
'Closed'
|
||||||
key: 'status',
|
],
|
||||||
colors: {
|
default: 'Open',
|
||||||
Open: indicatorColor.BLUE,
|
required: 1
|
||||||
Closed: indicatorColor.GREEN
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"fields": [
|
{
|
||||||
{
|
fieldname: 'description',
|
||||||
"fieldname": "subject",
|
label: 'Description',
|
||||||
"label": "Subject",
|
fieldtype: 'Text'
|
||||||
"fieldtype": "Data",
|
}
|
||||||
"required": 1
|
],
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "status",
|
|
||||||
"label": "Status",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"options": [
|
|
||||||
"Open",
|
|
||||||
"Closed"
|
|
||||||
],
|
|
||||||
"default": "Open",
|
|
||||||
"required": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "description",
|
|
||||||
"label": "Description",
|
|
||||||
"fieldtype": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
label: 'Close',
|
label: 'Close',
|
||||||
condition: (form) => form.doc.status !== 'Closed',
|
condition: (form) => form.doc.status !== 'Closed',
|
||||||
action: async (form) => {
|
action: async (form) => {
|
||||||
await form.doc.set('status', 'Closed');
|
await form.doc.set('status', 'Closed');
|
||||||
await form.doc.update();
|
await form.doc.update();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Re-Open',
|
label: 'Re-Open',
|
||||||
condition: (form) => form.doc.status !== 'Open',
|
condition: (form) => form.doc.status !== 'Open',
|
||||||
action: async (form) => {
|
action: async (form) => {
|
||||||
await form.doc.set('status', 'Open');
|
await form.doc.set('status', 'Open');
|
||||||
await form.doc.update();
|
await form.doc.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ module.exports = {
|
|||||||
SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'),
|
SystemSettings: require('./doctype/SystemSettings/SystemSettings.js'),
|
||||||
ToDo: require('./doctype/ToDo/ToDo.js'),
|
ToDo: require('./doctype/ToDo/ToDo.js'),
|
||||||
User: require('./doctype/User/User.js'),
|
User: require('./doctype/User/User.js'),
|
||||||
UserRole: require('./doctype/UserRole/UserRole.js')
|
UserRole: require('./doctype/UserRole/UserRole.js'),
|
||||||
|
File: require('./doctype/File/File.js'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"luxon": "^1.0.0",
|
"luxon": "^1.0.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"morgan": "^1.9.0",
|
"morgan": "^1.9.0",
|
||||||
|
"multer": "^1.3.1",
|
||||||
"mysql": "^2.15.0",
|
"mysql": "^2.15.0",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.7.2",
|
||||||
|
@ -18,12 +18,15 @@ const auth = require('./../auth/auth')();
|
|||||||
const morgan = require('morgan');
|
const morgan = require('morgan');
|
||||||
const { addWebpackMiddleware } = require('../webpack/serve');
|
const { addWebpackMiddleware } = require('../webpack/serve');
|
||||||
const { getAppConfig } = require('../webpack/utils');
|
const { getAppConfig } = require('../webpack/utils');
|
||||||
const appConfig = getAppConfig();
|
|
||||||
|
frappe.conf = getAppConfig();
|
||||||
|
|
||||||
require.extensions['.html'] = function (module, filename) {
|
require.extensions['.html'] = function (module, filename) {
|
||||||
module.exports = fs.readFileSync(filename, 'utf8');
|
module.exports = fs.readFileSync(filename, 'utf8');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
async start({backend, connectionParams, models, authConfig=null}) {
|
async start({backend, connectionParams, models, authConfig=null}) {
|
||||||
await this.init();
|
await this.init();
|
||||||
@ -39,9 +42,8 @@ module.exports = {
|
|||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
for (let staticPath of [appConfig.distPath, appConfig.staticPath]) {
|
app.use(express.static(frappe.conf.distPath));
|
||||||
app.use(express.static(staticPath));
|
app.use('/static', express.static(frappe.conf.staticPath))
|
||||||
}
|
|
||||||
|
|
||||||
app.use(morgan('tiny'));
|
app.use(morgan('tiny'));
|
||||||
|
|
||||||
@ -65,7 +67,7 @@ module.exports = {
|
|||||||
addWebpackMiddleware(app);
|
addWebpackMiddleware(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.config.port = appConfig.dev.devServerPort
|
frappe.config.port = frappe.conf.dev.devServerPort;
|
||||||
|
|
||||||
// listen
|
// listen
|
||||||
server.listen(frappe.config.port, () => {
|
server.listen(frappe.config.port, () => {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
const frappe = require('frappejs');
|
const frappe = require('frappejs');
|
||||||
|
const path = require('path');
|
||||||
|
const multer = require('multer');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setup(app) {
|
setup(app) {
|
||||||
@ -43,6 +45,45 @@ module.exports = {
|
|||||||
return response.json(doc.getValidDict());
|
return response.json(doc.getValidDict());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, frappe.conf.staticPath)
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const filename = file.originalname.split('.')[0];
|
||||||
|
const extension = path.extname(file.originalname);
|
||||||
|
const now = Date.now();
|
||||||
|
cb(null, filename + '-' + now + extension);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/upload/:doctype/:name/:fieldname', upload.array('files', 10), frappe.asyncHandler(async function(request, response) {
|
||||||
|
const files = request.files;
|
||||||
|
const { doctype, name, fieldname } = request.params;
|
||||||
|
|
||||||
|
let fileDocs = [];
|
||||||
|
for (let file of files) {
|
||||||
|
const doc = frappe.newDoc({
|
||||||
|
doctype: 'File',
|
||||||
|
name: path.join('/', file.path),
|
||||||
|
filename: file.originalname,
|
||||||
|
mimetype: file.mimetype,
|
||||||
|
size: file.size,
|
||||||
|
referenceDoctype: doctype,
|
||||||
|
referenceName: name,
|
||||||
|
referenceFieldname: fieldname
|
||||||
|
});
|
||||||
|
await doc.insert();
|
||||||
|
|
||||||
|
await frappe.db.setValue(doctype, name, fieldname, doc.name);
|
||||||
|
|
||||||
|
fileDocs.push(doc.getValidDict());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json(fileDocs);
|
||||||
|
}));
|
||||||
|
|
||||||
// get document
|
// get document
|
||||||
app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) {
|
app.get('/api/resource/:doctype/:name', frappe.asyncHandler(async function(request, response) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<form :class="['frappe-form-layout', { 'was-validated': invalid }]">
|
<form :class="['frappe-form-layout', { 'was-validated': invalid }]">
|
||||||
<div class="row" v-if="layoutConfig"
|
<div class="form-row" v-if="layoutConfig"
|
||||||
v-for="(section, i) in layoutConfig.sections" :key="i"
|
v-for="(section, i) in layoutConfig.sections" :key="i"
|
||||||
v-show="showSection(i)"
|
v-show="showSection(i)"
|
||||||
>
|
>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<list-actions
|
<list-actions
|
||||||
:doctype="doctype"
|
:doctype="doctype"
|
||||||
:showDelete="checkList.length"
|
:showDelete="checkList.length"
|
||||||
@new="newDoc"
|
@new="$emit('newDoc')"
|
||||||
@delete="deleteCheckedItems"
|
@delete="deleteCheckedItems"
|
||||||
/>
|
/>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@ -60,18 +60,18 @@ export default {
|
|||||||
this.updateList();
|
this.updateList();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async newDoc() {
|
async updateList(query = null) {
|
||||||
let doc = await frappe.getNewDoc(this.doctype);
|
let filters = null;
|
||||||
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
|
|
||||||
},
|
|
||||||
async updateList(query=null) {
|
|
||||||
let filters = null
|
|
||||||
if (query) {
|
if (query) {
|
||||||
filters = {
|
filters = {
|
||||||
keywords : ['like', query]
|
keywords: ['like', query]
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
const indicatorField = this.hasIndicator ? this.meta.indicators.key : null;
|
|
||||||
|
const indicatorField = this.hasIndicator
|
||||||
|
? this.meta.indicators.key
|
||||||
|
: null;
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'name',
|
'name',
|
||||||
indicatorField,
|
indicatorField,
|
||||||
@ -89,7 +89,7 @@ export default {
|
|||||||
},
|
},
|
||||||
openForm(name) {
|
openForm(name) {
|
||||||
this.activeItem = name;
|
this.activeItem = name;
|
||||||
this.$router.push(`/edit/${this.doctype}/${name}`);
|
this.$emit('openForm', name);
|
||||||
},
|
},
|
||||||
async deleteCheckedItems() {
|
async deleteCheckedItems() {
|
||||||
await frappe.db.deleteMany(this.doctype, this.checkList);
|
await frappe.db.deleteMany(this.doctype, this.checkList);
|
||||||
@ -112,7 +112,7 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "../../styles/variables";
|
@import '../../styles/variables';
|
||||||
|
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
|
@ -3,38 +3,55 @@ import Base from './Base';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: Base,
|
extends: Base,
|
||||||
computed: {
|
|
||||||
inputClass() {
|
|
||||||
return ['d-none'];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getWrapperElement(h) {
|
getWrapperElement(h) {
|
||||||
let fileName = this.docfield.placeholder || this._('Choose a file..');
|
let fileName = this.docfield.placeholder || this._('Choose a file..');
|
||||||
|
let filePath = null;
|
||||||
|
|
||||||
if (this.$refs.input && this.$refs.input.files.length) {
|
if (this.value && typeof this.value === 'string') {
|
||||||
|
filePath = this.value;
|
||||||
|
}
|
||||||
|
else if (this.$refs.input && this.$refs.input.files.length) {
|
||||||
fileName = this.$refs.input.files[0].name;
|
fileName = this.$refs.input.files[0].name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileButton = h('button', {
|
const fileLink = h('a', {
|
||||||
class: ['btn btn-outline-secondary btn-block'],
|
|
||||||
domProps: {
|
|
||||||
textContent: fileName
|
|
||||||
},
|
|
||||||
attrs: {
|
attrs: {
|
||||||
type: 'button'
|
href: filePath,
|
||||||
|
target: '_blank'
|
||||||
},
|
},
|
||||||
on: {
|
domProps: {
|
||||||
click: () => this.$refs.input.click()
|
textContent: this._('View File')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return h('div', {
|
const helpText = h('small', {
|
||||||
|
class: 'form-text text-muted'
|
||||||
|
}, [fileLink]);
|
||||||
|
|
||||||
|
const fileNameLabel = h('label', {
|
||||||
|
class: ['custom-file-label'],
|
||||||
|
domProps: {
|
||||||
|
textContent: filePath || fileName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileInputWrapper = h('div', {
|
||||||
|
class: ['custom-file']
|
||||||
|
},
|
||||||
|
[this.getInputElement(h), fileNameLabel, filePath ? helpText : null]
|
||||||
|
);
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
class: ['form-group', ...this.wrapperClass],
|
class: ['form-group', ...this.wrapperClass],
|
||||||
attrs: {
|
attrs: {
|
||||||
'data-fieldname': this.docfield.fieldname
|
'data-fieldname': this.docfield.fieldname
|
||||||
}
|
}
|
||||||
}, [this.getLabelElement(h), this.getInputElement(h), fileButton]);
|
},
|
||||||
|
[this.getLabelElement(h), fileInputWrapper]
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getInputAttrs() {
|
getInputAttrs() {
|
||||||
return {
|
return {
|
||||||
@ -48,6 +65,9 @@ export default {
|
|||||||
accept: (this.docfield.filetypes || []).join(',')
|
accept: (this.docfield.filetypes || []).join(',')
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getInputClass() {
|
||||||
|
return 'custom-file-input';
|
||||||
|
},
|
||||||
getInputListeners() {
|
getInputListeners() {
|
||||||
return {
|
return {
|
||||||
change: e => {
|
change: e => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="frappe-list-form row no-gutters">
|
<div class="frappe-list-form row no-gutters">
|
||||||
<div class="col-4 border-right">
|
<div class="col-4 border-right">
|
||||||
<frappe-list :doctype="doctype" :key="doctype" />
|
<frappe-list :doctype="doctype" :key="doctype" @newDoc="openNewDoc" @openForm="openForm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<frappe-form v-if="name" :key="doctype + name" :doctype="doctype" :name="name" @save="onSave" />
|
<frappe-form v-if="name" :key="doctype + name" :doctype="doctype" :name="name" @save="onSave" />
|
||||||
@ -13,22 +13,30 @@ import List from '../components/List/List';
|
|||||||
import Form from '../components/Form/Form';
|
import Form from '../components/Form/Form';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['doctype', 'name'],
|
props: ['doctype', 'name'],
|
||||||
components: {
|
components: {
|
||||||
FrappeList: List,
|
FrappeList: List,
|
||||||
FrappeForm: Form
|
FrappeForm: Form
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSave(doc) {
|
||||||
|
if (doc.name !== this.$route.params.name) {
|
||||||
|
this.$router.push(`/edit/${doc.doctype}/${doc.name}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
openForm(name) {
|
||||||
onSave(doc) {
|
name = encodeURIComponent(name);
|
||||||
if (doc.name !== this.$route.params.name) {
|
this.$router.push(`/edit/${this.doctype}/${name}`);
|
||||||
this.$router.push(`/edit/${doc.doctype}/${doc.name}`);
|
},
|
||||||
}
|
async openNewDoc() {
|
||||||
}
|
let doc = await frappe.getNewDoc(this.doctype);
|
||||||
|
this.$router.push(`/edit/${this.doctype}/${doc.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.frappe-list-form {
|
.frappe-list-form {
|
||||||
min-height: calc(100vh - 4rem);
|
min-height: calc(100vh - 4rem);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user