@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
270 lines (234 loc) • 11.5 kB
JavaScript
const cds = require('../../index')
function _getBacklinkName(on) {
const i = on.findIndex(e => e.ref && e.ref[0] === '$self')
if (i === -1) return
let ref
if (on[i + 1] && on[i + 1] === '=') ref = on[i + 2].ref
if (on[i - 1] && on[i - 1] === '=') ref = on[i - 2].ref
return ref && ref[ref.length - 1]
}
function _isCompositionBacklink(e) {
if (!e.isAssociation) return
if (!e._target?.associations) return
if (!(!e.isComposition && (e.keys || e.on))) return
for (const anchor of Object.values(e._target.associations)) {
if (!(anchor.isComposition && anchor.on?.length > 2)) continue
if (_getBacklinkName(anchor.on) === e.name && anchor.target === e.parent.name) return anchor
}
}
// NOTE: Keep outside of the function to avoid calling the parser repeatedly
const { Draft } = cds.linked(`
entity ActiveEntity { key ID: UUID; }
entity Draft {
virtual IsActiveEntity : Boolean; // REVISIT: these are calculated fields, aren't they?
virtual HasDraftEntity : Boolean; // REVISIT: these are calculated fields, aren't they?
HasActiveEntity : Boolean; // This should be written !!!
DraftAdministrativeData : Association to DRAFT.DraftAdministrativeData;
DraftAdministrativeData_DraftUUID : UUID;
// SiblingEntity : Association to ActiveEntity; // REVISIT: Why didn't we use a managed assoc here?
}
entity DRAFT.DraftAdministrativeData {
key DraftUUID : UUID;
LastChangedByUser : String(256); LastChangeDateTime : Timestamp;
CreatedByUser : String(256); CreationDateTime : Timestamp;
InProcessByUser : String(256);
DraftIsCreatedByMe : Boolean; // REVISIT: these are calculated fields, aren't they?
DraftIsProcessedByMe : Boolean; // REVISIT: these are calculated fields, aren't they?
}
`).definitions
function DraftEntity4(active, name = active.name + '.drafts') {
// skip compositions with @odata.draft.enabled: false
const active_elements = {}
for (const each in active.elements) {
const element = active.elements[each]
if (element.isComposition && element['@odata.draft.enabled'] === false) {
// exclude, i.e., do nothing
} else {
active_elements[each] = element
}
}
const draft = Object.create(active, {
name: { value: name }, // REVISIT: lots of things break if we do that!
elements: { value: { ...active_elements, ...Draft.elements }, enumerable: true },
actives: { value: active },
query: { value: undefined }, // to not inherit that from active
// drafts: { value: undefined }, // to not inherit that from active -> doesn't work yet as the coding in lean-draft.js uses .drafts to identify both active and draft entities
isDraft: { value: true }
})
// for quoted names, we need to overwrite the cds.persistence.name of the derived, draft entity
const _pname = active['@cds.persistence.name']
if (_pname) draft['@cds.persistence.name'] = _pname + '_drafts'
return draft
}
function addNewActionAnnotation(def) {
// Skip if a new action was defined manually
if (def.own('@Common.DraftRoot.NewAction')) return
// Skip for non draft roots
if (!def.own('@Common.DraftRoot.ActivationAction')) return
// TODO: This is perhaps THE ugliest way to automatically add a 'draftNew' action:
// TODO: > Instead, this should happen in cds-compiler/lib/transfrom/draft/odata.js
// TODO: > Within generateDrafts -> generateDraftForOData
// TODO: > Unfortunately, the 'createAction' utility does not currently allow creating collection bound actions
def['@Common.DraftRoot.NewAction'] = `${def._service.name}.draftNew`
// TODO: Find a better way than this:
// TODO: > By rewriting `draftNew` into a `NEW` req in draftHandle, action input validation is skipped
// TODO: > This causes issues if the action has parameters derived from key fields that should be mandatory
// TODO: > This will bubble up a NOT NULL CONSTRAINT error instead of raising a proper client error
// TODO: > This behavior also occurs for regular custom actions
// Format a list of cds action parameters, based on the entities key fields
// > E.g.: [ 'dayKey: Integer', 'nameKey: String', ...]
// > UUID keys are skipped as they are generated
const idParameters = Object.values(def.keys)
.filter(el => el.key && !el.virtual && el._type !== 'cds.UUID') // TODO: Ignore @UI.Hidden keys?
.map(el => `${el.name}: ${el._type}`)
// Use cds.linked to create a valid action definition
const { draftNew } = cds.linked(`
service Service {
entity ActiveEntity { } actions {
action draftNew(in: many $self, ${idParameters.join(', ')}) returns ActiveEntity;
}
}
`).definitions['Service.ActiveEntity'].actions
draftNew.name = 'draftNew'
draftNew.returns = Object.create(def)
draftNew.returns.type = def.name
draftNew.parent = { name: def.name}
delete draftNew['$location']
def.actions['draftNew'] = draftNew
}
module.exports = function cds_compile_for_lean_drafts(csn) {
function _redirect(assoc, target) {
assoc.target = target.name
assoc._target = target
}
function _isDraft(def) {
// return 'DraftAdministrativeData' in def.elements
return (
def.associations?.DraftAdministrativeData ||
(def.own('@odata.draft.enabled') && def.own('@Common.DraftRoot.ActivationAction'))
)
}
function addDraftEntity(active, model) {
const _draftEntity = active.name + '.drafts'
const d = model.definitions[_draftEntity]
if (d) return d
// We need to construct a fake draft entity definition
// We cannot use new cds.entity because runtime aspects would be missing
const draft = new DraftEntity4(active, _draftEntity)
Object.defineProperty(model.definitions, _draftEntity, { value: draft })
Object.defineProperty(active, 'drafts', { value: draft })
// Positive list would be bigger (search, requires, fiori, ...)
if (draft['@readonly']) draft['@readonly'] = undefined
if (draft['@insertonly']) draft['@insertonly'] = undefined
if (draft['@restrict']) {
const restrictions = ['CREATE', 'WRITE', '*']
draft['@restrict'] = draft['@restrict']
.map(d => ({
...d,
grant:
d.grant && Array.isArray(d.grant)
? d.grant.filter(g => restrictions.includes(g))
: typeof d.grant === 'string' && restrictions.includes(d.grant)
? [d.grant]
: []
}))
.filter(r => r.grant.length > 0)
if (draft['@restrict'].length > 0) {
// Change WRITE & CREATE to NEW
draft['@restrict'] = draft['@restrict'].map(d => {
if (d.grant.includes('WRITE') || d.grant.includes('CREATE')) {
return { ...d, grant: 'NEW' }
}
return d
})
} else {
draft['@restrict'] = undefined
}
}
if ('@Capabilities.DeleteRestrictions.Deletable' in draft)
draft['@Capabilities.DeleteRestrictions.Deletable'] = undefined
if ('@Capabilities.InsertRestrictions.Insertable' in draft)
draft['@Capabilities.InsertRestrictions.Insertable'] = undefined
if ('@Capabilities.UpdateRestrictions.Updatable' in draft)
draft['@Capabilities.UpdateRestrictions.Updatable'] = undefined
if ('@Capabilities.NavigationRestrictions.RestrictedProperties' in draft)
draft['@Capabilities.NavigationRestrictions.RestrictedProperties'] = undefined
// Recursively add drafts for compositions
let _2manies
for (const each in draft.elements) {
const e = draft.elements[each]
// add @odata.draft.enclosed to filtered compositions
if (e.$enclosed) {
e['@odata.draft.enclosed'] = true
} else if (e.$filtered) { //> REVISIT: remove with cds^8
_2manies ??= Object.keys(draft.elements).map(k => draft.elements[k]).filter(c => c.isComposition && c.is2many)
if (_2manies.find(c => c.name !== e.name && c.target.replace(/\.drafts$/, '') === e.target)) e['@odata.draft.enclosed'] = true
}
const newEl = Object.create(e)
if (
e.isComposition ||
(e.isAssociation && e['@odata.draft.enclosed']) ||
((!active['@Common.DraftRoot.ActivationAction'] || e._target === active) && _isCompositionBacklink(e) && _isDraft(e._target))
) {
if (e._target['@odata.draft.enabled'] === false) continue // happens for texts if @fiori.draft.enabled is not set
_redirect(newEl, addDraftEntity(e._target, model))
}
if (e.name === 'DraftAdministrativeData') {
// redirect to DraftAdministrativeData service entity
if (active._service?.entities.DraftAdministrativeData) _redirect(newEl, active._service.entities.DraftAdministrativeData)
}
Object.defineProperty(newEl, 'parent', { value: draft, enumerable: false, configurable: true, writable: true })
for (const key in newEl) {
if (
key === '@mandatory' ||
(key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory') ||
// key === '@Core.Immutable': Not allowed via UI anyway -> okay to cleanse them in PATCH
// REVISIT: Remove feature flag dependency: If active, validation errors will be degraded to messages and stored in draft admin data
(!active._service?.entities.DraftAdministrativeData.elements.DraftMessages && key.startsWith('@assert')) ||
key.startsWith('@PersonalData')
)
newEl[key] = undefined
}
draft.elements[each] = newEl
}
// For list-report hierarchies, there must not be deep deletes w.r.t. recursive compositions
// Therefore, they are degraded to associations.
// Note: For object-page hiearchies, deep delete on draft recursive compositions is still needed.
if (draft.elements.LimitedDescendantCount && draft['@Common.DraftRoot.ActivationAction']) {
for (const c in draft.compositions) {
const comp = draft.compositions[c]
if (comp.target === draft.name) {
// modify comp to assoc
comp.type = 'cds.Association'
comp.isComposition = false
comp.is = function(kind) { return kind === 'Association' }
delete draft.compositions[c]
draft.associations[c] = comp
}
}
}
return draft
}
for (const name in csn.definitions) {
const def = csn.definitions[name]
// Do nothing for entities that are not draft-enabled
if (!_isDraft(def) || def['@cds.external']) continue
// Mark elements as virtual as required
def.elements.IsActiveEntity.virtual = true
def.elements.HasDraftEntity.virtual = true
def.elements.HasActiveEntity.virtual = true
def.elements.DraftAdministrativeData.virtual = true
if (def.elements.DraftAdministrativeData_DraftUUID) def.elements.DraftAdministrativeData_DraftUUID.virtual = true
// For Hierarchies: Exclude recursive compoisitions from draft tree
if (def.elements.LimitedDescendantCount) {
// for hierarchies: make sure recursive compositions are not part of the draft tree
for (const c in def.compositions) {
const comp = def.compositions[c]
if (comp.target === def.name) comp['@odata.draft.ignore'] = true
}
}
// will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
addDraftEntity(def, csn)
if (cds.env.fiori.direct_crud) addNewActionAnnotation(def)
}
}