You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

734 lines
20 KiB

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
}