diff --git a/app/views/editor/level/add_thangs_view.coffee b/app/views/editor/level/add_thangs_view.coffee index cf1824201..2a635b2c9 100644 --- a/app/views/editor/level/add_thangs_view.coffee +++ b/app/views/editor/level/add_thangs_view.coffee @@ -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 \ No newline at end of file + @runSearch diff --git a/app/views/editor/level/system/add.coffee b/app/views/editor/level/system/add.coffee index 64caa52b4..fb42a866b 100644 --- a/app/views/editor/level/system/add.coffee +++ b/app/views/editor/level/system/add.coffee @@ -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 diff --git a/app/views/editor/level/thangs_tab_view.coffee b/app/views/editor/level/thangs_tab_view.coffee index 22a504480..fb5443460 100644 --- a/app/views/editor/level/thangs_tab_view.coffee +++ b/app/views/editor/level/thangs_tab_view.coffee @@ -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 diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index c8f0bc077..8f7a17420 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -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..] diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 317a6f1b8..fe5f814ba 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -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() diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index e8fdbd668..eb8d73ca3 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -63,26 +63,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 @@ -153,44 +199,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. diff --git a/server/routes/db.coffee b/server/routes/db.coffee index f65a56744..cddc70592 100644 --- a/server/routes/db.coffee +++ b/server/routes/db.coffee @@ -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]? diff --git a/test/server/functional/article.spec.coffee b/test/server/functional/article.spec.coffee index a3973007b..84c414a86 100644 --- a/test/server/functional/article.spec.coffee +++ b/test/server/functional/article.spec.coffee @@ -92,4 +92,33 @@ describe '/db/article', -> expect(res.statusCode).toBe(422) done() - \ No newline at end of file + it 'allows regular users to get all articles', (done) -> + loginJoe -> + request.get {uri:url}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(2) + + it 'allows regular users to get articles and use projection', (done) -> + loginJoe -> + # default projection + request.get {uri:url + '?project=true'}, (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'}, (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="friend"'}, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.length).toBe(1) + # expect name blabla + done()