Merge pull request #1138 from rubenvereecken/search-endpoint

Search endpoint is now without the /search prefix
This commit is contained in:
Scott Erickson 2014-06-12 11:33:28 -07:00
commit 8ab69cf833
9 changed files with 118 additions and 73 deletions

View file

@ -4,7 +4,7 @@ ThangType = require 'models/ThangType'
CocoCollection = require 'collections/CocoCollection'
class ThangTypeSearchCollection extends CocoCollection
url: '/db/thang.type/search?project=true'
url: '/db/thang.type?project=true'
model: ThangType
addTerm: (term) ->
@ -73,4 +73,4 @@ module.exports = class AddThangsView extends View
onEscapePressed: ->
@$el.find('input#thang-search').val("")
@runSearch
@runSearch

View file

@ -5,7 +5,7 @@ LevelSystem = require 'models/LevelSystem'
CocoCollection = require 'collections/CocoCollection'
class LevelSystemSearchCollection extends CocoCollection
url: '/db/level_system/search'
url: '/db/level_system'
model: LevelSystem
module.exports = class LevelSystemAddView extends View

View file

@ -21,7 +21,7 @@ componentOriginals =
"physics.Physical" : "524b75ad7fc0f6d519000001"
class ThangTypeSearchCollection extends CocoCollection
url: '/db/thang.type/search?project=original,name,version,slug,kind,components'
url: '/db/thang.type?project=original,name,version,slug,kind,components'
model: ThangType
module.exports = class ThangsTabView extends View

View file

@ -5,7 +5,7 @@ app = require('application')
class SearchCollection extends Backbone.Collection
initialize: (modelURL, @model, @term, @projection) ->
@url = "#{modelURL}/search?project="
@url = "#{modelURL}?project="
if @projection? and not (@projection == [])
@url += projection[0]
@url += ',' + projected for projected in projection[1..]

View file

@ -11,11 +11,5 @@ class AchievementHandler extends Handler
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin()
get: (req, res) ->
query = @modelClass.find({})
query.exec (err, documents) =>
return @sendDatabaseError(res, err) if err
documents = (@formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
module.exports = new AchievementHandler()

View file

@ -64,26 +64,72 @@ module.exports = class Handler
# generic handlers
get: (req, res) ->
# by default, ordinary users never get unfettered access to the database
return @sendUnauthorizedError(res) unless req.user?.isAdmin()
@sendUnauthorizedError(res) if not @hasAccess(req)
# admins can send any sort of query down the wire, though
conditions = JSON.parse(req.query.conditions || '[]')
query = @modelClass.find()
specialParameters = ['term', 'project', 'conditions']
try
for condition in conditions
name = condition[0]
f = query[name]
args = condition[1..]
query = query[name](args...)
catch e
return @sendError(res, 422, 'Badly formed conditions.')
# If the model uses coco search it's probably a text search
if @modelClass.schema.uses_coco_search
term = req.query.term
matchedObjects = []
filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
if @modelClass.schema.uses_coco_permissions and req.user
filters.push {filter: {index: req.user.get('id')}}
projection = null
if req.query.project is 'true'
projection = PROJECT
else if req.query.project
if @modelClass.className is 'User'
projection = PROJECT
log.warn "Whoa, we haven't yet thought about public properties for User projection yet."
else
projection = {}
projection[field] = 1 for field in req.query.project.split(',')
for filter in filters
callback = (err, results) =>
return @sendDatabaseError(res, err) if err
for r in results.results ? results
obj = r.obj ? r
continue if obj in matchedObjects # TODO: probably need a better equality check
matchedObjects.push obj
filters.pop() # doesn't matter which one
unless filters.length
res.send matchedObjects
res.end()
if term
filter.project = projection
@modelClass.textSearch term, filter, callback
else
args = [filter.filter]
args.push projection if projection
@modelClass.find(args...).limit(FETCH_LIMIT).exec callback
# if it's not a text search but the user is an admin, let him try stuff anyway
else if req.user?.isAdmin()
# admins can send any sort of query down the wire
filter = {}
filter[key] = (val for own key, val of req.query.filter when key not in specialParameters) if 'filter' of req.query
query = @modelClass.find(filter)
# Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated')
conditions = JSON.parse(req.query.conditions || '[]')
try
for condition in conditions
name = condition[0]
f = query[name]
args = condition[1..]
query = query[name](args...)
catch e
return @sendError(res, 422, 'Badly formed conditions.')
query.exec (err, documents) =>
return @sendDatabaseError(res, err) if err
documents = (@formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
# regular users are only allowed text searches for now, without any additional filters or sorting
else
return @sendUnauthorizedError(res)
query.exec (err, documents) =>
return @sendDatabaseError(res, err) if err
documents = (@formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
getById: (req, res, id) ->
# return @sendNotFoundError(res) # for testing
@ -154,44 +200,6 @@ module.exports = class Handler
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, document))
# project=true or project=name,description,slug for example
search: (req, res) ->
unless @modelClass.schema.uses_coco_search
return @sendNotFoundError(res)
term = req.query.term
matchedObjects = []
filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
if @modelClass.schema.uses_coco_permissions and req.user
filters.push {filter: {index: req.user.get('id')}}
projection = null
if req.query.project is 'true'
projection = PROJECT
else if req.query.project
if @modelClass.className is 'User'
projection = PROJECT
log.warn "Whoa, we haven't yet thought about public properties for User projection yet."
else
projection = {}
projection[field] = 1 for field in req.query.project.split(',')
for filter in filters
callback = (err, results) =>
return @sendDatabaseError(res, err) if err
for r in results.results ? results
obj = r.obj ? r
continue if obj in matchedObjects # TODO: probably need a better equality check
matchedObjects.push obj
filters.pop() # doesn't matter which one
unless filters.length
res.send matchedObjects
res.end()
if term
filter.project = projection
@modelClass.textSearch term, filter, callback
else
args = [filter.filter]
args.push projection if projection
@modelClass.find(args...).limit(FETCH_LIMIT).exec callback
versions: (req, res, id) ->
# TODO: a flexible system for doing GAE-like cursors for these sort of paginating queries
# Keeping it simple for now and just allowing access to the first FETCH_LIMIT results.

View file

@ -23,7 +23,7 @@ module.exports.PatchablePlugin = (schema) ->
schema.is_patchable = true
schema.index({'target.original':1, 'status':'1', 'created':-1})
RESERVED_NAMES = ['search', 'names']
RESERVED_NAMES = ['names']
module.exports.NamedPlugin = (schema) ->
schema.uses_coco_names = true

View file

@ -34,7 +34,6 @@ module.exports.setup = (app) ->
return handler.getLatestVersion(req, res, parts[1], parts[3]) if parts[2] is 'version'
return handler.versions(req, res, parts[1]) if parts[2] is 'versions'
return handler.files(req, res, parts[1]) if parts[2] is 'files'
return handler.search(req, res) if req.route.method is 'get' and parts[1] is 'search'
return handler.getNamesByIDs(req, res) if req.route.method in ['get', 'post'] and parts[1] is 'names'
return handler.getByRelationship(req, res, parts[1..]...) if parts.length > 2
return handler.getById(req, res, parts[1]) if req.route.method is 'get' and parts[1]?

View file

@ -8,6 +8,8 @@ describe '/db/article', ->
done()
article = {name: 'Yo', body:'yo ma'}
article2 = {name: 'Original', body:'yo daddy'}
url = getURL('/db/article')
articles = {}
@ -27,11 +29,22 @@ describe '/db/article', ->
expect(body.original).toBeDefined()
expect(body.creator).toBeDefined()
articles[0] = body
done()
# Having two articles allow for testing article search and such
request.post {uri:url, json:article2}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.slug).toBeDefined()
expect(body.body).toBeDefined()
expect(body.name).toBeDefined()
expect(body.original).toBeDefined()
expect(body.creator).toBeDefined()
articles[0] = body
done()
it 'allows admins to make new minor versions', (done) ->
new_article = _.clone(articles[0])
new_article.body = '...'
new_article.body = 'yo daddy'
request.post {uri:url, json:new_article}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.version.major).toBe(0)
@ -61,7 +74,6 @@ describe '/db/article', ->
expect(res.statusCode).toBe(200)
expect(body.body).toBe(articles[0].body)
done()
it 'does not allow regular users to make new versions', (done) ->
new_article = _.clone(articles[2])
@ -87,9 +99,41 @@ describe '/db/article', ->
it 'does not allow naming an article a reserved word', (done) ->
loginAdmin ->
new_article = {name: 'Search', body:'is a reserved word'}
new_article = {name: 'Names', body:'is a reserved word'}
request.post {uri:url, json:new_article}, (err, res, body) ->
expect(res.statusCode).toBe(422)
done()
it 'allows regular users to get all articles', (done) ->
loginJoe ->
request.get {uri:url, json:{}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.length).toBe(2)
done()
it 'allows regular users to get articles and use projection', (done) ->
loginJoe ->
# default projection
request.get {uri:url + '?project=true', json:{}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.length).toBe(2)
expect(body[0].created).toBeUndefined()
expect(body[0].version).toBeDefined()
# custom projection
request.get {uri:url + '?project=original', json:{}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.length).toBe(2)
expect(Object.keys(body[0]).length).toBe(2)
expect(body[0].original).toBeDefined()
done()
it 'allows regular users to perform a text search', (done) ->
loginJoe ->
request.get {uri:url + '?term="daddy"', json:{}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.length).toBe(1)
expect(body[0].name).toBe(article2.name)
expect(body[0].body).toBe(article2.body)
done()