UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

366 lines (324 loc) 13.2 kB
const cwd = process.env._original_cwd || process.cwd() const cds = require('../index') /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ // eslint-disable-next-line no-unused-vars const _tarLib = () => { try { return require('tar') } catch(_) {} } exports = module.exports = new class { get colors() { return super.colors = require('./colors') } get inflect() { return super.inflect = require('./inflect') } get inspect() { const options = { depth: 11, colors: this.colors.enabled } const {inspect} = require('node:util') return super.inspect = v => inspect(v,options) } get format() { const {format} = require('node:util') return super.format = format } get uuid() { return super.uuid = require('crypto').randomUUID } get yaml() { const yaml = require('js-yaml'); return super.yaml = Object.assign(yaml,{parse:yaml.load}) } get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') } get semver() { return super.semver = require('./version') } } /** @type {import('node:path')} */ const path = exports.path = require('path'), { dirname, join, resolve, relative } = path /** @type {import('node:fs')} */ const fs = exports.fs = Object.assign (exports,require('fs')) //> for compatibility /** * Variant of `Object.keys()` which includes all keys inherited from the * given object's prototypes. */ exports.Object_keys = o => ({ [Symbol.iterator]: function*(){ for (let k in o) yield k }, forEach(f){ let i=0; for (let k in o) f(k,i++,o) }, filter(f){ let i=0, r=[]; for (let k in o) f(k,i++,o) && r.push(k); return r }, map(f){ let i=0, r=[]; for (let k in o) r.push(f(k,i++,o)); return r }, some(f){ for (let k in o) if (f(k)) return true }, find(f){ for (let k in o) if (f(k)) return k }, }) /** * Simple helper to always access results as arrays. */ exports.results = oa => { return Array.isArray(oa) ? oa : oa != null ? [oa] : [] } /** * Simple helper to deep-merge two or more objects. * Entries from `xs` overwrite entries in `o`. * @example cds.utils.merge({foo:1},{bar:2},{baz:3}) * @returns `o` with entries from `xs` merged in. */ exports.merge = function merge (o,...xs) { let v; for (let x of xs) for (let k in x) if (k === '__proto__' || k === 'constructor') continue //> avoid prototype pollution else o[k] = is_object(v=x[k]) ? merge(o[k]??={},v) : v return o } const is_object = x => typeof x === 'object' && x !== null && !is_array(x) const is_array = Array.isArray /** * Should be used in data providers, i.e., db services to return single * rows in response to SELECT.one queries. */ exports.chimera = oa => { return Array.isArray(oa) ? oa : Object.defineProperties(oa,chimera) } const chimera = Object.getOwnPropertyDescriptors (class Chimera { *[Symbol.iterator] (){ yield this } forEach(f){ f(this,0,this) } filter(f){ return f(this,0,this) ? [this] : [] } map(f){ return [f(this,0,this)] } some(f){ return f(this,0,this) } find(f){ if (f(this,0,this)) return this } }.prototype) exports.decodeURIComponent = s => { try { return decodeURIComponent(s) } catch { return s } } exports.decodeURI = s => { try { return decodeURI(s) } catch { return s } } /** * Computes a relative path from the a working directory to the given relative file, * considering a divergent 'outer' root path that is not `cds.root`. * Needed for `cds watch/run <dir>` calls. * @param {string} file - the relative file path to compute * @returns {string} - the relative path */ exports.local = (file) => file && relative(cwd, resolve(cds.root,file)) const { prepareStackTrace, stackTraceLimit } = Error /** * Use this utility to get a stack trace from the current position in the code. * For example, try this in your code, or in cds repl: * * cds.utils.stack(22) .forEach (each => console.log ( * each.getTypeName()||'<anonymous>', * each.getMethodName()||'—', * each.getFunctionName(), * '(' + cds.utils.local (each.getFileName()) * + ':' + each.getLineNumber() * + ':' + each.getColumnNumber() * + ')' * )) * * **WARNING:** This is an **expensive** function → handle with care! * @param {number} [depth] - the number of stack frames to return (default: 11) * @returns {NodeJS.CallSite[]} - an array of CallSite objects, as returned by [`Error.prepareStackTrace`](https://v8.dev/docs/stack-trace-api) */ exports.stack = (depth=11) => { Error.prepareStackTrace = (_,stack) => stack Error.stackTraceLimit = depth const stack = (new Error).stack Error.stackTraceLimit = stackTraceLimit Error.prepareStackTrace = prepareStackTrace return stack } /** * Use this utility to get the location of the caller in the code. * For example: * * let [file,line,col] = cds.utils.location() * * Basically a shortcut for `cds.utils.stack(3)[2]`, * with filename, line number, and column number returned in an array. * * **WARNING:** This is an **expensive** function → handle with care! * @returns {[ filename:string, line:number, column:number ]} */ exports.location = function() { const l = this.stack(3)[2] return [ l.getFileName(), l.getLineNumber(), l.getColumnNumber() ] } exports.exists = function(x) { if (!x) return const y = resolve (cds.root,x) return fs.existsSync(y) } exports.isdir = function isdir (...args) { if (args.length) try { const y = resolve (cds.root,...args) const ls = fs.lstatSync(y) if (ls.isDirectory()) return y if (ls.isSymbolicLink()) return isdir (join (dirname(y), fs.readlinkSync(y))) } catch {/* ignore */} } exports.isfile = function isfile (...args) { if (args.length) try { const y = resolve (cds.root,...args) const ls = fs.lstatSync(y) if (ls.isFile()) return y if (ls.isSymbolicLink()) return isfile (join (dirname(y), fs.readlinkSync(y))) } catch {/* ignore */} } exports.stat = async function (x) { const d = resolve (cds.root,x) return fs.promises.stat(d) } exports.readdir = async function (x) { const d = resolve (cds.root,x) return fs.promises.readdir(d) } exports.read = async function read (file, _encoding) { const f = resolve (cds.root,file) const src = await fs.promises.readFile (f, _encoding !== 'json' && _encoding || 'utf8') if (_encoding === 'json' || !_encoding && f.endsWith('.json')) try { return JSON.parse(src) } catch(e) { throw new Error (`Failed to parse JSON in ${f}: ${e.message}`) } else return process.platform === 'win32' ? src?.replace(/\r\n/g, '\n') : src } exports.write = function write (file, data, o) { if (arguments.length === 1) return {to:(...path) => write(join(...path),file)} if (typeof data === 'object' && !Buffer.isBuffer(data)) { let indent = o?.spaces || file.match(/(package|.cdsrc).json$/) && 2 data = JSON.stringify(data, null, indent) + '\n' } const f = resolve (cds.root,file) return exports.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o)) } exports.append = function append (file, data, o) { if (arguments.length === 1) return {to:(...path) => append(join(...path), data)} const f = resolve (cds.root,file) return exports.mkdirp (dirname(f)).then (()=> fs.promises.writeFile (f,data,o)) } exports.copy = function copy (x,y) { if (arguments.length === 1) return {to:(...path) => copy(x,join(...path))} const src = resolve (cds.root,x) const dst = resolve (cds.root,y) return fs.promises.cp (src,dst,{recursive:true}) } exports.mkdirp = async function (...path) { const d = resolve (cds.root,...path) await fs.promises.mkdir (d,{recursive:true}) return d } exports.rmdir = async function (...path) { const d = resolve (cds.root,...path) await fs.promises.rm (d, {recursive:true}) return d } exports.rimraf = async function (...path) { const d = resolve (cds.root,...path) await fs.promises.rm (d, {recursive:true,force:true}) return d } exports.rm = async function rm (x) { const y = resolve (cds.root,x) await fs.promises.rm(y) return y } exports.find = function find (base, patterns='*', filter=()=>true) { const files=[]; base = resolve (cds.root,base) if (typeof patterns === 'string') patterns = patterns.split(',') if (typeof filter === 'string') filter = this[filter] patterns.forEach (pattern => { const star = pattern.indexOf('*') if (star >= 0) { const head = pattern.slice(0,star).replace(/[^/\\]*$/,'') const dir = join (base,head) try { const ls = fs.lstatSync(dir) if (ls.isDirectory()) { const [,suffix,tail] = /([^/\\]*)?(?:.(.*))?/.exec (pattern.slice(star+1)) const prefix = pattern.slice(head.length,star) let entries = fs.readdirSync(dir) //.filter (_filter) if (prefix) entries = entries.filter (e => e.startsWith(prefix)); if (!entries.length) return if (suffix) entries = entries.filter (e => e.endsWith(suffix)); if (!entries.length) return let paths = entries.map (e=>join(dir,e)) if (filter) paths = paths.filter (filter); if (!paths.length) return if (tail) for (let _files of paths.map (e=>find (e,tail,filter))) files.push (..._files) else files.push (...paths) } } catch {/* ignore */} } else { const file = join (base, pattern) if (fs.existsSync(file)) files.push (file) } }) return files } exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) => { const yellow = '\x1b[33m' const reset = '\x1b[0m' // use cds.log in production for custom logger const {warn} = cds.env.production ? cds.log() : console if (typeof fn !== 'function') { if (cds.env.features.deprecated === 'off') return [kind,old,use] = [fn.kind || 'Configuration',fn.old,fn.use] warn ( yellow, '\n------------------------------------------------------------------------------', '\nDEPRECATED:', old, '\n', '\n ', (kind ? `${kind} ${old}` : old), 'is deprecated and will be removed in upcoming releases!', use ? `\n => Please use ${use} instead.` : '', '\n', '\n------------------------------------------------------------------------------\n', reset ) } else return function() { if (cds.env.features.deprecated !== 'off' && !fn.warned) { let o={}; Error.captureStackTrace(o) warn ( yellow, '\n------------------------------------------------------------------------------', '\nDEPRECATED:', old, '\n', '\n ', (kind ? `${kind} ${old}` : old), 'is deprecated and will be removed in upcoming releases!', use ? `\n => Please use ${use} instead.` : '', '\n', o.stack.replace(/^Error:?\s*at.*\n/m,'\n'), '\n', '\n------------------------------------------------------------------------------\n', reset ) if (cds.env.features.deprecated !== 'show all') fn.warned = true } return fn.apply (this, arguments) } } exports.csv = require('./csv-reader') /** * Loads a file through ESM or CommonJs. * @returns { Promise<any> } */ // TODO find a better place. exports._import = id => { try { return require(id) // try CommonJS first } catch (err) { if (err.code !== 'ERR_REQUIRE_ESM') throw err // else try w/ ESM const { pathToFileURL } = require('url') return import (pathToFileURL(id).href) // must use a file: URL, esp. on Windows for C:\... paths } } const SECRETS = /(passw)|(cert)|(ca)|(secret)|(key)/i /** * Masks password-like strings, also reducing clutter in output * @param {any} cred - object or array with credentials * @returns {any} */ exports.redacted = function _redacted(cred) { if (!cred) return cred if (Array.isArray(cred)) return cred.map(c => typeof c === 'string' ? '...' : _redacted(c)) if (typeof cred === 'object') { const newCred = Object.assign({}, cred) Object.keys(newCred).forEach(k => (typeof newCred[k] === 'string' && SECRETS.test(k)) ? (newCred[k] = '...') : (newCred[k] = _redacted(newCred[k]))) return newCred } return cred } /** * Converts a time span with a unit into milliseconds. @example * ms4(5,'s') //> 5000 * ms4('5s') //> 5000 * @param {number|string} ts - time span as number, or string with unit suffix, with or without spaces * @param {string} [unit] - time span unit * @returns {number} - time span in milliseconds */ const ms4 = exports.ms4 = (ts, unit, u=unit) => { if (typeof ts === 'string') [,ts,u] = /(\d+) ?(\w*)/.exec(ts) || cds.error `Invalid time span format: ${ts}` return ts * ms4[u||unit||'ms'] || cds.error `Invalid time span unit: ${unit} in ${ts}` } /** * Constants for time spans factors to milliseconds. @example * const { days, hours, minutes, second } = cds.utils.ms4 * 4 * days + 3 * hours + 2 * minutes + 1 * second //> 356521000 */ const ms = ms4.ms = 1 ms4.seconds = ms4.second = ms4.s = ms4.sec = 1000 *ms ms4.minutes = ms4.minute = ms4.m = ms4.min = 1000 *ms * 60 ms4.hours = ms4.hour = ms4.h = ms4.hrs = 1000 *ms * 60 * 60 ms4.days = ms4.day = ms4.d = 1000 *ms * 60 * 60 * 24 ms4.weeks = ms4.week = ms4.w = 1000 *ms * 60 * 60 * 24 * 7