Skip to content

Commit

Permalink
Merge pull request #112 from nzzdev/release-4.0.0
Browse files Browse the repository at this point in the history
Release 4.0.0
  • Loading branch information
benib authored Apr 6, 2018
2 parents 870ad49 + 683d6e9 commit af66edf
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 64 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Q Server

**Maintainer**: [benib](https://github.com/benib)

__Q__ is a system that lets journalists create visual elements for stories. It is developed by [NZZ Storytelling](https://www.nzz.ch/storytelling) and used in the [NZZ](https://www.nzz.ch) newsroom. There is a Demo over here: https://q-demo.st.nzz.ch

This is the server for the Q Toolbox. To make use of Q server you will also need a [Q editor](https://github.com/nzzdev/Q-editor/).
Expand Down
33 changes: 26 additions & 7 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: Installation
1. Get [CouchDB](https://couchdb.apache.org) up an running. At NZZ we use [Cloudant on IBM Bluemix](https://console.ng.bluemix.net/catalog/services/cloudant-nosql-db). But there are other providers or you can host it yourself. You have to get a little bit familiar with CouchDB.
2. Create a new database to hold all the _item_ documents. Lets call it _items-db_ for now.
3. Implement an authentication scheme: q-server version >3 does not implement an authentication scheme, instead you need to configure a hapi auth scheme called q-auth on your own. At NZZ, we use a scheme letting users log in using the same credentials as the CMS, but you can also use https://github.com/ubilabs/hapi-auth-couchdb-cookie (if updated to support hapi 17) or some other hapi auth plugin or even roll your own. How exactly this works is out of scope for this howto, please find information in the hapi documentation: https://hapijs.com/api
4. Add this document as _items-db/\_design/items_ to get the neccessary views in place
4. Add these design documents to your couchdb

```json
{
Expand All @@ -28,12 +28,31 @@ title: Installation
"function (doc) {\n if (doc.tool) {\n emit(doc.tool, doc._id);\n }\n}",
"reduce": "_count"
}
},
"indexes": {
"search": {
"analyzer": "standard",
"index":
"function (doc) {\n if (doc._id.indexOf('_design/') === 0) {\n return;\n }\n index(\"id\", doc._id);\n if (doc.title) {\n index(\"title\", doc.title);\n }\n if (doc.annotations) {\n index(\"annotations\", doc.annotations);\n }\n if (doc.createdBy) {\n index(\"createdBy\", doc.createdBy);\n }\n if (doc.department) {\n index(\"department\", doc.department);\n }\n if (doc.tool) {\n index(\"tool\", doc.tool)\n }\n if (doc.active !== undefined) {\n index(\"active\", doc.active);\n } else {\n index(\"active\", false);\n }\n if (doc.updatedDate || doc.createdDate) {\n var date;\n if (doc.updatedDate) {\n date = new Date(doc.updatedDate);\n } else if (doc.createdDate) {\n date = new Date(doc.createdDate);\n }\n if (date) {\n index(\"orderDate\", date.valueOf());\n }\n }\n}"
}
}
```
```json
{
"_id": "_design/query-index",
"language": "query",
"views": {
"search-simple-index": {
"map": {
"fields": {
"updatedDate": "desc"
},
"partial_filter_selector": {}
},
"reduce": "_count",
"options": {
"def": {
"fields": [
{
"updatedDate": "desc"
}
]
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nzz/q-server",
"version": "3.4.4",
"version": "4.0.0",
"description": "",
"main": "index.js",
"scripts": {
Expand Down
57 changes: 28 additions & 29 deletions plugins/core/base/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
module.exports = {
name: 'q-base',
dependencies: ['q-db'],
name: "q-base",
dependencies: ["q-db"],
register: async function(server, options) {
await server.register([require("vision"), require("inert")]);

await server.register([
require('vision'),
require('inert')
]);
server.method("getCacheControlDirectivesFromConfig", async function(
cacheControlConfig
) {
const cacheControlDirectives = ["public"];

server.method('getCacheControlDirectivesFromConfig', async function(cacheControlConfig) {
const cacheControlDirectives = [
'public'
];

// return early if no config given
if (!cacheControlConfig) {
return cacheControlDirectives;
Expand All @@ -25,31 +21,34 @@ module.exports = {
cacheControlDirectives.push(`s-maxage=${cacheControlConfig.sMaxAge}`);
}
if (cacheControlConfig.staleWhileRevalidate) {
cacheControlDirectives.push(`stale-while-revalidate=${cacheControlConfig.staleWhileRevalidate}`);
cacheControlDirectives.push(
`stale-while-revalidate=${cacheControlConfig.staleWhileRevalidate}`
);
}
if (cacheControlConfig.staleIfError) {
cacheControlDirectives.push(`stale-if-error=${cacheControlConfig.staleIfError}`);
cacheControlDirectives.push(
`stale-if-error=${cacheControlConfig.staleIfError}`
);
}

return cacheControlDirectives;
});

server.event('item.new');
server.event('item.update');
server.event("item.new");
server.event("item.update");

await server.route([
require('./routes/item.js').get,
require('./routes/item.js').post,
require('./routes/item.js').put,
require('./routes/search.js'),
require('./routes/tool-default.js').getGetRoute(options),
require('./routes/tool-default.js').getPostRoute(options),
require('./routes/tool-schema.js').schema,
require('./routes/tool-schema.js').displayOptionsSchema,
require('./routes/health.js'),
require('./routes/version.js'),
require('./routes/admin/migration.js')
require("./routes/item.js").get,
require("./routes/item.js").post,
require("./routes/item.js").put,
require("./routes/search.js"),
require("./routes/tool-default.js").getGetRoute(options),
require("./routes/tool-default.js").getPostRoute(options),
require("./routes/tool-schema.js").schema,
require("./routes/tool-schema.js").displayOptionsSchema,
require("./routes/health.js"),
require("./routes/version.js"),
require("./routes/admin/migration.js")
]);

}
}
};
33 changes: 25 additions & 8 deletions plugins/core/base/routes/search.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
const Boom = require('boom');
const Joi = require('joi');
const Boom = require("boom");
const Joi = require("joi");

module.exports = {
path: '/search',
method: 'POST',
path: "/search",
method: "GET",
options: {
validate: {
payload: Joi.object().required()
query: {
limit: Joi.number().optional(),
bookmark: Joi.string().optional(),
tool: Joi.alternatives()
.try(Joi.array().items(Joi.string()), Joi.string())
.optional(),
createdBy: Joi.string().optional(),
department: Joi.string().optional(),
publication: Joi.string().optional(),
active: Joi.boolean().optional(),
searchString: Joi.string().optional()
}
},
tags: ['api', 'editor']
tags: ["api", "editor"]
},
handler: async (request, h) => {
return request.server.methods.db.item.search(request.payload);
// Creates new object filterProperties which contains all properties but bookmark and limit
const { bookmark, limit, ...filterProperties } = request.query;
return request.server.methods.db.item.search(
filterProperties,
limit,
bookmark
);
}
}
};
96 changes: 78 additions & 18 deletions plugins/core/db/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
const nano = require('nano');
const Boom = require('boom');
const nano = require("nano");
const Boom = require("boom");

function getSearchFilters(filterProperties) {
return Object.keys(filterProperties).map(parameterName => {
const parameterValue = filterProperties[parameterName];
if (parameterName === "searchString") {
const searchFields = ["_id", "title", "subtitle", "annotations"];
return {
$or: searchFields.map(searchField => {
const searchStringFilter = {};
searchStringFilter[searchField] = {
$regex: `(?i)${parameterValue}`
};
return searchStringFilter;
})
};
}
if (parameterName === "tool" && Array.isArray(parameterValue)) {
return {
$or: parameterValue.map(tool => {
return {
tool: {
$eq: tool
}
};
})
};
}
const filter = {};
filter[parameterName] = {
$eq: parameterValue
};
return filter;
});
}

module.exports = {
name: 'q-db',
register: async function (server, options) {
const dbUrl = `${options.protocol || 'https'}://${options.host}/${options.database}`;
server.log(['info'], `Connecting to database ${dbUrl}`);
name: "q-db",
register: async function(server, options) {
const dbUrl = `${options.protocol || "https"}://${options.host}/${
options.database
}`;
server.log(["info"], `Connecting to database ${dbUrl}`);

const nanoConfig = {
url: dbUrl
Expand All @@ -17,34 +53,36 @@ module.exports = {
user: options.user,
pass: options.pass
}
}
};
}

server.app.db = nano(nanoConfig);

server.method('db.item.getById', function(id, ignoreInactive = false) {
server.method("db.item.getById", function(id, ignoreInactive = false) {
return new Promise((resolve, reject) => {
server.app.db.get(id, (err, item) => {
if (err) {
return reject(new Boom(err.description, { statusCode: err.statusCode } ));
return reject(
new Boom(err.description, { statusCode: err.statusCode })
);
}

if (!ignoreInactive && item.active !== true) {
return reject(Boom.forbidden('Item is not active'));
return reject(Boom.forbidden("Item is not active"));
}
return resolve(item);
})
});
});
});

server.method('db.item.getAllByTool', function(tool) {
server.method("db.item.getAllByTool", function(tool) {
const options = {
keys: [tool],
include_docs: true,
reduce: false
};
return new Promise((resolve, reject) => {
server.app.db.view('items', 'byTool', options, async(err, data) => {
server.app.db.view("items", "byTool", options, async (err, data) => {
if (err) {
return reject(Boom.internal(err));
} else {
Expand All @@ -53,19 +91,41 @@ module.exports = {
});
return resolve(items);
}
})
});
});
});

server.method('db.item.search', function(payload) {
server.method("db.item.search", function(
filterProperties,
limit,
bookmark
) {
return new Promise((resolve, reject) => {
server.app.db.search('items', 'search', payload, (err, data) => {
const requestOptions = {
db: options.database,
path: "_find",
method: "POST",
body: {
selector: {
$and: getSearchFilters(filterProperties)
},
sort: [
{
updatedDate: "desc"
}
],
limit: limit || 18,
bookmark: bookmark || null
}
};

server.app.db.server.request(requestOptions, (err, data) => {
if (err) {
return reject(Boom.internal(err));
} else {
return resolve(data);
}
})
});
});
});
}
Expand Down

0 comments on commit af66edf

Please sign in to comment.