UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

147 lines (128 loc) 5.17 kB
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