foxxy
Version:
Foxx framework based on RiotJS + ArangoDB/Foxx
479 lines (454 loc) • 18.1 kB
JavaScript
;
const db = require('@arangodb').db;
const joi = require('joi');
const models = require('./models.js');
const _ = require('lodash');
const createAuth = require('@arangodb/foxx/auth');
const auth = createAuth();
const createRouter = require('@arangodb/foxx/router');
const sessionsMiddleware = require('@arangodb/foxx/sessions');
const jwtStorage = require('@arangodb/foxx/sessions/storages/jwt');
require("@arangodb/aql/cache").properties({ mode: "on" });
const router = createRouter();
const _settings = db.foxxy_settings.firstExample();
const sessions = sessionsMiddleware({
storage: jwtStorage({ secret: _settings.jwt_secret, ttl: 60 * 60 * 24 * 365 }),
ttl: 60 * 60 * 24 * 365, // one year in seconds
transport: 'header'
});
module.context.use(sessions);
module.context.use(router);
var typeCast = function(type, value) {
var value = unescape(value)
if(type == "integer") value = parseInt(value)
if(type == "float") value = parseFloat(value)
if (type == "html") value = JSON.parse(value)
return value
}
var fieldsToData = function(fields, body, headers) {
var data = {}
_.each(fields, function(f) {
if(f.tr != true) {
if(_.isArray(body[f.n])) {
data[f.n] = _.map(body[f.n], function(v) { return typeCast(f.t,v) })
} else {
if(body[f.n] === undefined) {
if(f.t == "boolean") data[f.n] = false
else data[f.n] = null
} else {
if(f.t == "boolean") data[f.n] = true
else data[f.n] = typeCast(f.t, body[f.n])
if(f.t == "password" || f.t == "password_confirmation") {
data.authData = auth.create(body[f.n])
delete data[f.n]
}
}
}
} else {
data[f.n] = {}
if(_.isArray(body[f.n])) {
data[f.n][headers['foxx-locale']] = _.map(
body[f.n], function(v) { return typeCast(f.t,v) }
)
} else {
data[f.n][headers['foxx-locale']] = typeCast(f.t, body[f.n])
}
}
})
return data
}
var save_revision = function (uid, object, data, max) {
db.revisions.save({
data: data, object_id: object._id, c_at: (+new Date()), user_key: uid
})
db._query(`
LET rev_ids = (
FOR doc IN revisions FILTER doc.object_id == @id
SORT doc.c_at LIMIT @max, 100 RETURN doc._key
)
FOR id IN rev_ids
REMOVE { _key: id } IN revisions
`, { id: object._id, max: max })
}
// Comment this block if you want to avoid authorization
module.context.use(function (req, res, next) {
if(!req.session.uid) res.throw('unauthorized')
res.setHeader("Access-Control-Expose-Headers", "X-Session-Id")
next();
});
// -----------------------------------------------------------------------------
router.get('/:service/page/:page/:perpage', function (req, res) {
let order = models()[req.pathParams.service].sort || 'SORT doc._key DESC'
if(models()[req.pathParams.service].sortable) order = 'SORT doc.order ASC'
let includes = ''
let include_merge = ''
if(models()[req.pathParams.service].includes) {
includes = models()[req.pathParams.service].includes.conditions
include_merge = models()[req.pathParams.service].includes.merges
}
let folder = ''
let folder_params = {}
if (req.queryParams.folder && req.queryParams.is_root != 'true') {
folder = 'FILTER doc.folder_key == @folder'
folder_params = { folder: req.queryParams.folder }
}
if (req.queryParams.is_root == 'true') {
folder = 'FILTER !HAS(doc, "folder_key") || doc.folder_key == @folder'
folder_params = { folder: req.queryParams.folder }
}
var data = db._query(`
LET count = LENGTH(FOR doc IN @@collection ${folder} RETURN 1)
LET data = (
FOR doc IN @@collection
LET image = (FOR u IN uploads FILTER u.object_id == doc._id SORT u.pos LIMIT 1 RETURN u)[0]
${folder} ${order} ${includes} LIMIT @offset, @perpage
RETURN MERGE(doc, { image: image ${include_merge} })
)
RETURN { count, data }
`, _.merge({
"@collection": req.pathParams.service,
"offset": (req.pathParams.page - 1) * parseInt(req.pathParams.perpage),
"perpage": parseInt(req.pathParams.perpage)
}, folder_params)
).toArray()
res.send({
model: models()[req.pathParams.service],
data
});
})
.header('X-Session-Id')
.description('Returns all objects');
// -----------------------------------------------------------------------------
router.get('/:service/search/:term', function (req, res) {
var locale = req.headers['foxx-locale']
if(locale.match(/[a-z]+/) == null) locale = 'en'
const order = models()[req.pathParams.service].sort || 'SORT doc._key DESC'
let includes = ''
let include_merge = ''
if(models()[req.pathParams.service].includes) {
includes = models()[req.pathParams.service].includes.conditions
include_merge = models()[req.pathParams.service].includes.merges
}
if(locale.match(/[a-z]+/) == null) locale = 'en'
res.send({ data: db._query(`
FOR doc IN FULLTEXT(@@collection, 'search.${locale}', @term)
LET image = (FOR u IN uploads FILTER u.object_id == doc._id SORT u.pos LIMIT 1 RETURN u)[0]
${order}
${includes}
LIMIT 100
RETURN MERGE(doc, { image: image ${include_merge} })`, { "@collection": req.pathParams.service,
"term": req.pathParams.term }).toArray() })
})
.header('foxx-locale')
.header('X-Session-Id')
.description('Returns First 100 found objects');
// -----------------------------------------------------------------------------
router.get('/:service/:id', function (req, res) {
const collection = db._collection(req.pathParams.service)
res.send({ fields: models()[req.pathParams.service],
data: collection.document(req.pathParams.id) });
})
.header('X-Session-Id')
.description('Returns object within ID');
// -----------------------------------------------------------------------------
router.get('/:service/fields', function (req, res) {
res.send({ fields: models()[req.pathParams.service] });
})
.header('X-Session-Id')
.description('Get all fields to build form');
// -----------------------------------------------------------------------------
router.post('/:service', function (req, res) {
const collection = db._collection(req.pathParams.service)
let fields = models()[req.pathParams.service]
const body = JSON.parse(req.body.toString())
var obj = null
var errors = []
if(!_.isArray(fields)) fields = fields.model
try {
var schema = {}
_.each(fields, function (f) {
schema[f.n] = _.isString(f.j) ? schema[f.n] = eval(f.j) : schema[f.n] = f.j
})
if (models()[req.pathParams.service].act_as_tree) {
schema['folder_key'] = joi.string().required()
}
errors = joi.validate(body, schema, { abortEarly: false }).error.details
}
catch(e) {}
if(errors.length == 0) {
var data = fieldsToData(fields, body, req.headers)
if (models()[req.pathParams.service].act_as_tree) {
data['folder_key'] = body.folder_key
}
if(models()[req.pathParams.service].search) {
var search_arr = []
_.each(models()[req.pathParams.service].search, function(s) {
if(_.isPlainObject(data[s])) {
search_arr.push(data[s][req.headers['foxx-locale']])
} else {
search_arr.push(data[s])
}
})
data.search = {}
data.search[req.headers['foxx-locale']] = search_arr.join(" ")
}
if(models()[req.pathParams.service].timestamps === true) { data.created_at = +new Date() }
if(models()[req.pathParams.service].slug) {
var slug = _.map(models()[req.pathParams.service].slug, function(field_name) {
return data[field_name]
})
data['slug'] = _.kebabCase(slug)
}
var filter_by_folder = ''
var folder_params = {}
if (models()[req.pathParams.service].act_as_tree) {
filter_by_folder = 'FILTER doc.folder_key == @folder'
folder_params['folder'] = body.folder_key
}
data['order'] = db._query(`
LET docs = (FOR doc IN @@collection ${filter_by_folder} RETURN 1)
RETURN LENGTH(docs)
`, _.merge({ "@collection": req.pathParams.service }, folder_params)
).toArray()[0]
obj = collection.save(data, { waitForSync: true })
save_revision(req.session.uid, obj, data, 10)
}
res.send({ success: errors.length == 0, data: obj, errors: errors });
}).header('foxx-locale')
.header('X-Session-Id')
.description('Create a new object.');
// -----------------------------------------------------------------------------
// TODO : change to PUT verb
router.post('/:service/:id', function (req, res) {
const collection = db._collection(req.pathParams.service)
let fields = models()[req.pathParams.service]
const body = JSON.parse(req.body.toString())
var obj = null
var errors = []
if(!_.isArray(fields)) fields = fields.model
try {
var schema = {}
_.each(fields, function (f) {
let validate = f.ju ? f.ju : f.j
schema[f.n] = _.isString(validate) ? eval(validate) : validate
})
errors = joi.validate(body, schema, { abortEarly: false }).error.details
}
catch(e) {}
if(errors.length == 0) {
var object = collection.document(req.pathParams.id)
var can_save = true
if (object.locked_by) {
const user = db.users.document(req.session.uid)
if(object.locked_by != user.fn + ' ' + user.ln) can_save = false
}
var data = fieldsToData(fields, body, req.headers)
if(models()[req.pathParams.service].search) {
data.search = {}
var search_arr = []
_.each(models()[req.pathParams.service].search, function(s) {
if(_.isPlainObject(data[s])) {
search_arr.push(data[s][req.headers['foxx-locale']])
} else {
search_arr.push(data[s])
}
})
data.search[req.headers['foxx-locale']] = search_arr.join(" ")
}
if(models()[req.pathParams.service].timestamps === true) { data.updated_at = +new Date() }
if(models()[req.pathParams.service].slug) {
var slug = _.map(models()[req.pathParams.service].slug, function(field_name) {
return data[field_name]
})
data['slug'] = _.kebabCase(slug)
}
if (can_save) {
obj = collection.update(object, data)
save_revision(req.session.uid, object, data, 10)
}
}
res.send({ success: errors.length == 0 && can_save, data: obj, errors: errors });
})
.header('foxx-locale')
.header('X-Session-Id')
.description('Update an object.');
// -----------------------------------------------------------------------------
router.patch('/:service/:id/:field/toggle', function (req, res) {
const collection = db._collection(req.pathParams.service)
var item = collection.firstExample({_key: req.pathParams.id})
let column = _.first(_.filter(models()[req.pathParams.service].columns, function(el) { return el.name == req.pathParams.field}))
if(item) {
var data = {}
data[req.pathParams.field] = !item[req.pathParams.field]
collection.update(item, data)
var returned_data = !item[req.pathParams.field]
if(column && column.values) returned_data = column.values[!item[req.pathParams.field]]
res.send({ success: true, data: returned_data })
} else {
res.send({ success: false })
}
})
.header('foxx-locale')
.header('X-Session-Id')
.description('Toggle boolean field.');
// -----------------------------------------------------------------------------
router.get('/:service/:id/duplicate', function (req, res) {
var new_obj = db._query(`
FOR doc IN @@collection
FILTER doc._key == @key
INSERT UNSET( doc, "_id", "_key", "_rev" ) IN @@collection RETURN NEW
`, { "@collection": req.pathParams.service, key: req.pathParams.id }).toArray()[0]
res.send(new_obj);
})
.header('X-Session-Id')
.description('duplicate an object.');
// -----------------------------------------------------------------------------
router.delete('/:service/:id', function (req, res) {
const collection = db._collection(req.pathParams.service)
collection.remove(req.pathParams.service + "/" + req.pathParams.id)
db.revisions.removeByExample(
{ object_id: req.pathParams.service + "/" + req.pathParams.id }
)
res.send({success: true });
})
.header('X-Session-Id')
.description('delete an object.');
// -----------------------------------------------------------------------------
router.put('/:service/orders/:from/:to', function (req, res) {
const collection = db._collection(req.pathParams.service)
const from = parseInt(req.pathParams.from)
const to = parseInt(req.pathParams.to)
var filter_by_folder = ''
var folder_params = {}
if (req.queryParams) {
filter_by_folder = 'FILTER doc.folder_key == @folder'
folder_params['folder'] = req.queryParams.folder_key
}
var doc = db._query(
`FOR doc IN @@collection ${filter_by_folder} SORT doc.order ASC LIMIT @pos, 1 RETURN doc`,
_.merge({ "@collection": req.pathParams.service, pos: parseInt(req.pathParams.from) }, folder_params)
).toArray()[0]
if (from < to) {
db._query(
`FOR doc IN @@collection ${filter_by_folder} FILTER doc.order <= @to and doc.order >= @from and doc._key != @key
UPDATE({ _key: doc._key, order: doc.order - 1 }) IN @@collection`,
_.merge({ "@collection": req.pathParams.service, from, to, key: doc._key }, folder_params)
)
} else {
db._query(
`FOR doc IN @@collection ${filter_by_folder} FILTER doc.order <= @from and doc.order >= @to and doc._key != @key
UPDATE({ _key: doc._key, order: doc.order + 1 }) IN @@collection`,
_.merge({ "@collection": req.pathParams.service, from, to, key: doc._key }, folder_params)
)
}
collection.update(doc._key, { order: to })
res.send({ success: true });
})
.header('foxx-locale')
.header('X-Session-Id')
.description('Swap 2 items');
// Sub
// -----------------------------------------------------------------------------
router.get('/:service/:id/:subservice/:key/page/:page/:perpage', function (req, res) {
let includes = ''
let include_merge = ''
if(models()[req.pathParams.service].sub_models[req.pathParams.subservice].includes) {
includes = models()[req.pathParams.service].sub_models[req.pathParams.subservice].includes.conditions
include_merge = models()[req.pathParams.service].sub_models[req.pathParams.subservice].includes.merges
}
res.send({ data: db._query(`
LET count = LENGTH(@@collection)
LET data = (
FOR doc IN @@collection
FILTER doc.@key == @id
SORT doc._key DESC
${includes}
LIMIT @offset,@perpage
RETURN MERGE(doc, { ${include_merge} })
)
RETURN { count: count, data: data }
`, { "@collection": req.pathParams.subservice,
"offset": (req.pathParams.page - 1) * parseInt(req.pathParams.perpage),
"perpage": parseInt(req.pathParams.perpage),
"key": req.pathParams.key,
"id": req.pathParams.id }).toArray() });
})
.header('X-Session-Id')
.description('Returns all sub objects');
// -----------------------------------------------------------------------------
router.post('/sub/:service/:subservice', function (req, res) {
const collection = db._collection(req.pathParams.subservice)
const fields = models()[req.pathParams.service].sub_models[req.pathParams.subservice].fields
const body = JSON.parse(req.body.toString())
var obj = null
var errors = []
try {
var schema = {}
_.each(fields, function (f) {
schema[f.n] = _.isString(f.j) ? eval(f.j) : f.j
})
errors = joi.validate(body, schema, { abortEarly: false }).error.details
}
catch(e) {}
if(errors.length == 0) {
var data = fieldsToData(fields, body, req.headers)
if(models()[req.pathParams.service].sub_models[req.pathParams.subservice].timestamps === true) { data.created_at = +new Date() }
obj = collection.save(data, { waitForSync: true })
save_revision(req.session.uid, obj, data, 10)
}
res.send({ success: errors.length == 0, data: obj, errors: errors });
}).header('foxx-locale')
.header('X-Session-Id')
.description('Create a new sub object.');
// -----------------------------------------------------------------------------
router.post('/sub/:service/:subservice/:id', function (req, res) {
const collection = db._collection(req.pathParams.subservice)
const fields = models()[req.pathParams.service].sub_models[req.pathParams.subservice].fields
const body = JSON.parse(req.body.toString())
var obj = null
var errors = []
try {
var schema = {}
_.each(fields, function (f) {
let validate = f.ju ? f.ju : f.j
schema[f.n] = _.isString(validate) ? eval(validate) : validate
})
errors = joi.validate(body, schema, { abortEarly: false }).error.details
}
catch(e) {}
if(errors.length == 0) {
var object = collection.document(req.pathParams.id)
var data = fieldsToData(fields, body, req.headers)
if(models()[req.pathParams.service].sub_models[req.pathParams.subservice].timestamps === true) { data.updated_at = +new Date() }
obj = collection.update(object, data)
save_revision(req.session.uid, object, data, 10)
}
res.send({ success: errors.length == 0, data: obj, errors: errors });
})
.header('foxx-locale')
.header('X-Session-Id')
.description('Update a sub object.');
////////////////////////////////////////////////////////////////////////////////
// POST /cruds/:service/:id/publish
router.post('/:service/:id/publish', function (req, res) {
var data = db._collection(req.pathParams.service).document(req.pathParams.id)
db.publications.save(
{ _key: req.pathParams.service + '_' + data._key, data },
{ overwrite: true }
)
db._query(`
FOR data IN @@collection FILTER data.parent_id == @key
UPSERT { _id: @id }
INSERT { data: data }
UPDATE { data: data }
IN publications
`, {
'@collection': req.pathParams.service,
key: req.pathParams.id,
id: 'publications/' + req.pathParams.id
})
res.send({ success: true });
})
.header('X-Session-Id')
.description('publish an object.');
module.context.use('/folders', require('./routes/folders.js'), 'folders');