UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

270 lines (234 loc) 11.5 kB
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) } }