diff options
author | Alexander Shorin <kxepal@apache.org> | 2015-11-18 21:39:21 +0300 |
---|---|---|
committer | Alexander Shorin <kxepal@apache.org> | 2015-12-22 00:41:31 +0300 |
commit | 101e54169b7b9ba389514a6dc00dc7ee401e5566 (patch) | |
tree | 71fa5072e1fc4e3aeb74c68f7fb8f2073548a879 | |
parent | bdc9681fd6042612fe77f122228a667813673ffd (diff) | |
download | couchdb-101e54169b7b9ba389514a6dc00dc7ee401e5566.tar.gz |
Rewrite via query server
COUCHDB-2874
-rw-r--r-- | rebar.config.script | 6 | ||||
-rw-r--r-- | share/server/loop.js | 3 | ||||
-rw-r--r-- | share/server/render.js | 26 | ||||
-rw-r--r-- | test/javascript/tests/rewrite_js.js | 340 | ||||
-rw-r--r-- | test/view_server/query_server_spec.rb | 61 |
5 files changed, 430 insertions, 6 deletions
diff --git a/rebar.config.script b/rebar.config.script index 324957e2d..ecb773b71 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -30,10 +30,10 @@ DepDescs = [ {couch_log, "couch-log", "939b3a7bda7dcb03f841b899762b188ca31bc230"}, {couch_log_lager, "couch-log-lager", "b2a0471a87765de50c5eb05c65c121f68a9ae9fa"}, {config, "config", "84197a6f1c5cb43447239df1fe57b4312b0c03c4"}, -{chttpd, "chttpd", "d31b2a4d44d68f8e9a1ac2c55c47e0cf4e95397b"}, -{couch, "couch", "57ecc0427c465156b0b10111737f32ae2242391e"}, +{chttpd, "chttpd", "417679a9cf2277693253d3f9c2ac0e52fa1ba75c"}, +{couch, "couch", "f33f9fa1281fd6c50a86cb9853bd2d9ec2dd007c"}, {couch_index, "couch-index", "14f579dcd142ee90300244c854b301bbd5c863ee"}, -{couch_mrview, "couch-mrview", "c3bed460ee844175b8ce11081386be27f686d8ff"}, +{couch_mrview, "couch-mrview", "6c9833d667e319b82a7a1fffb8ba92116534e63f"}, {couch_replicator, "couch-replicator", "3f268abba89bd5b93f43185465e66ef42b3876ad"}, {couch_plugins, "couch-plugins", "3e73b723cb126cfc471b560d17c24a8b5c540085"}, {couch_event, "couch-event", "835a41885d1e276d207758954f8238aa7bba0ae8"}, diff --git a/share/server/loop.js b/share/server/loop.js index e1226c387..551004ae9 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -62,7 +62,8 @@ var DDoc = (function() { "filters" : Filter.filter, "views" : Filter.filter_view, "updates" : Render.update, - "validate_doc_update" : Validate.validate + "validate_doc_update" : Validate.validate, + "rewrites" : Render.rewrite }; var ddocs = {}; return { diff --git a/share/server/render.js b/share/server/render.js index 49b0863cd..1fe4da298 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -314,7 +314,26 @@ var Render = (function() { } }; - function renderError(e, funSrc) { + function runRewrite(fun, ddoc, args) { + var result; + try { + result = fun.apply(ddoc, args); + } catch(error) { + renderError(error, fun.toString(), "rewrite_error"); + } + + if (!result) { + respond(["no_dispatch_rule"]); + return; + } + + if (typeof result === "string") { + result = {path: result, method: args[0].method}; + } + respond(["ok", result]); + } + + function renderError(e, funSrc, errType) { if (e.error && e.reason || e[0] == "error" || e[0] == "fatal") { throw(e); } else { @@ -322,7 +341,7 @@ var Render = (function() { (e.toSource ? e.toSource() : e.toString()) + " \n" + "stacktrace: " + e.stack; log(logMessage); - throw(["error", "render_error", logMessage]); + throw(["error", errType || "render_error", logMessage]); } }; @@ -347,6 +366,9 @@ var Render = (function() { }, list : function(fun, ddoc, args) { runList(fun, ddoc, args); + }, + rewrite : function(fun, ddoc, args) { + runRewrite(fun, ddoc, args); } }; })(); diff --git a/test/javascript/tests/rewrite_js.js b/test/javascript/tests/rewrite_js.js new file mode 100644 index 000000000..b2c165b52 --- /dev/null +++ b/test/javascript/tests/rewrite_js.js @@ -0,0 +1,340 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + + + +couchTests.rewrite = function(debug) { + if (debug) debugger; + var dbNames = ["test_suite_db", "test_suite_db/with_slashes"]; + for (var i=0; i < dbNames.length; i++) { + var db = new CouchDB(dbNames[i]); + var dbName = encodeURIComponent(dbNames[i]); + db.deleteDb(); + db.createDb(); + + var designDoc = { + _id:"_design/test", + language: "javascript", + _attachments:{ + "foo.txt": { + content_type:"text/plain", + data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=" + } + }, + rewrites: stringFun(function(req) { + prefix = req.path[4]; + if (prefix === 'foo') { + return 'foo.txt'; + } + if (prefix === 'foo2') { + return {path: 'foo.txt', method: 'GET'}; + } + if (prefix === 'hello') { + if (req.method != 'PUT') { + return + } + id = req.path[5]; + return {path: '_update/hello/' + id}; + } + if (prefix === 'welcome') { + if (req.path.length == 6){ + name = req.path[5]; + return {path: '_show/welcome', query: {'name': name}}; + } + return '_show/welcome'; + } + if (prefix === 'welcome2') { + return {path: '_show/welcome', query: {'name': 'user'}}; + } + if (prefix === 'welcome3') { + name = req.path[5]; + if (req.method == 'PUT') { + path = '_update/welcome2/' + name; + } else if (req.method == 'GET') { + path = '_show/welcome2/' + name; + } else { + return; + } + return path; + } + if (prefix === 'welcome4') { + return {path: '_show/welcome3', query: {name: req.path[5]}}; + } + if (prefix === 'welcome5') { + rest = req.path.slice(5).join('/'); + return {path: '_show/' + rest, query: {name: rest}}; + } + if (prefix === 'basicView') { + rest = req.path.slice(5).join('/'); + return {path: '_view/basicView'}; + } + if (req.path.slice(4).join('/') === 'simpleForm/basicView') { + return {path: '_list/simpleForm/basicView'}; + } + if (req.path.slice(4).join('/') === 'simpleForm/basicViewFixed') { + return {path: '_list/simpleForm/basicView', + query: {startkey: '"3"', endkey: '"8"'}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView') { + return {path: '_list/simpleForm/complexView', + query: {key: JSON.stringify([1,2])}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView2') { + return {path: '_list/simpleForm/complexView', + query: {key: JSON.stringify(['test', {}])}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView3') { + return {path: '_list/simpleForm/complexView', + query: {key: JSON.stringify(['test', ['test', 'essai']])}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView4') { + return {path: '_list/simpleForm/complexView2', + query: {key: JSON.stringify({"c": 1})}}; + } + if (req.path.slice(4).join('/') === 'simpleForm/complexView4') { + return {path: '_list/simpleForm/complexView2', + query: {key: JSON.stringify({"c": 1})}}; + } + if (req.path.slice(4).join('/') === '/') { + return {path: '_view/basicView'}; + } + if (prefix === 'db') { + return {path: '../../' + req.path.slice(5).join('/')}; + } + }), + lists: { + simpleForm: stringFun(function(head, req) { + log("simpleForm"); + send('<ul>'); + var row, row_number = 0, prevKey, firstKey = null; + while (row = getRow()) { + row_number += 1; + if (!firstKey) firstKey = row.key; + prevKey = row.key; + send('\n<li>Key: '+row.key + +' Value: '+row.value + +' LineNo: '+row_number+'</li>'); + } + return '</ul><p>FirstKey: '+ firstKey + ' LastKey: '+ prevKey+'</p>'; + }), + }, + shows: { + "welcome": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }), + "welcome2": stringFun(function(doc, req) { + return "Welcome " + doc.name; + }), + "welcome3": stringFun(function(doc,req) { + return "Welcome " + req.query["name"]; + }) + }, + updates: { + "hello" : stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id : req.id + }, "New World"] + } + return [null, "Empty World"]; + } + doc.world = "hello"; + doc.edited_by = req.userCtx; + return [doc, "hello doc"]; + }), + "welcome2": stringFun(function(doc, req) { + if (!doc) { + if (req.id) { + return [{ + _id: req.id, + name: req.id + }, "New World"] + } + return [null, "Empty World"]; + } + return [doc, "hello doc"]; + }) + }, + views : { + basicView : { + map : stringFun(function(doc) { + if (doc.integer) { + emit(doc.integer, doc.string); + } + + }) + }, + complexView: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit([doc.a, doc.b], doc.string); + } + }) + }, + complexView2: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit(doc.a, doc.string); + } + }) + }, + complexView3: { + map: stringFun(function(doc) { + if (doc.type == "complex") { + emit(doc.b, doc.string); + } + }) + } + } + } + + db.save(designDoc); + + var docs = makeDocs(0, 10); + db.bulkSave(docs); + + var docs2 = [ + {"a": 1, "b": 1, "string": "doc 1", "type": "complex"}, + {"a": 1, "b": 2, "string": "doc 2", "type": "complex"}, + {"a": "test", "b": {}, "string": "doc 3", "type": "complex"}, + {"a": "test", "b": ["test", "essai"], "string": "doc 4", "type": "complex"}, + {"a": {"c": 1}, "b": "", "string": "doc 5", "type": "complex"} + ]; + + db.bulkSave(docs2); + + // test simple rewriting + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/foo"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/foo2"); + T(req.responseText == "This is a base64 encoded text"); + T(req.getResponseHeader("Content-Type") == "text/plain"); + + + // test POST + // hello update world + + var doc = {"word":"plankton", "name":"Rusty"} + var resp = db.save(doc); + T(resp.ok); + var docid = resp.id; + + xhr = CouchDB.request("PUT", "/"+dbName+"/_design/test/_rewrite/hello/"+docid); + T(xhr.status == 201); + T(xhr.responseText == "hello doc"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))) + + doc = db.open(docid); + T(doc.world == "hello"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome?name=user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome/user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome2"); + T(req.responseText == "Welcome user"); + + xhr = CouchDB.request("PUT", "/"+dbName+"/_design/test/_rewrite/welcome3/test"); + T(xhr.status == 201); + T(xhr.responseText == "New World"); + T(/charset=utf-8/.test(xhr.getResponseHeader("Content-Type"))); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome3/test"); + T(xhr.responseText == "Welcome test"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome4/user"); + T(req.responseText == "Welcome user"); + + req = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/welcome5/welcome3"); + T(req.responseText == "Welcome welcome3"); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/basicView"); + T(xhr.status == 200, "view call"); + T(/{"total_rows":9/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView"); + T(xhr.status == 200, "with query params"); + T(/FirstKey: [1, 2]/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView2"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 3/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView3"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 4/.test(xhr.responseText)); + + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/simpleForm/complexView4"); + T(xhr.status == 200, "with query params"); + T(/Value: doc 5/.test(xhr.responseText)); + + // COUCHDB-2031 - path normalization versus qs params + xhr = CouchDB.request("GET", "/"+dbName+"/_design/test/_rewrite/db/_design/test?meta=true"); + T(xhr.status == 200, "path normalization works with qs params"); + var result = JSON.parse(xhr.responseText); + T(result['_id'] == "_design/test"); + T(typeof(result['_revs_info']) === "object"); + + // test early response + var ddoc = { + _id: "_design/response", + rewrites: stringFun(function(req){ + status = parseInt(req.query.status); + return {code: status, + body: JSON.stringify({"status": status}), + headers: {'x-foo': 'bar', 'Content-Type': 'application/json'}}; + }) + } + T(db.save(ddoc).ok); + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/response/_rewrite?status=200"); + T(xhr.status == 200); + T(xhr.headers['x-foo'] == 'bar'); + T(xhr.responseText == '{"status":200}'); + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/response/_rewrite?status=451"); + T(xhr.status == 451); + T(xhr.headers['Content-Type'] == 'application/json'); + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/response/_rewrite?status=600"); + T(xhr.status == 500); + + + // test path relative to server + var ddoc = { + _id: "_design/relative", + rewrites: stringFun(function(req){ + return '../../../_uuids' + }) + } + T(db.save(ddoc).ok); + var xhr = CouchDB.request("GET", "/"+dbName+"/_design/relative/_rewrite/uuids"); + T(xhr.status == 200); + var result = JSON.parse(xhr.responseText); + T(result.uuids.length == 1); + + // test loop + var ddoc_loop = { + _id: "_design/loop", + rewrites: stringFun(function(req) { + return '_rewrite/loop'; + }) + }; + db.save(ddoc_loop); + var url = "/"+dbName+"/_design/loop/_rewrite/loop"; + var xhr = CouchDB.request("GET", url); + TEquals(400, xhr.status); + } +} diff --git a/test/view_server/query_server_spec.rb b/test/view_server/query_server_spec.rb index 77bc01d70..59883c0eb 100644 --- a/test/view_server/query_server_spec.rb +++ b/test/view_server/query_server_spec.rb @@ -447,6 +447,30 @@ functions = { end. ERLANG }, + "rewrite-basic" => { + "js" => <<-JS, + function(req) { + return "new/location"; + } + JS + "erlang" => <<-ERLANG, + fun(Req) -> + {[{"path", "new/location"}]} + end. + ERLANG + }, + "rewrite-no-rule" => { + "js" => <<-JS, + function(req) { + return; + } + JS + "erlang" => <<-ERLANG, + fun(Req) -> + undefined + end. + ERLANG + }, "error" => { "js" => <<-JS, function() { @@ -747,7 +771,44 @@ describe "query server normal case" do should == true end end + + describe "ddoc rewrites" do + describe "simple rewrite" do + before(:all) do + @ddoc = { + "_id" => "foo", + "rewrites" => functions["rewrite-basic"][LANGUAGE] + } + @qs.teach_ddoc(@ddoc) + end + it "should run normal" do + ok, resp = @qs.ddoc_run(@ddoc, + ["rewrites"], + [{"path" => "foo/bar"}, {"method" => "POST"}] + ) + ok.should == "ok" + resp["path"].should == "new/location" + end + end + + describe "no rule" do + before(:all) do + @ddoc = { + "_id" => "foo", + "rewrites" => functions["rewrite-no-rule"][LANGUAGE] + } + @qs.teach_ddoc(@ddoc) + end + it "should run normal" do + resp = @qs.ddoc_run(@ddoc, + ["rewrites"], + [{"path" => "foo/bar"}, {"method" => "POST"}] + ) + resp.should == ['no_dispatch_rule'] + end + end end +end |