UNPKG

@cap-js/hana

Version:

CDS database service for SAP HANA

219 lines (193 loc) 7.57 kB
'use strict' const cds = require('@sap/cds') const isTime = /^\d{1,2}:\d{1,2}:\d{1,2}$/ const isDate = /^\d{1,4}-\d{1,2}-\d{1,2}$/ const isVal = x => x && 'val' in x const getTimeType = x => (isTime.test(x.val) ? 'TIME' : 'TIMESTAMP') const getTimeCast = x => (isVal(x) ? `TO_${getTimeType(x)}(${x})` : x) const getDateType = x => (isDate.test(x.val) ? 'DATE' : 'TIMESTAMP') const getDateCast = x => (isVal(x) ? `TO_${getDateType(x)}(${x})` : x) const StandardFunctions = { // ============================== // String Functions // ============================== /** * Generates SQL statement that produces the index of the first occurrence of the second string in the first string * @param {string} x - The string to search * @param {string} y - The substring to find * @returns {string} - SQL statement */ indexof: (x, y) => `locate(${x},${y}) - 1`, // locate is 1 indexed /** * Generates SQL statement that produces a boolean value indicating whether the first string starts with the second string * @param {string} x - The string to evaluate * @param {string} y - The prefix to check * @returns {string} - SQL statement */ startswith: (x, y) => `(CASE WHEN locate(${x},${y}) = 1 THEN TRUE ELSE FALSE END)`, /** * Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string * @param {string} x - The string to evaluate * @param {string} y - The suffix to check * @returns {string} - SQL statement */ endswith: (x, y) => `(CASE WHEN substring(${x},length(${x})+1 - length(${y})) = ${y} THEN TRUE ELSE FALSE END)`, /** * Generates SQL statement that matches the given string against a regular expression * @param {string} x - The string to match * @param {string} y - The regular expression * @returns {string} - SQL statement */ matchesPattern: (x, y) => `(CASE WHEN ${x} LIKE_REGEXPR ${y} THEN TRUE ELSE FALSE END)`, /** * Alias for matchesPattern * @param {string} x - The string to match * @param {string} y - The regular expression * @returns {string} - SQL statement */ matchespattern: (x, y) => `(CASE WHEN ${x} LIKE_REGEXPR ${y} THEN TRUE ELSE FALSE END)`, /** * Generates SQL statement that checks if the first string contains the second string * @param {...string} args - The strings to evaluate * @returns {string} - SQL statement */ contains: (...args) => args.length > 2 ? `CONTAINS(${args})` : `(CASE WHEN coalesce(locate(${args}),0)>0 THEN TRUE ELSE FALSE END)`, // ============================== // Search Function // ============================== /** * Generates SQL statement for search functionality * @param {string} ref - Reference object containing columns * @param {string} arg - Argument object containing search values * @returns {string} - SQL statement */ search: function (ref, arg) { if (cds.env.hana.fuzzy === false) { // Handle non-fuzzy search arg = arg.xpr ? arg.xpr : arg if (Array.isArray(arg)) { arg = [ { val: arg .filter(a => a.val) .map(a => a.val) .join(' '), }, ] } else arg = [arg] const searchTerms = arg[0].val .match(/("")|("(?:[^"]|\\")*(?:[^\\]|\\\\)")|(\S*)/g) .filter(el => el.length) .map(el => { try { return `%${JSON.parse(el).toLowerCase()}%` } catch { return `%${el.toLowerCase()}%` } }) const columns = ref.list const xpr = [] for (const s of searchTerms) { const nestedXpr = [] for (const c of columns) { if (nestedXpr.length) nestedXpr.push('or') nestedXpr.push({ func: 'lower', args: [c] }, 'like', { val: s }) } if (xpr.length) xpr.push('and') xpr.push({ xpr: nestedXpr }) } const { toString } = ref return `(CASE WHEN (${toString({ xpr })}) THEN TRUE ELSE FALSE END)` } // fuzziness config const fuzzyIndex = cds.env.hana?.fuzzy || 0.7 const csnElements = ref.list // if column specific value is provided, the configuration has to be defined on column level if (csnElements.some(e => e.element?.['@Search.ranking'] || e.element?.['@Search.fuzzinessThreshold'])) { csnElements.forEach(e => { let fuzzy = `FUZZY` // weighted search const rank = e.element?.['@Search.ranking']?.['='] switch (rank) { case 'HIGH': fuzzy += ' WEIGHT 0.8' break case 'LOW': fuzzy += ' WEIGHT 0.3' break case 'MEDIUM': case undefined: fuzzy += ' WEIGHT 0.5' break default: throw new Error( `Invalid configuration ${rank} for @Search.ranking. HIGH, MEDIUM, LOW are supported values.`, ) } fuzzy += ` MINIMAL TOKEN SCORE ${e.element?.['@Search.fuzzinessThreshold'] || fuzzyIndex} SIMILARITY CALCULATION MODE 'search'` // rewrite ref to xpr to mix in search config // ensure in place modification to reuse .toString method that ensures quoting e.xpr = [{ ref: e.ref }, fuzzy] delete e.ref }) } else { ref = `${ref} FUZZY MINIMAL TOKEN SCORE ${fuzzyIndex} SIMILARITY CALCULATION MODE 'search'` } if (Array.isArray(arg.xpr)) { arg = { val: arg.xpr .filter(a => a.val) .map(a => a.val) .join(' '), } } return `(CASE WHEN SCORE(${arg} IN ${ref}) > 0 THEN TRUE ELSE FALSE END)` }, // ============================== // Arithmetic Functions // ============================== /** * Generates SQL statement that produces the rounded value of a given number * @param {string} x - The number input * @param {string} [p] - Precision * @param {string} [r] - Rounding mode (for compatibility with native HANA function) * <rounding_mode> ::= ROUND_HALF_UP | ROUND_HALF_DOWN | ROUND_HALF_EVEN | ROUND_UP | ROUND_DOWN | ROUND_CEILING | ROUND_FLOOR * @returns {string} - SQL statement */ round: (x, p, r) => { if (p) { if (r) { // REVISIT: r is a literal string, should be passed as is and not as param // e.g. ROUND(1.2345, 2, ROUND_HALF_UP) return `ROUND(${x}, ${p}, ${r})` } return `ROUND(${x}, ${p})` } return `ROUND(${x})` }, // ============================== // Date and Time Functions // ============================== year: x => `YEAR(${getDateCast(x)})`, month: x => `MONTH(${getDateCast(x)})`, day: x => `DAYOFMONTH(${getDateCast(x)})`, hour: x => `HOUR(${getTimeCast(x)})`, minute: x => `MINUTE(${getTimeCast(x)})`, second: x => `TO_INTEGER(SECOND(${getTimeCast(x)}))`, date: x => `TO_DATE(${x})`, time: x => `TO_TIME(${x})`, now: () => `session_context('$now')`, fractionalseconds: x => `(TO_DECIMAL(SECOND(${x}),5,3) - TO_INTEGER(SECOND(${x})))`, } const HANAFunctions = { current_date: () => 'current_utcdate', current_time: () => 'current_utctime', current_timestamp: () => 'current_utctimestamp', current_utctimestamp: x => (x ? `current_utctimestamp(${x})` : 'current_utctimestamp'), HIERARCHY: undefined, HIERARCHY_DESCENDANTS: undefined, HIERARCHY_ANCESTORS: undefined, } for (let each in HANAFunctions) HANAFunctions[each.toUpperCase()] = HANAFunctions[each] module.exports = { ...StandardFunctions, ...HANAFunctions }