const express = require('express') const router = express.Router() const path = require('path') const fs = require('fs') const ObjectId = require('mongodb').ObjectId const async = require('async') const Ajv = require('ajv') const jsf = require('json-schema-faker') // const CFG = require('../../config') const TAG = 'REST' const REST_CTX = { models: [], api: [ {method: 'GET', url: '/:collection', desc: ''}, {method: 'POST', url: '/:collection', desc: ''}, {method: 'PUT', url: '/:collection/:id', desc: ''}, {method: 'DELETE', url: '/:collection', desc: ''}, {method: 'GET', url: '/:collection/private/json-schema', desc: ''}, {method: 'GET', url: '/:collection/private/json-schema/fake', desc: ''} ] } jsf.extend('faker', function () { return require('faker') }) // router.use((req, res, next) => { // console.log('API isAuth', req.isAuth) // next() // }) function initCollections (pCtx) { pCtx.restapi = {} fs.readdir(path.join(__dirname, './collections'), (pErr, pColls) => { pColls.forEach((pColl) => { if (/^__/.test(pColl)) return let coll = require(path.join(__dirname, './collections', pColl, pColl))(pCtx) if (pCtx.logEnabled) console.log(`[-] ${TAG} | Loading collection: ${coll.name}`) // INIT INDEXES coll.indexes = coll.indexes || [] coll.indexes.forEach((pIndex) => { pCtx.db.collection(coll.name).createIndex(pIndex.keys, pIndex.options) }) pCtx.restapi[coll.name] = coll initRestApi(pCtx, coll) REST_CTX.models.push(coll.name) }) }) router.get('/__intern/collections', function (req, res, next) { let all = [] for (let coll in pCtx.restapi) { all.push({ coll: coll, label: pCtx.restapi[coll].label, icon: pCtx.restapi[coll].icon, description: pCtx.restapi[coll].description }) } res.done(null, all.sort((a, b) => a.label.localeCompare(b.label))) }) router.get('/', function (req, res, next) { res.done(null, REST_CTX) }) } function initRestApi (pCtx, pColl) { let collHandler = pCtx.db.collection(pColl.name) let ajv = new Ajv({removeAdditional: true, allErrors: true}) let collValidator = ajv.compile(pColl.schema) if (collValidator.error) { console.error(collValidator) process.exit(1) } router.use(function (req, res, next) { generateFilterObject(req) next() }) pColl.__services = { read: null, readOne: null, create: null, update: null, delete: null } pColl.__db = collHandler pColl.__addMetadata = function (doc, user, req) { doc.__metadata = { owner: user ? user._id : null, updatedBy: null, createdAt: new Date(), updatedAt: null, deletedAt: null } } pColl.__updateMetadata = function (doc, user) { delete doc.__metadata doc['__metadata.updatedAt'] = new Date() if (user) doc['__metadata.updatedBy'] = user ? user._id : null } pColl.__generateFilterObject = generateFilterObject pColl.__buildRestOp = buildRestOpObject // Add new routes if (pColl.__internal.router) router.use('/' + pColl.name + '/methods/', pColl.__internal.router) /** * @api {get} /api/:collection Get all * @apiName get:/api/:collection * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Get all documents from one collection * * @apiParam {String} collection Required collection * * @apiSuccess {Object} res Response. * @apiSuccess {Object} res.ok 1 if success. * @apiSuccess {Object} res.data List of documents. * @apiSuccess {Object} res.limit Results limitation. * @apiSuccess {Object} res.skip Skipped results. * @apiSuccess {Object} res.count Total results available on server. * @apiSuccess {Object} res.links Links to navigate into documents. * * @apiUse SessionError */ router.get(`/${pColl.name}`, (req, res) => { pColl.__services.read(req.restOp, res.done) }) pColl.__services.read = function (pArgs, pCallback) { let restOp = pArgs || buildRestOpObject() // Find operator let findOp = { $and: [] } findOp.$and.push(restOp.filter) // Projection operator let projOp = Object.keys(restOp.projection).length ? restOp.projection : {} // Find all matching elements collHandler.find(findOp).count(function (pErr, pCount) { collHandler // Filters elements .find(findOp) // Project fields .project(projOp) // Sort result .sort(restOp.sort) // Limit the number .limit(restOp.limit) // Skip .skip(restOp.skip) // Get results .toArray(function (pErr, pResults) { if (pErr) return pCallback(pErr) pCallback(null, { ok: 1, label: pCtx.restapi[pColl.name].label, icon: pCtx.restapi[pColl.name].icon, description: pCtx.restapi[pColl.name].description, data: pResults, limit: restOp.limit, count: pCount, skip: restOp.skip, links: { base: '', next: `/api/${pColl.name}?limit=${restOp.limit}&skip=${restOp.skip + restOp.limit}`, prev: `/api/${pColl.name}?limit=${restOp.limit}&skip=${Math.max(0, restOp.skip - restOp.limit)}` }, sort: restOp.sort }) }) }) } /** * @api {post} /api/:collection Create * @apiName post:/api/:collection * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Create one new document * * @apiSuccess {Object} res Response. * @apiSuccess {Object} res.ok 1 if success. * @apiSuccess {Object} res.data Returned document. * * @apiError Db:DuplicateKey When a field set as unique index has been already created * @apiUse SessionError */ router.post(`/${pColl.name}`, (req, res) => { pColl.__services.create(req.body, req.user, res.done) }) pColl.__services.create = function (pDoc, pUser, pCallback) { collValidator(pDoc) let errors = collValidator.errors if (errors && errors.length) return pCallback(errors) // let tasks = [] let createdDoc = {ok: 0, doc: null} tasks.push(function (pCb) { pColl.hooks.create.before(pCtx, pDoc, function (pErr) { pCb(pErr) }) }) tasks.push(function (pCb) { // delete pDoc._id pColl.__addMetadata(pDoc, pUser) collHandler.insertOne(pDoc, function (pErr, pResult) { if (pErr) { switch (pErr.code) { case 11000: createdDoc = pErr return pCb(new Error('Db:DuplicateKey'), pErr) default: return pCb(pErr) } } createdDoc.ok = pResult.insertedCount createdDoc.doc = pResult.ops[0] pCb() }) }) tasks.push(function (pCb) { pColl.hooks.create.after(pCtx, createdDoc.doc, function (pErr) { pCb(pErr) }) }) async.waterfall(tasks, function (pErr) { pCallback(pErr, createdDoc) }) } /** * @api {get} /api/:collection/:id Get one * @apiName get:/api/:collection/:id * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Get one document * * @apiParam {String} collection Required collection * @apiParam {ObjectId} id Required document Id * * @apiSuccess {Object} res Response. * @apiSuccess {Object} res.ok 1 if success. * @apiSuccess {Object} res.data Returned document. * * @apiError Db:WrongDbId ID parameter is not a MongoDB ObjectId * @apiError Db:NotFound Required document ID not found in DB * @apiUse SessionError */ router.get(`/${pColl.name}/:id`, (req, res) => { pColl.__services.readOne(req.params.id, req.restOp, res.done) }) pColl.__services.readOne = function (pId, pArgs, pCallback) { // Convert ID if it's a correct one else returns one error let objectId = getObjectId(pId, pColl.customId) if (!objectId) { return pCallback(new Error('Db:WrongDbId')) } let tasks = [] let oneDoc = {ok: 0, id: pId, data: null} // Call before hook to transform data tasks.push(function (pCb) { pColl.hooks.readOne.before(pCtx, objectId, function (pErr) { pCb(pErr) }) }) // Get one document tasks.push(function (pCb) { collHandler .find({_id: objectId}) .project(pArgs.projection) .limit(1) .next(function (pErr, pDoc) { if (pErr) return pCallback(pErr) if (!pDoc) return pCallback(400, new Error('Db:NotFound')) oneDoc.ok = 1 oneDoc.label = pCtx.restapi[pColl.name].label oneDoc.icon = pCtx.restapi[pColl.name].icon oneDoc.description = pCtx.restapi[pColl.name].description oneDoc.data = pDoc pCb() }) }) // Call after hook to perform some operations tasks.push(function (pCb) { pColl.hooks.readOne.after(pCtx, oneDoc.data, function (pErr) { pCb(pErr) }) }) async.waterfall(tasks, function (pErr) { pCallback(pErr, oneDoc) }) } /** * @api {put} /api/:collection/:id Update * @apiName put:/api/:collection/:id * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Update some fields of one document * * @apiParam {String} collection Required collection * @apiParam {ObjectId} id Required document Id * * @apiSuccess {Object} res Response. * @apiSuccess {Object} res.ok 1 if success. * @apiSuccess {Object} res.data Returned document. * * @apiError Db:WrongDbId ID parameter is not a MongoDB ObjectId * @apiError Db:NotFound Required document ID not found in DB * @apiUse SessionError */ router.put(`/${pColl.name}/:id`, (req, res) => { pColl.__services.update(req.params.id, req.body, req.user, 'router', res.done) }) pColl.__services.update = function (pId, pDoc, pUser, pOrigin, pCallback) { // console.log(pDoc) // Convert ID if it's a correct one else returns one error let objectId = getObjectId(pId, pColl.customId) if (!objectId) { return pCallback(new Error('Db:WrongDbId')) } // collValidator(pDoc) if (collValidator.errors) { // console.dir(collValidator.errors, {depth: null, colors: true}) let errors = collValidator.errors.filter(error => error.keyword !== 'required') if (errors.length) return pCallback('JSONSchema:ValidationFailed', errors) } // let tasks = [] let updatedDoc = { ok: 0, id: pId, doc: null } // Call before hook to transform data tasks.push(function (pCb) { pColl.hooks.update.before(pCtx, objectId, pDoc, pOrigin, function (pErr) { pCb(pErr) }) }) // Save in DB tasks.push(function (pCb) { delete pDoc._id // console.log(pDoc) pColl.__updateMetadata(pDoc, pUser) collHandler .findOneAndUpdate( {_id: objectId}, {$set: pDoc}, {returnOriginal: false}, function (pErr, pResult) { if (pErr) return pCb(pErr) updatedDoc.ok = pResult.ok updatedDoc.doc = pResult.value pCb() }) }) // Call after hook to perform some operations tasks.push(function (pCb) { pColl.hooks.update.after(pCtx, updatedDoc.doc, pOrigin, function (pErr) { pCb(pErr) }) }) async.waterfall(tasks, function (pErr) { pCallback(pErr, updatedDoc) }) } /** * @api {delete} /api/:collection/:id Delete * @apiName delete:/api/:collection/:id * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Delete one document * * @apiParam {String} collection Required collection * @apiParam {ObjectId} id Required document Id * * @apiSuccess {Object} res Response. * @apiSuccess {Object} res.id [DbId] of deleted document. * @apiSuccess {Object} res.ok 1 if success. * * @apiError Db:WrongDbId ID parameter is not a MongoDB ObjectId * @apiUse SessionError */ router.delete(`/${pColl.name}/:id`, (req, res) => { pColl.__services.delete(req.params.id, req.user, res.done) }) pColl.__services.delete = function (pId, pUser, pCallback) { // Convert ID if it's a correct one else returns one error let objectId = getObjectId(pId, pColl.customId) if (!objectId) { return pCallback(new Error('Db:WrongDbId')) } let tasks = [] let deletedDoc = {ok: 0, id: pId} // Call before hook to transform data tasks.push(function (pCb) { pColl.hooks.delete.before(pCtx, objectId, function (pErr) { pCb(pErr) }) }) // Get one document tasks.push(function (pCb) { collHandler.removeOne({_id: objectId}, function (pErr, pDoc) { if (pErr) return pCb(pErr) deletedDoc.ok = pDoc.deletedCount pCb() }) }) // Call after hook to perform some operations tasks.push(function (pCb) { pColl.hooks.delete.after(pCtx, true, function (pErr) { pCb(pErr) }) }) async.waterfall(tasks, function (pErr) { pCallback(pErr, deletedDoc) }) } /** * @api {get} /api/:collection/private/json-schema Get json schema * @apiName get:/api/:collection/private/json-schema * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Get JSON schema defined for this collection * * @apiParam {String} collection Required collection * * @apiSuccess {Json} schema JSON schema. * * @apiUse SessionError */ router.get(`/${pColl.name}/private/json-schema`, (req, res) => { res.done(null, pColl.schema) }) router.get(`/${pColl.name}/private/json-form`, (req, res) => { res.done(null, pColl.form || []) }) router.get(`/${pColl.name}/private/json-form-create`, (req, res) => { res.done(null, pColl.formCreate || []) }) router.get(`/${pColl.name}/private/listing-header`, (req, res) => { res.done(null, pColl.listingHeader || [{ text: 'Nom', align: 'left', value: 'name' }]) }) router.get(`/${pColl.name}/private/new-objectid`, (req, res) => { res.done(null, new ObjectId()) }) router.put(`/${pColl.name}/upsert/from/:field`, (req, res) => { let findOp = { [req.params.field]: req.body[req.params.field] } let cat = req.body.categories delete req.body[req.params.value] delete req.body.categories let upOp = {$set: req.body} if (cat) { upOp['$addToSet'] = {categories: cat} } collHandler.findOneAndUpdate( findOp, upOp, {upsert: true}, res.done) }) /** * @api {get} /api/:collection/private/json-schema/fake Get fake schema * @apiName get:/api/:collection/private/json-schema/fake * @apiVersion 0.0.1 * @apiGroup Restapi * @apiDescription Get a fake JSON from schema defined for this collection * * @apiParam {String} collection Required collection * * @apiSuccess {Json} schema Fake JSON. * * @apiUse SessionError */ router.get(`/${pColl.name}/private/json-schema/fake`, (req, res) => { jsf.option({ alwaysFakeOptionals: true }) jsf .resolve(pColl.schema) .then(function (result) { if (result.planning) { result.planning.byFormat = { bannersHome: { byAirline: { AirFrance: { deltaImpressions: 10000, allowedFormats: ['iab-leaderboard', 'iab-squra'] }, TAROM: { deltaImpressions: -10000, allowedFormats: ['iab-squra'] } } } } } res.done(null, result) }) .catch(function (error) { res.done(error) }) }) } function buildRestOpObject () { return { filter: {}, projection: {}, sort: {}, limit: 100, skip: 0 } } function generateFilterObject (pReq) { var filterObject = {} var sortObject = {} var projObject = {} var request = null var queries = pReq.query var queryKeys = Object.keys(queries) var length = queryKeys.length var limit = 0 var skip = 0 var index, key, localData, lQueries, regArray, match, query, type var deletedDocs = null for (index = 0; index < length; index++) { key = queryKeys[index] if (key === 'limit') { limit = parseInt(queries.limit) } else if (key === 'skip') { skip = parseInt(queries.skip) } else if (key === 'projection') { queries.projection.split(',').forEach(function (pProj) { var projField = pProj.split(':') projObject[projField[0]] = +projField[1] }) } else if (key === 'sort') { queries.sort.split(',').forEach(function (pSort) { if (pSort[0] === '-') { sortObject[pSort.substring(1)] = -1 } else { sortObject[pSort] = 1 } }) } else if (key === 'search') { filterObject['$text'] = { '$search': queries.search } } else if (key === 'deletedDocs') { deletedDocs = queries.deletedDocs } else { request = key.split('__') if (request.length <= 1 || !queries[key]) { // console.log('unknown key', key); continue } match = queries[key].match(/\((.*)\)(.*)/) if (match) { type = match[1] query = castValue(match[2], type) } else { type = null query = queries[key] } filterObject[request[0]] = filterObject[request[0]] || {} switch (request[1]) { case 'eq' : if (!type) { switch (query) { case 'true': query = true break case 'false': query = false break default: if (!isNaN(query)) { query = +query } } } filterObject[request[0]]['$eq'] = query break case 'lt' : filterObject[request[0]]['$lt'] = query break case 'lte' : filterObject[request[0]]['$lte'] = query break case 'gt' : filterObject[request[0]]['$gt'] = query break case 'gte' : filterObject[request[0]]['$gte'] = query break case 'ne' : filterObject[request[0]]['$ne'] = query break case 'regex' : regArray = query.split(',') filterObject[request[0]]['$regex'] = new RegExp(regArray[0], regArray[1]) break case 'regexNot' : regArray = query.split(',') filterObject[request[0]]['$not'] = new RegExp(regArray[0], regArray[1]) break case 'exists' : filterObject[request[0]]['$exists'] = query == 'true' break case 'in' : case 'all' : case 'nin' : if (typeof query === 'string') { localData = query.split(',') } else { localData = [query] } if (!type && request[0] === '_id') { type = 'ObjectId' } if (type) { lQueries = [] localData.forEach(function (pElem) { lQueries.push(castValue(pElem, type)) }) } else { lQueries = localData } filterObject[request[0]]['$' + request[1]] = lQueries break default: ; } } } // if (!deletedDocs) { // filterObject['__metadata.deletionDate'] = { $exists: false } // } else { // if (deletedDocs === 'only') { // filterObject['__metadata.deletionDate'] = { $exists: true } // } // } pReq.restOp = buildRestOpObject() pReq.restOp.filter = filterObject pReq.restOp.projection = projObject pReq.restOp.sort = sortObject pReq.restOp.limit = limit || 100 pReq.restOp.skip = skip return pReq.restOp } function getObjectId (pId, pCustomId) { let objectId = pCustomId ? pId : null try { objectId = ObjectId(pId) } catch (e) {} return objectId } function castValue (pValue, pType) { var query = pValue switch (pType) { case 'Number': query = +query break case 'Boolean': query = query == 'true' break case 'Date': query = new Date(query) break case 'ObjectId': try { query = ObjectId(query) } catch (e) { // query = query } break case 'NULL': query = null break default: console.warn('Unknown cast type', pType) } return query } module.exports = function (pCtx) { initCollections(pCtx) return router }