UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

171 lines (146 loc) 6.28 kB
const { pipeline } = require('node:stream/promises') const cds = require('../../index'), { inspect, decodeURI } = cds.utils const express = require('express') const SELECT = require('../../ql/SELECT') const PROD = process.env.NODE_ENV === 'production' const LOG = cds.log ('hcql') class HCQLAdapter extends require('./http') { get router() { const bpo = this.body_parser_options const router = super.router .get ('/\\$csn', this.schema.bind(this)) .use (express.json(bpo)) //> for application/json -> cqn .use (express.text(bpo)) //> for text/plain -> cql -> cqn .use (this.log_request) PROD || router.use (['/:entity/:id','/:entity'], this.shortcuts.bind(this)) router.use ('/:event', this.action.bind(this)) router.use (this.query.bind(this)) return router } /** * Handler for logging incoming requests. */ log_request (req,_,next) { LOG.info (req.method, decodeURI (req.baseUrl + req.path), typeof req.body === 'string' ? req.body : inspect (req.body, { depth: 4 }) ); next() } /** * Return the CSN as schema in response to /<srv>/$csn requests * @type {express.RequestHandler} */ schema (_, res) { let csn = cds.minify (this.service.model, { service: this.service.name }) return res.json (csn) } /** * Handler for REST-style convenience shortcuts like GET /Books/201 * @type {express.RequestHandler} */ shortcuts (req, res, next) { let { entity, id } = req.params, tail, body = req.body if (entity in this.service.actions) return this.action (req, res, next) switch (req.method) { case 'GET': { if (entity.includes(' ')) [,entity,tail] = /^(\w+)( .*)?/.exec(entity) else if (id?.includes(' ')) [,id,tail] = /^(\w+)( .*)/.exec(id) else if (typeof body === 'string') [tail,body] = [body,undefined] req.body = tail?.length ? id ? SELECT (`from ${entity} [ID=${id}] ${tail}`) : SELECT (`from ${entity} ${tail}`) : SELECT.from (entity, id) if (typeof body === 'object') Object.assign (req.body.SELECT, body) if (typeof body === 'string') { let { from, ...clauses } = SELECT(`from x ${body}`).SELECT // eslint-disable-line no-unused-vars Object.assign (req.body.SELECT, clauses) } break } case 'POST': req.body = INSERT.into (entity) .entries ({...req.query,...req.body}); break case 'PUT': req.body = UPSERT.into (entity) .entries ({...req.query,...req.body}); break case 'PATCH': req.body = UPDATE (entity, id) .with ({...req.query,...req.body}); break case 'DELETE': req.body = DELETE.from (entity, id); break } return next() } /** * Handler for custom actions and functions. * @type {express.RequestHandler} */ action (req, res, next) { const { event } = req.params if (!(event in this.service.actions)) return next() //> CRUD /:entity/:id if (!(req.method in {GET:1,POST:2})) return next(501) //> only GET and POST allowed const data = { ...req.query, ...req.body } return this.exec ({ event, data }, req) .catch (next) } /** * Handler for all CRUD requests. * @type {express.RequestHandler} */ query (req, res, next) { // Obtain query from request body, which may be a CQN object or a CQL string const q = cds.ql (req.body) || cds.error (400, 'Invalid query', { query: req.body }) // Validate target entity against given model if (this.service.definition) { const t = cds.infer.target (q, this.service) if (!t || t._unresolved) throw cds.error (400, 'Cannot determine target entity of query.', { query:q }) } // Apply request headers const $ = q.SELECT if ($) { if (req.get('Accept-Language')) $.localized = true if (req.get('X-Total-Count')) $.count = true } // Turn SELECT where $search(...) predicates into SELECT.search = [...] as expected for CQNs if ($?.where?.[0].func === '$search') { $.search = $.where[0].args if ($.where.length === 1) delete $.where else $.where.splice (0, $.where[1] === '>' ? 4 : 2) } // Deviate to streaming if query has .stream and client accepts octet-stream if (q.stream && req.headers.accept?.includes ('application/octet-stream')) // return this.service.read(q) .then (stream => { return this.service.run (()=> q.bind (this.service) .stream() .then (stream => { res.set ('content-type', 'application/octet-stream') return pipeline (stream, res) })) .catch (next) // Regular handling for non-streaming queries return this.exec (q,req) .catch (next) } /** * Central method to dispatch requests to the service and replying outcomes back to the client. * @param {cds.ql.Query} query the query to be executed * @param {express.Request} req the HTTP request object * @param {express.Response} res the HTTP response object * @returns {Promise<any>} results from service dispatch, which will be sent as JSON response body */ async exec (query, req, res = req.res) { const request = new cds.Request (query.event ? { ...query, req, res } : { query, req, res }) const data = await this.service.dispatch (request) if (res.headersSent) return if (data == null && !request.messages) return res.sendStatus(204) if (request.event === 'CREATE') res.status(201) if (data.$count !== undefined) res.set ('X-Total-Count', data.$count) res.json (this.response4 ({ data, messages: request.messages })) return data } /** * Overrides base method to format responses according to HCQL expectations, * i.e. wrapping results in a `data` property and normalizing errors into * an `errors` array. */ response4 (outcomes) { let { data, error, errors, messages } = super.response4 (outcomes) let response = {} if (data != null) { response.data = data if (data.affected !== undefined) response.affected = data.affected if (data.$count !== undefined) response.$count = data.$count } if (error) errors ??= error.details || [ error ] if (errors) response.errors = errors if (messages) response.messages = messages return response } } module.exports = HCQLAdapter