UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

317 lines (282 loc) 10.5 kB
const cds = require('../../..') const { WELL_KNOWN_EVENTS } = require('../../req/event') const FLOW_STATUS = '@flow.status' const FROM = '@from' const TO = '@to' const FLOW_PREVIOUS = '$flow.previous' const getFrom = action => { let from = action[FROM] return Array.isArray(from) ? from : [from] } function addOperationAvailableToActions(actions, statusEnum, statusElementName) { action: for (const action of Object.values(actions)) { const fromList = getFrom(action) const conditions = [] for (const from of fromList) { const value = from['#'] ? statusEnum[from['#']]?.val ?? from['#'] : from if (typeof value !== 'string') { const msg = `Error while constructing @Core.OperationAvailable for action "${action.name}" of "${action.parent.name}". Value of @from must either be an enum symbol or a raw string.` cds.log('cds|edmx').warn(msg) continue action } conditions.push(`$self.${statusElementName} = '${value}'`) } const condition = `(${conditions.join(' OR ')})` const parsedXpr = cds.parse.expr(condition) action['@Core.OperationAvailable'] ??= { ...parsedXpr, ['=']: condition } } } function addSideEffectToActions(actions, statusElementName) { for (const action of Object.values(actions)) { const properties = [] if (statusElementName.endsWith('.code')) { const baseName = statusElementName.slice(0, -5) properties.push(`in/${statusElementName}`) properties.push(`in/${baseName}/*`) properties.push(`in/${baseName}_code`) } else { properties.push(`in/${statusElementName}`) } const sideEffect = '@Common.SideEffects.TargetProperties' if (action[sideEffect]) { action[sideEffect].push(...properties) } else { action[sideEffect] = properties } } } function addActionsToTarget(targetAnnotation, entity, actions) { const identification = (entity[targetAnnotation] ??= []) for (const item of identification) { if ( item.$Type === 'UI.DataFieldForAction' && !Object.hasOwn(item, '@UI.Hidden') && entity['@odata.draft.enabled'] === true ) { item['@UI.Hidden'] = { '=': true, xpr: [{ ref: ['$self', 'IsActiveEntity'] }, '=', { val: false }] } } } const existingActionNames = identification?.filter(item => item.$Type === 'UI.DataFieldForAction').map(item => item.Action.split('.').pop()) ?? [] actions.forEach(action => { const actionName = action.name if (!existingActionNames.includes(actionName)) { identification.push({ $Type: 'UI.DataFieldForAction', Action: `${entity._service.name}.${actionName}`, Label: action['@Common.Label'] ?? action['@title'] ?? `{i18n>${actionName}}`, ...(entity['@odata.draft.enabled'] && { '@UI.Hidden': { '=': true, xpr: [{ ref: ['$self', 'IsActiveEntity'] }, '=', { val: false }] } }) }) } }) } function resolveStatusEnum(csn, codeElem) { if (codeElem.enum !== undefined) return codeElem.enum if (codeElem.type) { const typeDef = csn.definitions[codeElem.type] return typeDef ? typeDef.enum : undefined } } function enhanceCSNwithFlowAnnotations4FE(csn) { for (const definition of Object.values(csn.definitions)) { if (definition.kind !== 'entity') continue const entity = definition if (!entity.elements || !entity.actions) continue for (const [elemName, element] of Object.entries(entity.elements)) { if (!element[FLOW_STATUS]) continue const fromActions = [] const toActions = [] for (const action of Object.values(entity.actions)) { if (action[FROM]) fromActions.push(action) if (action[TO]) toActions.push(action) } if (fromActions.length === 0 && toActions.length === 0) continue addActionsToTarget('@UI.Identification', entity, toActions) addActionsToTarget('@UI.LineItem', entity, toActions) if (element.enum) { // Element is an enum directly addSideEffectToActions(toActions, elemName) addOperationAvailableToActions(fromActions, element.enum, elemName) } else if (element.target) { // Element is an association to a codelist const targetDef = csn.definitions[element.target] if (targetDef?.elements?.code) { const codeElem = targetDef.elements.code const statusEnum = resolveStatusEnum(csn, codeElem) if (statusEnum) { // REVISIT: is there no way to know from the CSN? const statusElementName = csn._4java ? elemName + '.code' : elemName + '_code' addSideEffectToActions(toActions, statusElementName) addOperationAvailableToActions(fromActions, statusEnum, statusElementName) } } } else if (element['@odata.foreignKey4']) { // when compiling to edmx, the foreign key is also annotated with @flow.status, but has no info about the target continue } else { cds.error( `Status element in entity ${entity.name} is not an enum and does not have a valid target with code enum.` ) } } } } module.exports = function cds_compile_for_flows(csn) { const { history_for_flows } = cds.env.features const _requires_history = !history_for_flows ? () => false : history_for_flows === 'all' ? def => { for (const each in def.elements) { if (def.elements[each]['@flow.status']) { return true } } } : def => { for (const each in def.actions) { const action = def.actions[each] if (action && action[TO]?.['='] === FLOW_PREVIOUS) { return true } } } /* * 1. propagate flows for well-known actions from extensions to definitions */ if (csn.extensions) { for (const ext of csn.extensions) { if (!ext.actions) continue const def = csn.definitions[ext.annotate] if (!def || !def.kind || def.kind !== 'entity') continue for (const each in ext.actions) { if (!(each in WELL_KNOWN_EVENTS)) continue def.actions ??= {} def.actions[each] ??= { kind: 'action' } Object.assign(def.actions[each], ext.actions[each]) } } } const to_be_extended = new Set() for (const name in csn.definitions) { const def = csn.definitions[name] /* * 2. propagate @flow.status to respective element and make it @readonly */ if (def['@flow.status']?.['=']) { const element = def.elements?.[def['@flow.status']['=']] if (element) { element['@flow.status'] = true if (!('@readonly' in element)) element['@readonly'] = true } } if (!def.kind || def.kind !== 'entity' || !def.actions) continue /* * 3. normalize @from and @to annotations */ for (const each in def.actions) { const action = def.actions[each] if (action['@flow.from']) action['@from'] = action['@flow.from'] if (action['@flow.to']) action['@to'] = action['@flow.to'] } /* * 4. automatically apply aspect FlowHistory if needed and not present yet */ if (!_requires_history(def)) continue const projections = _get_projection_stack(name, csn) const base_name = projections.pop() const base = csn.definitions[base_name] if (base.elements?.transitions_) continue //> manually added -> don't interfere // add aspect FlowHistory to db entity to_be_extended.add(base_name) } if (to_be_extended.size) { // REVISIT: ensure sap.common.FlowHistory is there csn.definitions['sap.common.FlowHistory'] ??= JSON.parse(FlowHistory) const extensions = [...to_be_extended].map(extend => ({ extend, includes: ['sap.common.FlowHistory'] })) const dsn = cds.extend(csn).with({ extensions }) // REVISIT: annotate all generated X.transitions_ with @cds.autoexpose: false for (const each of to_be_extended) dsn.definitions[`${each}.transitions_`]['@cds.autoexpose'] = false // hack for "excludes" not possible via extensions for (const name in dsn.definitions) { const _new = dsn.definitions[name] if ( _new.kind !== 'entity' || to_be_extended.has(name.replace(/\.transitions_$/, '')) || (!name.match(/\.transitions_$/) && !_new.elements.transitions_) ) { continue } const _old = csn.definitions[name] if (!_old) delete dsn.definitions[name] else if (_new.elements.transitions_ && !_old.elements.transitions_) delete _new.elements.transitions_ } csn = dsn } // REVISIT: annotate all X.transitions_ with @odata.draft.enabled: false for (const name in csn.definitions) if (name.endsWith('.transitions_')) csn.definitions[name]['@odata.draft.enabled'] = false return csn } function _get_projection_stack(name, csn, stack = []) { stack.push(name) const def = csn.definitions[name] if (def.projection || def.query) { const base = (def.projection || def.query.SELECT)?.from?.ref?.[0] if (!base) throw new Error(`Unable to determine base entity of ${name}`) return _get_projection_stack(base, csn, stack) } return stack } const FlowHistory = `{ "kind": "aspect", "@cds.persistence.skip": "if-unused", "elements": { "transitions_": { "@odata.draft.enabled": false, "type": "cds.Composition", "cardinality": { "max": "*" }, "targetAspect": { "elements": { "timestamp": { "@cds.on.insert": { "=": "$now" }, "@UI.HiddenFilter": true, "@UI.ExcludeFromNavigationContext": true, "@Core.Immutable": true, "@title": "{i18n>CreatedAt}", "@readonly": true, "key": true, "type": "cds.Timestamp" }, "user": { "@cds.on.insert": { "=": "$user" }, "@UI.HiddenFilter": true, "@UI.ExcludeFromNavigationContext": true, "@Core.Immutable": true, "@title": "{i18n>CreatedBy}", "@readonly": true, "@description": "{i18n>UserID.Description}", "type": "cds.String", "length": 255 }, "status": { "type": "cds.String" }, "comment": { "type": "cds.String" } } } } } }` module.exports.enhanceCSNwithFlowAnnotations4FE = enhanceCSNwithFlowAnnotations4FE module.exports.getFrom = getFrom