UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

160 lines (134 loc) 5.15 kB
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 }