@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
160 lines (134 loc) • 5.15 kB
JavaScript
const cds = require('../../lib')
const { INTERNAL_USER } = require('./consts')
// prettier-ignore
const _TO_COPY = ['inbound', 'event', 'data', 'flushOpts', 'headers', 'queue', 'results', 'method', 'path', 'params', 'entity', 'service']
// REVISIT: delete with old queue impl
const targetName = (name, opts) => (opts.targetPrefix ? opts.targetPrefix + name : name)
function _deterministic_task_id(service, task, optional_discriminator) {
return deterministic_uuid(`${service}:${task}${optional_discriminator ? `:${optional_discriminator}` : ''}`)
}
const create_task = (queueName, msg, context, serviceName, queueOpts, reoccurrence) => {
// REVISIT: use something like $cds for control data
const _msg = {
[INTERNAL_USER]: context.user.id,
service: serviceName
}
const _newContext = {}
for (const key in context) {
if (!queueOpts._ignoredContext.includes(key)) _newContext[key] = context[key]
}
_msg.context = _newContext
if (msg._fromSend || msg.reply) _msg._fromSend = true // send or emit?
for (const prop of _TO_COPY) if (msg[prop]) _msg[prop] = msg[prop]
if (msg.query) {
_msg.query = typeof msg.query.flat === 'function' ? msg.query.flat() : msg.query
delete _msg.query._target
delete _msg.query.__target
delete _msg.query.target
delete _msg.data // `req.data` should be a getter to whatever is in `req.query`
}
let timestamp
if (msg.queue?.every && cds.utils.cron(msg.queue.every)) {
// cron: derive the next absolute fire time, ignore .after
timestamp = next_execution_time_for_cron(msg.queue.every)
} else {
// needs to be different for each emit
timestamp = get_100ns_timestamp_iso_string(reoccurrence ? msg.queue?.every : msg.queue?.after)
}
const taskMsg = {
ID: cds.utils.uuid(),
appid: cds.env.appid,
target: cds.env.appid ? queueName : targetName(queueName, queueOpts),
timestamp,
msg: JSON.stringify(_msg)
}
if (msg.queue?.task) {
taskMsg.ID = _deterministic_task_id(serviceName, msg.queue.task, cds.env.appid ?? queueOpts.targetPrefix)
taskMsg.task = msg.queue.task
}
return taskMsg
}
const get_100ns_timestamp_iso_string = (offset = 0) => {
const now = new Date(Date.now() + offset)
const nanoseconds = Number(process.hrtime.bigint() % 1_000_000_000n)
return now.toISOString().replace('Z', `${nanoseconds}`.padStart(9, '0').substring(3, 7) + 'Z')
}
const is_provider_tenant = tenant => tenant && tenant === cds.requires.multitenancy?.t0
const crypto = require('crypto')
function deterministic_uuid(input) {
const hash = crypto.createHash('sha256').update(input).digest()
hash[6] = (hash[6] & 0x0f) | 0x40 // version 4
hash[8] = (hash[8] & 0x3f) | 0x80 // variant 1
const hex = hash.toString('hex')
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
}
function next_execution_time_for_cron(cronExpr, fromTime = new Date()) {
if (!cds.utils.cron(cronExpr)) throw new Error(`Invalid cron expression: '${cronExpr}'`)
const parse = (field, min, max) => {
const vals = new Set()
for (const part of field.split(',')) {
const [range, stepStr] = part.split('/')
const step = stepStr ? +stepStr : 1
const [lo, hi] =
range === '*'
? [min, max]
: range.includes('-')
? range.split('-').map(Number)
: [+range, stepStr ? max : +range]
for (let i = lo; i <= hi; i += step) vals.add(i)
}
return vals
}
const fields = cronExpr.trim().split(/\s+/)
const has_seconds = fields.length === 6
const [sF, mF, hF, domF, monF, dowF] = has_seconds ? fields : [null, ...fields]
const secs = has_seconds ? parse(sF, 0, 59) : null
const mins = parse(mF, 0, 59),
hrs = parse(hF, 0, 23),
doms = parse(domF, 1, 31),
mons = parse(monF, 1, 12)
const dow = new Set([...parse(dowF, 0, 7)].map(d => (d === 7 ? 0 : d)))
const next = new Date(fromTime)
if (has_seconds) {
next.setMilliseconds(0)
next.setSeconds(next.getSeconds() + 1)
} else {
next.setSeconds(0, 0)
next.setMinutes(next.getMinutes() + 1)
}
const limit = new Date(fromTime)
limit.setFullYear(limit.getFullYear() + 4)
while (next <= limit) {
if (!mons.has(next.getMonth() + 1)) {
next.setMonth(next.getMonth() + 1, 1)
next.setHours(0, 0, 0, 0)
continue
}
if (!doms.has(next.getDate()) || !dow.has(next.getDay())) {
next.setDate(next.getDate() + 1)
next.setHours(0, 0, 0, 0)
continue
}
if (!hrs.has(next.getHours())) {
next.setHours(next.getHours() + 1, 0, 0, 0)
continue
}
if (!mins.has(next.getMinutes())) {
next.setMinutes(next.getMinutes() + 1, 0, 0)
continue
}
if (secs && !secs.has(next.getSeconds())) {
next.setSeconds(next.getSeconds() + 1, 0)
continue
}
return next.toISOString()
}
throw new Error(`No matching time found for cron expression '${cronExpr}'`)
}
module.exports = {
targetName,
create_task,
is_provider_tenant,
deterministic_uuid,
next_execution_time_for_cron //> exported for test
}