Skip to content

Commit

Permalink
Merge branch 'release-4.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
jimlambie committed Aug 1, 2018
2 parents 3ee17e7 + 8a1bc63 commit 835449c
Show file tree
Hide file tree
Showing 40 changed files with 10,510 additions and 472 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [4.1.0] (2018-08-01)

### Added

Two new features in this version of API, see https://docs.dadi.cloud/api for full details:

- Multi-language support
- Document indexing and Search

## [4.0.4] (2018-07-30)

### Fixed
Expand Down
45 changes: 45 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,34 @@ var conf = convict({
env: 'DB_AUTH_NAME'
}
},
search: {
enabled: {
doc: 'If true, API responds to collection /search endpoints',
format: Boolean,
default: false
},
minQueryLength: {
doc: 'Minimum search string length',
format: Number,
default: 3
},
wordCollection: {
doc: 'The name of the datastore collection that will hold tokenized words',
format: String,
default: 'words'
},
datastore: {
doc: 'The datastore to use for storing and querying indexed documents',
format: String,
default: '@dadi/api-mongodb'
},
database: {
doc: 'The name of the database to use for storing and querying indexed documents',
format: String,
default: 'search',
env: 'DB_SEARCH_NAME'
}
},
caching: {
ttl: {
doc: '',
Expand Down Expand Up @@ -370,6 +398,23 @@ var conf = convict({
format: Number,
default: 10
}
},
i18n: {
defaultLanguage: {
doc: 'ISO-639-1 code of the default language',
format: String,
default: 'en'
},
languages: {
doc: 'List of ISO-639-1 codes for the supported languages',
format: Array,
default: []
},
fieldCharacter: {
doc: 'Special character to denote a translated field',
format: String,
default: ':'
}
}
})

Expand Down
7 changes: 7 additions & 0 deletions config/config.test.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
"defaultBucket": "mediaStore",
"basePath": "test/acceptance/temp-workspace/media"
},
"search": {
"enabled": false,
"minQueryLength": 3,
"wordCollection": "words",
"datastore": "./../../../test/test-connector",
"database": "search"
},
"feedback": false,
"cors": false,
"cluster": false
Expand Down
49 changes: 24 additions & 25 deletions config/mongodb.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,35 @@
"ssl": false,
"replicaSet": "",
"enableCollectionDatabases": false,
"testdb": {
"hosts": [
{
"host": "127.0.0.1",
"port": 27017
}
]
"databases": {
"test_auth_db": {
"hosts": [
{
"host": "127.0.0.1",
"port": 27017
}
],
"username": "",
"password": "",
"replicaSet": "",
"ssl": false
},
"testdb": {
"hosts": [
{
"host": "127.0.0.1",
"port": 27017
}
],
"username": "",
"password": "",
},
"test_auth_db": {
"search": {
"hosts": [
{
"host": "127.0.0.1",
"port": 27017
}
],
"username": "",
"password": "",
"replicaSet": "",
"ssl": false
]
},
"testdb": {
"hosts": [
{
"host": "127.0.0.1",
"port": 27017
}
],
"username": "",
"password": "",
"replicaSet": "",
"ssl": false
}
}
8 changes: 4 additions & 4 deletions dadi/lib/controller/documents.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ Collection.prototype.delete = function (req, res, next) {
}

Collection.prototype.get = function (req, res, next) {
let path = url.parse(req.url, true)
let options = path.query
let options = this._getURLParameters(req.url)
let callback = options.callback || this.model.settings.callback

// Determine if this is JSONP.
Expand All @@ -102,6 +101,7 @@ Collection.prototype.get = function (req, res, next) {

return this.model.get({
client: req.dadiApiClient,
language: options.lang,
query,
options: queryOptions,
req
Expand Down Expand Up @@ -214,7 +214,7 @@ Collection.prototype.registerRoutes = function (route, filePath) {
})

// Creating generic route.
this.server.app.use(`${route}/:id(${this.ID_PATTERN})?/:action(count|stats)?`, (req, res, next) => {
this.server.app.use(`${route}/:id(${this.ID_PATTERN})?/:action(count|search|stats)?`, (req, res, next) => {
try {
// Map request method to controller method.
let method = req.params.action || (req.method && req.method.toLowerCase())
Expand Down Expand Up @@ -252,7 +252,7 @@ Collection.prototype.stats = function (req, res, next) {

Collection.prototype.unregisterRoutes = function (route) {
this.server.app.unuse(`${route}/config`)
this.server.app.unuse(`${route}/:id(${this.ID_PATTERN})?/:action(count|stats)?`)
this.server.app.unuse(`${route}/:id(${this.ID_PATTERN})?/:action(count|search|stats)?`)
}

module.exports = function (model, server) {
Expand Down
78 changes: 67 additions & 11 deletions dadi/lib/controller/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,21 @@ const ID_PATTERN = '[a-fA-F0-9-]*'

const Controller = function () {}

Controller.prototype._getURLParameters = function (requestUrl) {
let parsedUrl = url.parse(requestUrl, true)

return parsedUrl.query
}

Controller.prototype._prepareQuery = function (req) {
let path = url.parse(req.url, true)
let apiVersion = path.pathname.split('/')[1]
let options = path.query
let options = this._getURLParameters(req.url)
let query = help.parseQuery(options.filter)

// Formatting query
query = this.model.formatQuery(query)

// Remove filter params that don't exist in
// the model schema.
// if (!Array.isArray(query)) {
// Object.keys(query).forEach(key => {
// if (!this.model.isKeyValid(key)) {
// delete query[key]
// }
// })
// }

// If id is present in the url, add to the query.
if (req.params && req.params.id) {
Object.assign(query, {
Expand Down Expand Up @@ -106,6 +102,11 @@ Controller.prototype._prepareQueryOptions = function (options) {
)
}

// `q` represents a search query, e.g. `?q=foo bar baz`.
if (options.q) {
queryOptions.search = options.q
}

// Specified / default number of records to return.
let limit = parseInt(options.count || settings.count) || 50

Expand Down Expand Up @@ -165,6 +166,61 @@ Controller.prototype._prepareQueryOptions = function (options) {

Controller.prototype.ID_PATTERN = ID_PATTERN

/**
* Handle collection search endpoints
* Example: /1.0/library/books/search?q=title
*/
Controller.prototype.search = function (req, res, next) {
let path = url.parse(req.url, true)
let options = path.query

let queryOptions = this._prepareQueryOptions(options)

if (queryOptions.errors.length !== 0) {
return help.sendBackJSON(400, res, next)(null, queryOptions)
} else {
queryOptions = queryOptions.queryOptions
}

return this.model.search({
client: req.dadiApiClient,
options: queryOptions
}).then(query => {
let ids = query._id['$containsAny'].map(id => id.toString())

return this.model.find({
client: req.dadiApiClient,
language: options.lang,
query,
options: queryOptions
}).then(results => {
results.results = results.results.sort((a, b) => {
let aIndex = ids.indexOf(a._id.toString())
let bIndex = ids.indexOf(b._id.toString())

if (aIndex === bIndex) return 0

return aIndex > bIndex ? 1 : -1
})

return this.model.formatForOutput(
results.results,
{
client: req.dadiApiClient,
composeOverride: queryOptions.compose,
language: options.lang,
urlFields: queryOptions.fields
}
).then(formattedResults => {
results.results = formattedResults
return help.sendBackJSON(200, res, next)(null, results)
})
})
}).catch(error => {
return help.sendBackJSON(null, res, next)(error)
})
}

module.exports = function (model) {
return new Controller(model)
}
Expand Down
65 changes: 65 additions & 0 deletions dadi/lib/controller/languages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const acl = require('./../model/acl')
const config = require('./../../../config')
const help = require('./../help')
const langs = require('langs')

const FORBIDDEN_FIELD_CHARACTERS = ['.', '@']

const Languages = function (server) {
FORBIDDEN_FIELD_CHARACTERS.forEach(character => {
if (config.get('i18n.fieldCharacter').includes(character)) {
throw new Error(
`Fatal error in configuration: character "${character}" is not allowed in "i18n.fieldCharacter" value`
)
}
})

server.app.routeMethods('/api/languages', {
get: this.get.bind(this)
})
}

Languages.prototype._getLanguageDetails = function (code) {
let defaultLanguage = config.get('i18n.defaultLanguage')
let match = langs.where('1', code)
let language = {
code,
default: defaultLanguage === code
}

if (match) {
language.name = match.name
language.local = match.local
}

return language
}

Languages.prototype.get = function (req, res, next) {
if (!req.dadiApiClient.clientId) {
return help.sendBackJSON(null, res, next)(
acl.createError(req.dadiApiClient)
)
}

let supportedLanguages = config.get('i18n.languages')
let defaultLanguage = config.get('i18n.defaultLanguage')

if (!supportedLanguages.includes(defaultLanguage)) {
supportedLanguages.unshift(defaultLanguage)
}

let languages = supportedLanguages.map(this._getLanguageDetails)

let metadata = {
defaultLanguage: this._getLanguageDetails(defaultLanguage),
totalCount: supportedLanguages.length
}

return help.sendBackJSON(200, res, next)(null, {
results: languages,
metadata
})
}

module.exports = server => new Languages(server)
Loading

0 comments on commit 835449c

Please sign in to comment.