summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Shorin <kxepal@apache.org>2015-11-18 21:39:21 +0300
committerAlexander Shorin <kxepal@apache.org>2015-12-22 00:41:31 +0300
commit101e54169b7b9ba389514a6dc00dc7ee401e5566 (patch)
tree71fa5072e1fc4e3aeb74c68f7fb8f2073548a879
parentbdc9681fd6042612fe77f122228a667813673ffd (diff)
downloadcouchdb-101e54169b7b9ba389514a6dc00dc7ee401e5566.tar.gz
Rewrite via query server
COUCHDB-2874
-rw-r--r--rebar.config.script6
-rw-r--r--share/server/loop.js3
-rw-r--r--share/server/render.js26
-rw-r--r--test/javascript/tests/rewrite_js.js340
-rw-r--r--test/view_server/query_server_spec.rb61
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