@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
147 lines (128 loc) • 5.17 kB
JavaScript
const cds = require('../../index'), {inspect} = cds.utils
const express = require('express')
const LOG = cds.log('hcql')
const PROD = process.env.NODE_ENV === 'production'
class HCQLAdapter extends require('./http') {
get router() {
const router = super.router
.get ('/\\$csn', this.schema.bind(this)) //> return the CSN as schema
.use (express.json(this.body_parser_options)) //> for application/json -> cqn
.use (express.text(this.body_parser_options)) //> for text/plain -> cql -> cqn
.use ((req,res,next) => {
const q = typeof req.body === 'string' ? req.body : inspect(req.body, { depth: 4 })
LOG.info (req.method, decodeURI (req.baseUrl + req.path), q)
next()
})
// Route for custom actions and functions ...
const action = this.action.bind(this)
router.param('action', (r,_,next,a) => a in this.service.actions ? next() : next('route'))
router.route('/:action')
.post (action)
.get (action)
.all ((req,res,next) => next(501))
// Route for REST-style convenience shortcuts with queries in URL + body ...
const $ = cb => (req,_,next) => { req.body = cb(req.params,req); next() }
PROD || router.route(express.application.del ? '/:entity/:id?' : '/:entity{/:id}')
.get ($(({entity,id,tail}, req) => {
if (entity.includes(' ')) [,entity,tail] = /^(\w+)( .*)?/.exec(entity)
if (id?.includes(' ')) [,id,tail] = /^(\w+)( .*)/.exec(id)
let q = SELECT.from (entity,id), body = req.body
if (body) Object.assign (q.SELECT, ql_fragment(body))
if (tail) Object.assign (q.SELECT, ql_fragment(tail))
return q
}))
.post ($(({entity}, {query,body}) => INSERT.into (entity) .entries ({...query,...body})))
.put ($(({entity,id}, {query,body}) => UPDATE (entity,id) .with ({...query,...body})))
.patch ($(({entity,id}, {query,body}) => UPDATE (entity,id) .with ({...query,...body})))
.delete ($(({entity,id}) => DELETE.from (entity, id)))
// The ultimate handler for CRUD requests
router.use (this.crud.bind(this))
return router
}
log (req) { } // eslint-disable-line no-unused-vars
/**
* Handle requests to custom actions and functions.
*/
action (req, res, next) {
return this.service.send (req.params.action, { ...req.query, ...req.body })
.then (results => this.reply (results, res))
.catch (next)
}
/**
* The ultimate handler for all CRUD requests.
*/
crud (req, res, next) {
let query = this.query4 (req)
return this.service.run (query)
.then (results => this.reply (results, res))
.catch (next)
}
/**
* Constructs an instance of cds.ql.Query from an incoming request body,
* which is expected to be a plain CQN object or a CQL string.
*/
query4 (/** @type express.Request */ req) {
let q = req.body = cds.ql(req.body ?? {}) || this.error (400, 'Invalid query', { query: req.body })
// handle request headers
if (q.SELECT) {
if (req.get('Accept-Language')) q.SELECT.localized = true
if (req.get('X-Total-Count')) q.SELECT.count = true
// special handling for $search queries
const {where} = q.SELECT, $search = where?.[0]
if ($search?.func === '$search') {
q.SELECT.search = $search.args
where.splice(0, where[1] === '>' ? 4 : 2) // remove $search(...) > 0.1 and ...
if (where.length === 0) delete q.SELECT.where // remove empty where clause
}
}
// got a valid query
if (LOG._debug) LOG.debug (inspect(q))
return this.valid(q)
}
/**
* Checks whether the service actually serves the target entity.
*/
valid (query) {
if (!this.service.definition) return query
let target = cds.infer.target (query, this.service)
if (!target) throw this.error (400, 'Cannot determine target entity of query.')
if (target._unresolved) throw this.error (400, `${target.name} is not an entity served by '${this.service.name}'.`, { query })
return query
}
/**
* Serialize the results into response.
*/
reply (results, /** @type express.Response */ res, q = res.req.body) {
if (q.INSERT) res.statusCode = 201
if (results == null) return res.sendStatus(204)
if (results.$count) res.set ('X-Total-Count', results.$count)
if (typeof results === 'object') return res.json (results)
if (typeof results === 'number') results = String (results)
res.set('Content-Type','application/json').send (results)
}
/**
* Throw an Error with given status and message.
*/
error (status, message, details) {
if (typeof status === 'string') [ message, details, status ] = [ status, message ]
let err = Object.assign (new Error(message), details)
if (status) err.status = status
if (new.target) return err
else throw err
}
/**
* Return the CSN as schema in response to /<srv>/$csn requests
*/
schema (_, res) {
let csn = cds.minify (this.service.model, { service: this.service.name })
return res.json (csn)
}
}
const ql_fragment = x => {
if (x.length) {
x = SELECT (`from x ${x}`).SELECT
delete x.from
}
return x
}
module.exports = HCQLAdapter