diff --git a/README.md b/README.md index dc828bc5..7fd83280 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ await frappe.init(); await frappe.init_db('sqlite', {db_path: 'test.db'}); // get all open todos -let todos = await frappe.db.get_all('ToDo', ['name'], {status: "Open"}); +let todos = await frappe.db.get_all({doctype:'ToDo', fields:['name'], filters: {status: "Open"}); let first_todo = await frappe.get_doc('ToDo', toods[0].name); ``` @@ -109,7 +109,7 @@ await frappe.init(); await frappe.init_db('sqlite', {db_path: 'test.db'}); // get all open todos -let todos = await frappe.db.get_all('ToDo', ['name'], {status: "Open"}); +let todos = await frappe.db.get_all({doctype:'ToDo', fields:['name'], filters: {status: "Open"}); let first_todo = await frappe.get_doc('ToDo', toods[0].name); first_todo.status = 'Closed'; @@ -126,7 +126,7 @@ await frappe.init(); await frappe.init_db('sqlite', {db_path: 'test.db'}); // get all open todos -let todos = await frappe.db.get_all('ToDo', ['name'], {status: "Open"}); +let todos = await frappe.db.get_all({doctype:'ToDo', fields:['name'], filters: {status: "Open"}); let first_todo = await frappe.get_doc('ToDo', toods[0].name); await first_todo.delete(); @@ -281,6 +281,28 @@ Response: } ] ``` + +## REST Client + +Frappe comes with a built in REST client so you can also use REST as a database backend with the frappe API + +### Create, Read, Update, Delete + +You can manage documents, using the same Document API as if it were a local database + +```js +await frappe.init(); +await frappe.init_db('rest', {server: 'localhost:8000'}); + +let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'}); +await doc.insert(); + +doc.subject = 'subject changed'; +await doc.update(); + +let data = await frappe.db.get_all({doctype:'ToDo'}); +``` + ## Tests All tests are in the `tests` folder and are run using `mocha`. To run tests diff --git a/frappe/backends/rest_client.js b/frappe/backends/rest_client.js index e69de29b..28424f96 100644 --- a/frappe/backends/rest_client.js +++ b/frappe/backends/rest_client.js @@ -0,0 +1,89 @@ +const frappe = require('frappe-core'); +const path = require('path'); +const fetch = require('node-fetch'); + +class RESTClient { + constructor({server, protocol='http'}) { + this.server = server; + this.protocol = protocol; + this.json_headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + } + + connect() { + + } + + async insert(doctype, doc) { + doc.doctype = doctype; + let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`); + let response = await fetch(url, { + method: 'POST', + headers: this.json_headers, + body: JSON.stringify(doc) + }); + + return await response.json(); + } + + async get(doctype, name) { + let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`); + let response = await fetch(url, { + method: 'GET', + headers: this.json_headers + }); + return await response.json(); + } + + async get_all({doctype, fields, filters, start, limit, sort_by, order}) { + let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}`); + let response = await fetch(url, { + method: 'GET', + params: { + fields: JSON.stringify(fields), + filters: JSON.stringify(filters), + start: start, + limit: limit, + sort_by: sort_by, + order: order + }, + headers: this.json_headers + }); + return await response.json(); + + } + + async update(doctype, doc) { + doc.doctype = doctype; + let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${doc.name}`); + let response = await fetch(url, { + method: 'PUT', + headers: this.json_headers, + body: JSON.stringify(doc) + }); + + return await response.json(); + } + + async delete(doctype, name) { + let url = this.protocol + '://' + path.join(this.server, `/api/resource/${frappe.slug(doctype)}/${name}`); + + let response = await fetch(url, { + method: 'DELETE', + headers: this.json_headers + }); + + return await response.json(); + } + + close() { + + } + +} + +module.exports = { + Database: RESTClient +} \ No newline at end of file diff --git a/frappe/backends/sqlite.js b/frappe/backends/sqlite.js index 2a232a5e..98a68ce2 100644 --- a/frappe/backends/sqlite.js +++ b/frappe/backends/sqlite.js @@ -88,11 +88,12 @@ class sqliteDatabase { return await this.run(`delete from ${frappe.slug(doctype)} where name=?`, name); } - get_all(doctype, fields=['name'], filters, start, limit) { + get_all({doctype, fields=['name'], filters, start, limit, order_by='modified', order='desc'} = {}) { return new Promise(resolve => { this.conn.all(`select ${fields.join(", ")} from ${frappe.slug(doctype)} ${filters ? "where" : ""} ${this.get_filter_conditions(filters)} + ${order_by ? ("order by " + order_by) : ""} ${order_by ? (order || "asc") : ""} ${limit ? ("limit " + limit) : ""} ${start ? ("offset " + start) : ""}`, (err, rows) => { resolve(rows); @@ -153,7 +154,12 @@ class sqliteDatabase { filters = {name: filters}; } - let row = await this.get_all(doctype, [fieldname], filters, 0, 1); + let row = await this.get_all({ + doctype:doctype, + fields: [fieldname], + filters: filters, + start: 0, + limit: 1}); return row.length ? row[0][fieldname] : null; } diff --git a/frappe/rest_server.js b/frappe/rest_server.js index 368b5ed4..3b03d261 100644 --- a/frappe/rest_server.js +++ b/frappe/rest_server.js @@ -4,31 +4,78 @@ module.exports = { setup(app) { // get list app.get('/api/resource/:doctype', async function(request, response) { - let data = await frappe.db.get_all(request.params.doctype, ['name', 'subject'], null, - start = request.params.start || 0, limit = request.params.limit || 20); - return response.json(data); + try { + let fields, filters; + for (key of ['fields', 'filters']) { + if (request.params[key]) { + request.params[key] = JSON.parse(request.params[key]); + } + } + + let data = await frappe.db.get_all({ + doctype: request.params.doctype, + fields: request.params.fields || ['name', 'subject'], + filters: request.params.filters, + start: request.params.start || 0, + limit: request.params.limit || 20, + order_by: request.params.order_by, + order: request.params.order + }); + + return response.json(data); + } catch (e) { + throw e; + } }); // create app.post('/api/resource/:doctype', async function(request, response) { - data = request.body; - data.doctype = request.params.doctype; - let doc = await frappe.get_doc(data) - await doc.insert(); - await frappe.db.commit(); - return response.json(doc.get_valid_dict()); + try { + data = request.body; + data.doctype = request.params.doctype; + let doc = await frappe.get_doc(data); + await doc.insert(); + await frappe.db.commit(); + return response.json(doc.get_valid_dict()); + } catch (e) { + throw e; + } }); + // update + app.put('/api/resource/:doctype/:name', async function(request, response) { + try { + data = request.body; + let doc = await frappe.get_doc(request.params.doctype, request.params.name); + Object.assign(doc, data); + await doc.update(); + await frappe.db.commit(); + return response.json(doc.get_valid_dict()); + } catch (e) { + throw e; + } + }); + + // get document app.get('/api/resource/:doctype/:name', async function(request, response) { - let data = await frappe.get_doc(request.params.doctype, request.params.name).get_valid_dict(); - return response.json(data); + try { + let doc = await frappe.get_doc(request.params.doctype, request.params.name); + return response.json(doc.get_valid_dict()); + } catch (e) { + throw e; + } }); // delete app.delete('/api/resource/:doctype/:name', async function(request, response) { - let data = await frappe.get_doc(request.params.doctype, request.params.name).delete(); - return response.json(data); + try { + let doc = await frappe.get_doc(request.params.doctype, request.params.name) + await doc.delete(); + return response.json({}); + } catch (e) { + throw e; + } }); } diff --git a/frappe/tests/test_database.js b/frappe/tests/test_database.js index 49e62882..daf9e1f4 100644 --- a/frappe/tests/test_database.js +++ b/frappe/tests/test_database.js @@ -13,7 +13,7 @@ describe('Database', () => { await frappe.insert({doctype:'ToDo', subject: 'testing 3'}); await frappe.insert({doctype:'ToDo', subject: 'testing 2'}); - let subjects = await frappe.db.get_all('ToDo', ['name', 'subject']) + let subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject']}) subjects = subjects.map(d => d.subject); assert.ok(subjects.includes('testing 1')); @@ -29,16 +29,16 @@ describe('Database', () => { await frappe.insert({doctype:'ToDo', subject: 'testing 3', status: 'Open'}); await frappe.insert({doctype:'ToDo', subject: 'testing 2', status: 'Closed'}); - subjects = await frappe.db.get_all('ToDo', ['name', 'subject'], - {status: 'Open'}) + subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'], + filters:{status: 'Open'}}); subjects = subjects.map(d => d.subject); assert.ok(subjects.includes('testing 1')); assert.ok(subjects.includes('testing 3')); assert.equal(subjects.includes('testing 2'), false); - subjects = await frappe.db.get_all('ToDo', ['name', 'subject'], - {status: 'Closed'}) + subjects = await frappe.db.get_all({doctype:'ToDo', fields:['name', 'subject'], + filters:{status: 'Closed'}}); subjects = subjects.map(d => d.subject); assert.equal(subjects.includes('testing 1'), false); diff --git a/frappe/tests/test_rest_api.js b/frappe/tests/test_rest_api.js index 3d60a3ad..9891b737 100644 --- a/frappe/tests/test_rest_api.js +++ b/frappe/tests/test_rest_api.js @@ -11,11 +11,13 @@ var test_server; describe('REST', () => { before(async function() { - await helpers.init_sqlite(); test_server = spawn('node', ['frappe/tests/test_server.js'], { - stdio: [0, 'pipe', 'pipe' ] + stdio: [process.stdin, process.stdout, process.stderr, 'pipe', 'pipe'] }); + await frappe.init(); + await frappe.init_db('rest', {server: 'localhost:8000'}); + // wait for server to start await frappe.sleep(1); }); @@ -26,28 +28,35 @@ describe('REST', () => { }); it('should create a document', async () => { - let res = await fetch('http://localhost:8000/api/resource/todo', { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({subject: 'test rest insert 1', description: 'test rest description 1'}) - }); - let doc = await res.json(); - assert.equal(doc.subject, 'test rest insert 1'); - assert.equal(doc.description, 'test rest description 1'); + let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'}); + await doc.insert(); + + let doc1 = await frappe.get_doc('ToDo', doc.name); + + assert.equal(doc.subject, doc1.subject); + assert.equal(doc1.status, 'Open'); }); - // it('should create a document with rest backend', async () => { + it('should update a document', async () => { + let doc = await frappe.get_doc({doctype:'ToDo', subject:'test rest insert 1'}); + await doc.insert(); - // frappe.init_db('rest', { server: 'http://localhost:8000' }); + doc.subject = 'subject changed'; + await doc.update(); - // let doc = await frappe.get_doc({doctype: 'ToDo', subject: 'test rest backend 1'}); - // await doc.insert(); + let doc1 = await frappe.get_doc('ToDo', doc.name); + assert.equal(doc.subject, doc1.subject); + }); - // let doc_reloaded = await frappe.get_doc('ToDo', doc.name); + it('should get multiple documents', async () => { + await frappe.insert({doctype:'ToDo', subject:'all test 1'}); + await frappe.insert({doctype:'ToDo', subject:'all test 2'}); + + let data = await frappe.db.get_all({doctype:'ToDo'}); + let subjects = data.map(d => d.subject); + assert.ok(subjects.includes('all test 1')); + assert.ok(subjects.includes('all test 2')); + }); - // }) }); \ No newline at end of file