@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,336 lines (1,251 loc) • 61.9 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
//Provides mixin sap.ui.model.odata.v4.ODataBinding for classes extending sap.ui.model.Binding
sap.ui.define([
"./lib/_Helper",
"sap/ui/base/SyncPromise",
"sap/ui/model/ChangeReason",
"sap/ui/model/odata/OperationMode",
"sap/ui/model/odata/v4/Context"
], function (_Helper, SyncPromise, ChangeReason, OperationMode, Context) {
"use strict";
var aChangeReasonPrecedence = [ChangeReason.Context, ChangeReason.Change, ChangeReason.Refresh,
ChangeReason.Sort, ChangeReason.Filter],
// Whether a path segment is an index or contains a transient predicate
rIndexOrTransientPredicate = /\/\d|\(\$uid=/;
/*
* Tells whether the first given change reason has precedence over the second one.
*
* @param {string} sChangeReason0 - A change reason
* @param {string} sChangeReason1 - A change reason
* @returns {boolean} Whether the first given change reason has precedence over the second one
*/
function hasPrecedenceOver(sChangeReason0, sChangeReason1) {
return aChangeReasonPrecedence.indexOf(sChangeReason0)
> aChangeReasonPrecedence.indexOf(sChangeReason1);
}
/**
* A mixin for all OData V4 bindings.
*
* @alias sap.ui.model.odata.v4.ODataBinding
* @mixin
*/
function ODataBinding() {
// maps a canonical path of a quasi-absolute or relative binding to a cache object that may
// be reused
this.mCacheByResourcePath = undefined;
// the current cache of this binding as delivered by oCachePromise
// undefined: unknown whether the binding has an own cache or not
// null: binding does not have an own cache
this.oCache = null;
this.oCachePromise = SyncPromise.resolve(null);
this.mCacheQueryOptions = undefined;
this.fnDeregisterChangeListener = undefined;
// used to create cache only for the latest call to #fetchCache
this.oFetchCacheCallToken = undefined;
// the absolute binding path (possibly reduced if the binding uses a parent binding's cache)
// may be incorrect while cache creation is pending (this.oCache === undefined)
this.sReducedPath = undefined;
// change reason to be used when the binding is resumed
this.sResumeChangeReason = undefined;
}
/**
* Returns <code>true</code> if this binding or its dependent bindings have changes.
*
* Note: This private function is needed in order to hide the additional parameter
* <code>sPathPrefix</code> from the public API {@link #hasPendingChanges}.
*
* @param {boolean} [bIgnoreKeptAlive]
* Whether to ignore changes which will not be lost by certain APIs, see
* {@link #hasPendingChanges}
* @param {boolean} [sPathPrefix]
* If supplied, only caches having a resource path starting with <code>sPathPrefix</code> are
* checked
* @returns {boolean}
* <code>true</code> if the binding is resolved and has pending changes
*
* @private
*/
ODataBinding.prototype._hasPendingChanges = function (bIgnoreKeptAlive, sPathPrefix) {
return this.isResolved()
&& (this.hasPendingChangesForPath("", bIgnoreKeptAlive)
|| this.hasPendingChangesInDependents(bIgnoreKeptAlive, sPathPrefix));
};
/**
* Resets all pending changes of this binding, see {@link #hasPendingChanges}. Resets also
* invalid user input.
*
* Note: This private function is needed in order to hide the additional parameter
* <code>sPathPrefix</code> from the public API {@link #resetChanges}.
*
* @param {boolean} [sPathPrefix]
* If supplied, only caches having a resource path starting with <code>sPathPrefix</code> are
* reset
* @returns {Promise<void>}
* A promise which is resolved without a defined result as soon as all changes in the binding
* itself and all dependent bindings are canceled
* @throws {Error}
* If the binding's root binding is suspended or if there is a change of this binding which
* has been sent to the server and for which there is no response yet
*
* @private
*/
ODataBinding.prototype._resetChanges = function (sPathPrefix) {
var aPromises = [];
this.checkSuspended();
this.resetChangesForPath("", aPromises);
this.resetChangesInDependents(aPromises, sPathPrefix);
this.resetInvalidDataState();
return Promise.all(aPromises).then(function () {});
};
/**
* Adjusts the paths of all contexts of this binding by replacing the given transient predicate
* with the given predicate. Recursively adjusts all child bindings.
*
* @param {string} sTransientPredicate - The transient predicate to be replaced
* @param {string} sPredicate - The new predicate
*
* @private
*/
ODataBinding.prototype.adjustPredicate = function (sTransientPredicate, sPredicate) {
this.sReducedPath = this.sReducedPath.replace(sTransientPredicate, sPredicate);
};
/**
* Checks binding-specific parameters from the given map. "Binding-specific" parameters are
* those with a key starting with '$$', i.e. OData query options provided as binding parameters
* are ignored. The following parameters are supported, if the parameter name is contained in
* the given 'aAllowed' parameter:
* <ul>
* <li> '$$aggregation' with allowed values as specified in
* {@link sap.ui.model.odata.v4.ODataListBinding#updateAnalyticalInfo} (but without
* validation here)
* <li> '$$canonicalPath' with value <code>true</code>
* <li> '$$clearSelectionOnFilter' with value <code>true</code>
* <li> '$$groupId' with allowed values as specified in {@link #checkGroupId}
* <li> '$$inheritExpandSelect' with allowed values <code>false</code> and <code>true</code>
* <li> '$$noPatch' with value <code>true</code>
* <li> '$$operationMode' with value {@link sap.ui.model.odata.OperationMode.Server}
* <li> '$$ownRequest' with value <code>true</code>
* <li> '$$patchWithoutSideEffects' with value <code>true</code>
* <li> '$$updateGroupId' with allowed values as specified in {@link #checkGroupId}
* <li> '$$separate' with value <code>string[]</code>
* </ul>
*
* @param {object} mParameters
* The map of binding parameters
* @param {string[]} aAllowed
* The array of allowed binding parameter names
* @throws {Error}
* For unsupported parameter names or parameter values
*
* @private
*/
ODataBinding.prototype.checkBindingParameters = function (mParameters, aAllowed) {
var that = this;
Object.keys(mParameters).forEach(function (sKey) {
var vValue = mParameters[sKey];
if (!sKey.startsWith("$$")) {
return;
}
if (!aAllowed.includes(sKey)) {
throw new Error("Unsupported binding parameter: " + sKey);
}
switch (sKey) {
case "$$aggregation":
// no validation here
break;
case "$$groupId":
case "$$updateGroupId":
_Helper.checkGroupId(vValue, false,
"Unsupported value for binding parameter '" + sKey + "': ");
break;
case "$$ignoreMessages":
case "$$sharedRequest":
if (vValue !== true && vValue !== false) {
throw new Error("Unsupported value for binding parameter '" + sKey + "': "
+ vValue);
}
break;
case "$$inheritExpandSelect":
if (vValue !== true && vValue !== false) {
throw new Error("Unsupported value for binding parameter "
+ "'$$inheritExpandSelect': " + vValue);
}
if (!that.oOperation) {
throw new Error("Unsupported binding parameter $$inheritExpandSelect: "
+ "binding is not an operation binding");
}
if (mParameters.$expand) {
throw new Error("Must not set parameter $$inheritExpandSelect on a binding "
+ "which has a $expand binding parameter");
}
break;
case "$$operationMode":
if (vValue !== OperationMode.Server) {
throw new Error("Unsupported operation mode: " + vValue);
}
break;
case "$$getKeepAliveContext":
if (that.isRelative() && !mParameters.$$ownRequest) {
throw new Error(
"$$getKeepAliveContext requires $$ownRequest in a relative binding");
}
["$$aggregation", "$$canonicalPath", "$$sharedRequest"]
.forEach(function (sForbidden, i) {
if (sForbidden in mParameters
&& (i > 0 || _Helper.isDataAggregation(mParameters))) {
throw new Error("Cannot combine $$getKeepAliveContext and "
+ sForbidden);
}
});
// fall through
case "$$canonicalPath":
case "$$clearSelectionOnFilter":
case "$$noPatch":
case "$$ownRequest":
case "$$patchWithoutSideEffects":
if (vValue !== true) {
throw new Error("Unsupported value for binding parameter '" + sKey + "': "
+ vValue);
}
break;
case "$$separate":
if (mParameters.$$aggregation) {
throw new Error("Cannot combine $$aggregation and $$separate");
}
break;
default:
throw new Error("Unknown binding-specific parameter: " + sKey);
}
});
};
/**
* Throws an error that the response is being ignored if the current cache is not the expected
* one. The error has the property <code>canceled : true</code>
*
* @param {sap.ui.model.odata.v4.lib._Cache} oExpectedCache - The expected cache
* @param {number} [iResetCount] - The cache's expected reset count
* @throws {Error} If the current cache is not the expected one
*
* @private
*/
ODataBinding.prototype.checkSameCache = function (oExpectedCache, iResetCount) {
var oError;
if (this.oCache !== oExpectedCache
|| iResetCount !== undefined && this.oCache.iResetCount !== iResetCount) {
oError = new Error(this + " is ignoring response from inactive cache: "
+ oExpectedCache);
oError.canceled = true;
throw oError;
}
};
/**
* Throws an Error if the binding's root binding is suspended.
*
* @param {boolean} [bIfNoResumeChangeReason]
* Whether to accept a suspended root binding as long as no <code>sResumeChangeReason</code>
* is known for this binding (which must not be a root itself) or any of its dependents
* @throws {Error} If the binding's root binding is suspended, except if
* <code>bIfNoResumeChangeReason</code> is used as described
*
* @private
*/
ODataBinding.prototype.checkSuspended = function (bIfNoResumeChangeReason) {
if (this.isRootBindingSuspended()
&& (!bIfNoResumeChangeReason || this.isRoot() || this.getResumeChangeReason())) {
throw new Error("Must not call method when the binding's root binding is suspended: "
+ this);
}
};
/**
* Throws an Error if the binding is {@link #isTransient transient}.
*
* @throws {Error} If the binding is transient
*
* @private
*/
ODataBinding.prototype.checkTransient = function () {
if (this.isTransient()) {
throw new Error("Must not call method when the binding is part of a deep create: "
+ this);
}
};
/**
* Calls {@link #checkUpdateInternal}.
*
* @param {boolean} [bForceUpdate]
* Whether the change event is fired in any case
* @throws {Error}
* If there are unexpected parameters
*
* @private
*/
// @override sap.ui.model.Binding#checkUpdate
ODataBinding.prototype.checkUpdate = function (bForceUpdate) {
if (arguments.length > 1) {
throw new Error("Only the parameter bForceUpdate is supported");
}
this.checkUpdateInternal(bForceUpdate).catch(this.oModel.getReporter());
// do not rethrow, ManagedObject doesn't react on this either
// throwing an error would cause "Uncaught (in promise)" in Chrome
};
/**
* A property binding re-fetches its value and fires a change event if the value has changed. A
* parent binding checks dependent bindings for updates or refreshes the binding if the resource
* path of its parent context changed.
*
* @param {boolean} [bForceUpdate]
* Whether the change event is fired in any case (only allowed for property bindings)
* @returns {sap.ui.base.SyncPromise<void>}
* A promise which is resolved without a defined result when the check is finished, or
* rejected in case of an error
* @throws {Error} If called with illegal parameters
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#checkUpdateInternal
* @private
* @see #checkUpdate
* @see sap.ui.model.Binding#checkUpdate
*/
/**
* Creates and sets the cache, handles mCacheByResourcePath and adds some cache-relevant
* properties.
*
* @param {object} mQueryOptions
* The cache query options; the options of oModel.mURLParameters are added
* @param {string} sResourcePath
* The resource path
* @param {sap.ui.model.Context} [oContext]
* The context instance to be used, undefined for absolute bindings
* @param {string} [sGroupId]
* The group ID; mandatory if <code>bSideEffectsRefresh</code> is set
* @param {boolean} [bSideEffectsRefresh]
* Whether to perform a side-effects refresh
* @param {sap.ui.model.odata.v4.lib._Cache} [oOldCache]
* The old cache, in case it may be reused
* @returns {sap.ui.model.odata.v4.lib._Cache}
* The cache or <code>null</code> if the binding is relative and the given context is
* transient
*
* @private
*/
ODataBinding.prototype.createAndSetCache = function (mQueryOptions, sResourcePath, oContext,
sGroupId, bSideEffectsRefresh, oOldCache) {
var oCache, sDeepResourcePath, iGeneration;
this.mCacheQueryOptions = Object.assign({}, this.oModel.mURLParameters, mQueryOptions);
if (this.bRelative) { // quasi-absolute or relative binding
// mCacheByResourcePath has to be reset if parameters are changing
oCache = this.mCacheByResourcePath && this.mCacheByResourcePath[sResourcePath];
iGeneration = oContext.getGeneration?.() ?? 0;
if (oCache && oCache.$generation >= iGeneration) {
oCache.setActive(true);
} else {
sDeepResourcePath = this.oModel.resolve(this.sPath, oContext).slice(1);
oCache = this.doCreateCache(sResourcePath, this.mCacheQueryOptions, oContext,
sDeepResourcePath, sGroupId, bSideEffectsRefresh, oOldCache);
if (!(this.mParameters && this.mParameters.$$sharedRequest)) {
this.mCacheByResourcePath ??= {};
this.mCacheByResourcePath[sResourcePath] = oCache;
}
oCache.$deepResourcePath = sDeepResourcePath;
oCache.$generation = iGeneration;
}
} else { // absolute binding
oCache = this.doCreateCache(sResourcePath, this.mCacheQueryOptions, undefined,
undefined, sGroupId, bSideEffectsRefresh, oOldCache);
}
if (oOldCache && oOldCache !== oCache) {
this.deregisterChangeListener();
oOldCache.setActive(false);
}
if (this.mLateQueryOptions) {
oCache.setLateQueryOptions(this.mLateQueryOptions, /*bInvalidateTypes*/true);
}
this.oCache = oCache;
return oCache;
};
/**
* Deregisters the binding using the function it got via {@link #setDeregisterChangeListener}.
*
* @private
*/
ODataBinding.prototype.deregisterChangeListener = function () {
this.fnDeregisterChangeListener?.();
this.fnDeregisterChangeListener = undefined;
};
/**
* Destroys the object. The object must not be used anymore after this function was called.
*
* @public
* @since 1.66.0
*/
ODataBinding.prototype.destroy = function () {
this.mCacheByResourcePath = undefined;
this.deregisterChangeListener();
this.oCachePromise.then(function (oOldCache) {
oOldCache?.setActive(false);
}, function () {});
this.oCache = null;
this.oCachePromise = SyncPromise.resolve(null); // be nice to #withCache
this.mCacheQueryOptions = undefined;
// resolving functions e.g. for oReadPromise in #checkUpdateInternal may run after destroy
// of this binding and must not access the context
this.oContext = undefined;
this.oFetchCacheCallToken = undefined;
};
/**
* Hook method for {@link #fetchCache} to create a cache for this binding with the given
* resource path and query options.
*
* @param {string} sResourcePath
* The resource path, for example "EMPLOYEES"
* @param {object} mQueryOptions
* A map of key-value pairs representing the query string (requires "copy on write"!)
* @param {sap.ui.model.Context} [oContext]
* The context instance to be used, must be <code>undefined</code> for absolute bindings
* @param {string} [sDeepResourcePath=sResourcePath]
* The deep resource path to be used to build the target path for bound messages
* @param {string} [sGroupId]
* The group ID; mandatory if <code>bSideEffectsRefresh</code> is set
* @param {boolean} [bSideEffectsRefresh]
* Whether to perform a side-effects refresh
* @param {sap.ui.model.odata.v4.lib._Cache} [oOldCache]
* The old cache, in case it may be reused
* @returns {sap.ui.model.odata.v4.lib._Cache}
* The new cache instance
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#doCreateCache
* @private
*/
/**
* Hook method for {@link #fetchOrGetQueryOptionsForOwnCache} to determine the query options for
* this binding.
*
* @param {sap.ui.model.Context} [oContext]
* The context instance to be used for a relative binding
* @returns {object|undefined|sap.ui.base.SyncPromise<object|undefined>}
* The binding's query options (if any) or a promise resolving with them
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#doFetchOrGetQueryOptions
* @private
*/
/**
* @override
* @see sap.ui.base.EventProvider#getEventingParent
*/
ODataBinding.prototype.getEventingParent = function () {
// this allows that dataRequested/dataReceived events are bubbled up to the model
return this.oModel;
};
/**
* Creates a cache for this binding if a cache is needed and updates <code>oCachePromise</code>.
*
* @param {sap.ui.model.Context} [oContext]
* The context instance to be used, may be undefined for absolute bindings
* @param {boolean} [bIgnoreParentCache]
* Whether the parent cache is ignored and a new cache shall be created. This is for example
* needed during the resume process in case this binding has changed but its parent
* binding has not (see {@link sap.ui.model.odata.v4.ODataListBinding#resumeInternal})
* @param {boolean} [bKeepQueryOptions]
* Whether to keep existing (late) query options and not to run auto-$expand/$select again
* (cannot be combined with <code>bIgnoreParentCache</code>!)
* @param {string} [sGroupId]
* The group ID; mandatory if <code>bSideEffectsRefresh</code> is set
* @param {boolean} [bSideEffectsRefresh]
* Whether to perform a side-effects refresh
* @param {boolean} [bSync]
* Whether to create and set the cache synchronously; <code>bKeepQueryOptions</code> must be
* <code>true</code>
* @throws {Error} If
* <ul>
* <li> auto-$expand/$select is still running and query options shall be kept (this case is
* just not yet implemented and should not be needed),
* <li> bSync is set but bKeepQueryOptions is not.
* </ul>
*
* @private
*/
ODataBinding.prototype.fetchCache = function (oContext, bIgnoreParentCache, bKeepQueryOptions,
sGroupId, bSideEffectsRefresh, bSync) {
var oCache = this.oCache,
oCallToken = {
// propagate old cache from first call of fetchCache to the latest call
oOldCache : oCache === undefined ? this.oFetchCacheCallToken.oOldCache : oCache
},
aPromises,
that = this;
if (!this.bRelative) {
oContext = undefined;
}
if (!oCache && bKeepQueryOptions) {
if (oCache === undefined) {
throw new Error("Unsupported bKeepQueryOptions while oCachePromise is pending");
}
return;
}
if (bSync && !bKeepQueryOptions) {
throw new Error("Unsupported bSync w/o bKeepQueryOptions");
}
this.oCache = undefined;
this.oFetchCacheCallToken = oCallToken;
if (bKeepQueryOptions) {
// re-create an equivalent cache, but skip auto-$expand/$select
this.oCachePromise = SyncPromise.resolve(bSync || Promise.resolve()).then(function () {
return that.createAndSetCache(that.mCacheQueryOptions, oCache.getResourcePath(),
oContext, sGroupId, bSideEffectsRefresh, oCache);
});
return;
}
aPromises = [
this.fetchOrGetQueryOptionsForOwnCache(oContext, bIgnoreParentCache),
this.oModel.oRequestor.ready()
];
this.mCacheQueryOptions = undefined;
this.oCachePromise = SyncPromise.all(aPromises).then(function (aResult) {
var mQueryOptions = aResult[0].mQueryOptions;
if (oCallToken.initiallySuspended) {
return null;
}
if (aResult[0].sReducedPath) {
that.sReducedPath = aResult[0].sReducedPath;
}
// If there are mQueryOptions, the binding must create a cache. Do not create a cache
// for a virtual context or if below a transient context
if (!that.prepareDeepCreate(oContext, mQueryOptions)) {
return that.fetchResourcePath(oContext).then(function (sResourcePath) {
// create cache only for the latest call to fetchCache
if (that.oFetchCacheCallToken !== oCallToken) {
// a previous call waits for the current one to finish
return that.oCachePromise.then(function (oNewCache) {
// the previous call must fail if a new cache was created
if (oNewCache === oCallToken.oOldCache) {
return oNewCache;
}
const oError
= new Error("Cache discarded as a new cache has been created");
oError.canceled = true;
throw oError;
});
}
return that.oModel.waitForKeepAliveBinding(that).then(function () {
that.oFetchCacheCallToken = undefined; // cleanup
return that.createAndSetCache(mQueryOptions, sResourcePath, oContext,
sGroupId, bSideEffectsRefresh, oCallToken.oOldCache);
});
});
}
oCallToken.oOldCache = undefined; // cleanup own token only
if (oCache) {
oCache.setActive(false);
}
that.oCache = null;
return null;
});
// Note: this happens if the promise to read data for the canonical path's
// key predicate is rejected with a canceled error or the cache creation failed (e.g. in
// case the cache has been discarded because a new cache has been created).
this.oCachePromise.catch(this.oModel.getReporter());
};
/**
* Fetches the query options to create the cache for this binding and the binding's reduced
* path.
*
* @param {sap.ui.model.Context} [oContext]
* The context instance to be used, must be undefined for absolute bindings
* @param {boolean} [bIgnoreParentCache]
* Whether the query options of the parent cache shall be ignored and own query options are
* determined (see {@link #fetchCache})
* @returns {object|sap.ui.base.SyncPromise<object>}
* An object having two properties (or a promise resolving with it):
* {object} mQueryOptions - The query options to create the cache for this binding or
* <code>undefined</code> if no cache is to be created
* {string} sReducedPath - The binding's absolute, reduced path in the cache hierarchy
*
* @private
*/
ODataBinding.prototype.fetchOrGetQueryOptionsForOwnCache = function (oContext,
bIgnoreParentCache) {
var bHasNonSystemQueryOptions,
vQueryOptions, // {object|undefined|sap.ui.base.SyncPromise<object|undefined>}
sResolvedPath = this.oModel.resolve(this.sPath, oContext),
that = this;
/*
* Wraps the given query options and adds sReducedPath to create a result for
* #fetchOrGetQueryOptionsForOwnCache.
*
* @param {object} [mQueryOptions]
* Map of query options, or <code>undefined</code>
* @param {boolean} [bDropEmptyObject]
* Whether an empty query options object should be replaced by <code>undefined</code>
* @param {string} [sReducedPath=sResolvedPath]
* The reduced path
* @returns {object}
* A result for #fetchOrGetQueryOptionsForOwnCache
*/
function _wrapQueryOptions(mQueryOptions, bDropEmptyObject, sReducedPath) {
if (bDropEmptyObject && mQueryOptions && _Helper.isEmptyObject(mQueryOptions)) {
mQueryOptions = undefined;
}
return {
mQueryOptions : mQueryOptions,
sReducedPath : sReducedPath || sResolvedPath
};
}
/*
* Waits for <code>vQueryOptions</code> (if needed) and then creates a result for
* #fetchOrGetQueryOptionsForOwnCache.
*
* @param {boolean} [bDropEmptyObject]
* Whether an empty query options object should be replaced by <code>undefined</code>
* @param {string} [sReducedPath=sResolvedPath]
* The reduced path
* @returns {object|sap.ui.base.SyncPromise<object>}
* A result for #fetchOrGetQueryOptionsForOwnCache
*/
function wrapQueryOptions(bDropEmptyObject, sReducedPath) {
if (vQueryOptions instanceof SyncPromise) {
if (!vQueryOptions.isFulfilled()) {
return vQueryOptions.then(function (mQueryOptions) {
return _wrapQueryOptions(mQueryOptions, bDropEmptyObject, sReducedPath);
});
}
vQueryOptions = vQueryOptions.getResult();
}
return _wrapQueryOptions(vQueryOptions, bDropEmptyObject, sReducedPath);
}
if (this.oOperation // operation binding manages its cache on its own
|| !sResolvedPath // unresolved binding
|| this.isMeta()) {
return _wrapQueryOptions();
}
// auto-$expand/$select and binding is a parent binding, so that it needs to wait until all
// its child bindings know via the corresponding promise in this.aChildCanUseCachePromises
// if they can use the parent binding's cache
// With $$aggregation, no auto-$expand/$select is needed
vQueryOptions = this.doFetchOrGetQueryOptions(oContext);
if (this.oModel.bAutoExpandSelect && this.aChildCanUseCachePromises
&& !_Helper.isDataAggregation(this.mParameters)) {
// For auto-$expand/$select, wait for query options of dependent bindings:
// Promise.resolve() ensures all dependent bindings are created and have sent their
// query options promise to this binding via fetchIfChildCanUseCache.
// The aggregated query options of this binding and its dependent bindings are available
// in that.mAggregatedQueryOptions once all these promises are fulfilled.
vQueryOptions = SyncPromise.all([
vQueryOptions,
Promise.resolve().then(function () {
return SyncPromise.all(that.aChildCanUseCachePromises);
})
]).then(function (aResult) {
that.aChildCanUseCachePromises = [];
that.updateAggregatedQueryOptions(aResult[0]);
return that.mAggregatedQueryOptions;
});
}
// parent cache is ignored or (quasi-)absolute binding
if (bIgnoreParentCache || !this.bRelative || !oContext.fetchValue) {
// the binding shall create its own cache
return wrapQueryOptions();
}
// auto-$expand/$select: Use parent binding's cache if possible
if (this.oModel.bAutoExpandSelect) {
bHasNonSystemQueryOptions = this.mParameters
&& Object.keys(that.mParameters).some(function (sKey) {
return sKey[0] !== "$" || sKey[1] === "$";
});
if (bHasNonSystemQueryOptions) {
return wrapQueryOptions();
}
return oContext.getBinding()
.fetchIfChildCanUseCache(oContext, that.sPath, vQueryOptions,
!this.mParameters) // duck typing for property binding
.then(function (sReducedPath) {
if (sReducedPath) {
vQueryOptions = undefined;
} else { // fetchCache only creates a cache if there are query options
vQueryOptions ??= {};
}
return wrapQueryOptions(false, sReducedPath);
});
}
// relative list or context binding with parameters which are not query options
// (such as $$groupId)
if (this.mParameters && !_Helper.isEmptyObject(this.mParameters)) {
return wrapQueryOptions();
}
// relative binding which may have query options from UI5 filter or sorter objects
return wrapQueryOptions(true);
};
/**
* Fetches the OData resource path for this binding using the given context.
* If '$$canonicalPath' is set or the context's path contains indexes, the resource path uses
* the context's canonical path, otherwise it uses the context's path.
*
* @param {sap.ui.model.Context|sap.ui.model.odata.v4.Context} [oContext=this.oContext]
* A context; if omitted, the binding's context is used
* @returns {sap.ui.base.SyncPromise<string|undefined>}
* A promise resolving with the resource path or <code>undefined</code> for an unresolved
* binding. If computation of the canonical path fails, the promise is rejected.
*
* @private
*/
ODataBinding.prototype.fetchResourcePath = function (oContext) {
var bCanonicalPath,
sContextPath,
oContextPathPromise,
that = this;
if (!this.bRelative) {
return SyncPromise.resolve(this.sPath.slice(1));
}
oContext ??= this.oContext;
if (!oContext) {
return SyncPromise.resolve();
}
sContextPath = oContext.getPath();
bCanonicalPath = oContext.fetchCanonicalPath
&& (this.mParameters && this.mParameters.$$canonicalPath
|| !this.isTransient() && rIndexOrTransientPredicate.test(sContextPath));
oContextPathPromise = bCanonicalPath
? oContext.fetchCanonicalPath()
: SyncPromise.resolve(sContextPath);
return oContextPathPromise.then(function (sContextResourcePath) {
return _Helper.buildPath(sContextResourcePath, that.sPath).slice(1);
});
};
/**
* Fires the 'dataReceived' event. The event is bubbled up to the model, unless it is prevented.
*
* @param {object} oParameters
* The event parameters
* @param {object} [oParameters.data]
* An empty data object if a back-end request succeeds
* @param {Error} [oParameters.error]
* The error object if a back-end request failed.
* @param {boolean} [bPreventBubbling]
* Whether to prevent bubbling this event to the model
*
* @private
*/
// @override sap.ui.model.Binding#fireDataReceived
ODataBinding.prototype.fireDataReceived = function (oParameters, bPreventBubbling) {
this.fireEvent("dataReceived", oParameters, /*bAllowPreventDefault*/false,
/*bEnableEventBubbling*/!bPreventBubbling);
};
/**
* Fires the 'dataRequested' event. The event is bubbled up to the model, unless it is
* prevented.
*
* @param {boolean} [bPreventBubbling]
* Whether to prevent bubbling this event to the model
*
* @private
*/
// @override sap.ui.model.Binding#fireDataRequested
ODataBinding.prototype.fireDataRequested = function (bPreventBubbling) {
this.fireEvent("dataRequested", undefined, /*bAllowPreventDefault*/false,
/*bEnableEventBubbling*/!bPreventBubbling);
};
/**
* Returns all bindings which have this binding as parent binding.
*
* @returns {sap.ui.model.odata.v4.ODataBinding[]}
* A list of dependent bindings, never <code>null</code>
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#getDependentBindings
* @private
*/
/**
* Returns the group ID of the binding that is used for read requests. The group ID of the
* binding is alternatively defined by
* <ul>
* <li> the <code>groupId</code> parameter of the OData model; see
* {@link sap.ui.model.odata.v4.ODataModel#constructor},
* <li> the <code>$$groupId</code> binding parameter; see
* {@link sap.ui.model.odata.v4.ODataModel#bindList} and
* {@link sap.ui.model.odata.v4.ODataModel#bindContext}.
* </ul>
*
* @returns {string}
* The group ID
*
* @public
* @since 1.81.0
*/
ODataBinding.prototype.getGroupId = function () {
return this.sGroupId
|| (this.bRelative && this.oContext && this.oContext.getGroupId
&& this.oContext.getGroupId())
|| this.oModel.getGroupId();
};
/**
* Returns the relative path for a given absolute path by stripping off the binding's resolved
* path or the path of the binding's return value context. Returns relative paths unchanged.
* The binding must be resolved to call this function.
*
* Note that the resulting path may start with a key predicate.
*
* Example: (The binding's resolved path is "/foo/bar"):
* "baz" -> "baz"
* "/foo/bar/baz" -> "baz"
* "/foo/bar('baz')" -> "('baz')"
* "/foo" -> undefined
*
* @param {string} sPath
* A path (absolute or relative to this binding)
* @returns {string|undefined}
* The given path, if it is already relative; otherwise the path relative to the binding's
* resolved path or return value context path; <code>undefined</code> if the path does not
* start with either of these paths.
*
* @private
*/
ODataBinding.prototype.getRelativePath = function (sPath) {
var sRelativePath;
if (sPath[0] === "/") {
sRelativePath = _Helper.getRelativePath(sPath, this.getResolvedPath());
if (sRelativePath === undefined && this.oReturnValueContext) {
sRelativePath = _Helper.getRelativePath(sPath, this.oReturnValueContext.getPath());
}
return sRelativePath;
}
return sPath;
};
/**
* Returns the "strongest" change reason that {@link #resume} would fire for this binding or any
* of its dependents.
*
* @returns {sap.ui.model.ChangeReason|undefined}
* The "strongest" change reason, or <code>undefined</code>
*
* @private
* @see #getDependentBindings
* @see #setResumeChangeReason
*/
ODataBinding.prototype.getResumeChangeReason = function () {
var sStrongestChangeReason = this.sResumeChangeReason;
this.getDependentBindings().forEach(function (oDependentBinding) {
var sDependentChangeReason = oDependentBinding.getResumeChangeReason();
if (sDependentChangeReason
&& hasPrecedenceOver(sDependentChangeReason, sStrongestChangeReason)) {
sStrongestChangeReason = sDependentChangeReason;
}
});
return sStrongestChangeReason;
};
/**
* Returns a promise which resolves as soon as this binding is resumed.
*
* @returns {sap.ui.base.SyncPromise<void>|undefined}
* This binding's current promise for {@link sap.ui.model.odata.v4.ODataParentBinding#resume},
* or <code>undefined</code> in case it is not currently suspended.
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#getResumePromise
* @private
* @see sap.ui.model.Binding#isSuspended
*/
/**
* Returns the root binding of this binding's hierarchy, see
* {@link topic:fccfb2eb41414f0792c165e69a878717 Initialization and Read Requests}.
*
* @returns {sap.ui.model.odata.v4.ODataContextBinding|sap.ui.model.odata.v4.ODataListBinding|
* sap.ui.model.odata.v4.ODataPropertyBinding|undefined}
* The root binding or <code>undefined</code> if this binding is unresolved (see
* {@link sap.ui.model.Binding#isResolved}).
*
* @public
* @since 1.53.0
*/
ODataBinding.prototype.getRootBinding = function () {
if (this.bRelative) {
if (!this.oContext) {
return undefined;
}
if (this.oContext.getBinding) {
return this.oContext.getBinding().getRootBinding();
}
}
return this;
};
/**
* Returns a promise which resolves as soon as this binding's root binding is resumed.
*
* @returns {sap.ui.base.SyncPromise<void>}
* The root binding's current promise for {@link #resume}, or
* <code>SyncPromise.resolve()</code> in case we have no root binding or it is not currently
* suspended.
*
* @private
* @see #checkSuspended
* @see #getResumePromise
* @see #isRootBindingSuspended
*/
ODataBinding.prototype.getRootBindingResumePromise = function () {
var oRootBinding = this.getRootBinding();
return oRootBinding && oRootBinding.getResumePromise() || SyncPromise.resolve();
};
/**
* Returns the group ID of the binding that is used for update requests. The update group ID of
* the binding is alternatively defined by
* <ul>
* <li> the <code>updateGroupId</code> parameter of the OData model; see
* {@link sap.ui.model.odata.v4.ODataModel#constructor},
* <li> the <code>$$updateGroupId</code> binding parameter; see
* {@link sap.ui.model.odata.v4.ODataModel#bindList} and
* {@link sap.ui.model.odata.v4.ODataModel#bindContext}.
* </ul>
*
* @returns {string}
* The update group ID
*
* @public
* @since 1.81.0
*/
ODataBinding.prototype.getUpdateGroupId = function () {
return this.sUpdateGroupId
|| (this.bRelative && this.oContext && this.oContext.getUpdateGroupId
&& this.oContext.getUpdateGroupId())
|| this.oModel.getUpdateGroupId();
};
/**
* Returns <code>true</code> if this binding or its dependent bindings have property changes,
* created entities, or entity deletions which have not been sent successfully to the server.
* This function does not take the invocation of OData operations
* (see {@link sap.ui.model.odata.v4.ODataContextBinding#invoke}) into account. Since 1.98.0,
* {@link sap.ui.model.odata.v4.Context#isInactive inactive} contexts are ignored, unless
* (since 1.100.0) their
* {@link sap.ui.model.odata.v4.ODataListBinding#event:createActivate activation} has been
* prevented and {@link sap.ui.model.odata.v4.Context#isInactive} therefore returns
* <code>1</code>.
*
* Note: If this binding is relative, its data is cached separately for each parent context
* path. This method returns <code>true</code> if there are pending changes for the current
* parent context path of this binding. If this binding is unresolved (see
* {@link sap.ui.model.Binding#isResolved}), it returns <code>false</code>.
*
* @param {boolean} [bIgnoreKeptAlive]
* Whether to ignore changes which will not be lost by APIs like
* {@link sap.ui.model.odata.v4.ODataListBinding#changeParameters changeParameters},
* {@link sap.ui.model.odata.v4.ODataListBinding#filter filter},
* {@link sap.ui.model.odata.v4.ODataListBinding#refresh refresh} (since 1.100.0),
* {@link sap.ui.model.odata.v4.ODataListBinding#sort sort}, or
* {@link sap.ui.model.odata.v4.ODataListBinding#suspend suspend} because they relate to a
* {@link sap.ui.model.odata.v4.Context#isKeepAlive kept-alive} (since 1.97.0) or
* {@link sap.ui.model.odata.v4.Context#delete deleted} (since 1.108.0) context of this
* binding. Since 1.98.0, {@link sap.ui.model.odata.v4.Context#isTransient transient}
* contexts of a {@link #getRootBinding root binding} are treated as kept alive by this flag.
* Since 1.99.0, the same happens for bindings using the <code>$$ownRequest</code> parameter
* (see {@link sap.ui.model.odata.v4.ODataModel#bindList}).
* @returns {boolean}
* <code>true</code> if the binding is resolved and has pending changes
*
* @public
* @since 1.39.0
*/
ODataBinding.prototype.hasPendingChanges = function (bIgnoreKeptAlive) {
return this._hasPendingChanges(bIgnoreKeptAlive);
};
/**
* Checks whether there are pending changes for the given path in the binding's cache (which may
* be inherited from the parent).
*
* @param {string} sPath
* The path (absolute or relative to this binding)
* @param {boolean} [bIgnoreKeptAlive]
* Whether to ignore changes which will not be lost by APIs like sort or filter because they
* relate to a deleted context or a context which is kept alive
* @returns {boolean}
* <code>true</code> if there are pending changes for the path
*
* @private
*/
ODataBinding.prototype.hasPendingChangesForPath = function (sPath, bIgnoreKeptAlive) {
return this.withCache(function (oCache, sCachePath, oBinding) {
return oCache.hasPendingChangesForPath(sCachePath, bIgnoreKeptAlive,
bIgnoreKeptAlive && (oBinding.isRoot() || oBinding.mParameters.$$ownRequest));
}, sPath, true).unwrap();
};
/**
* Checks whether there are pending changes in caches stored by resource path at this binding
* which have the given resource path as prefix. Called for unresolved bindings only.
*
* @param {string} sResourcePathPrefix
* The resource path prefix to identify the relevant caches
* @returns {boolean}
* <code>true</code> if there are pending changes in caches
*
* @private
*/
ODataBinding.prototype.hasPendingChangesInCaches = function (sResourcePathPrefix) {
var that = this;
if (!this.mCacheByResourcePath) {
return false;
}
return Object.keys(this.mCacheByResourcePath).some(function (sResourcePath) {
var oCache = that.mCacheByResourcePath[sResourcePath];
return oCache.$deepResourcePath.startsWith(sResourcePathPrefix)
&& oCache.hasPendingChangesForPath("");
});
};
/**
* Returns whether any dependent binding of this binding has pending changes
*
* @param {boolean} [bIgnoreKeptAlive]
* Whether to ignore changes which will not be lost by APIs like sort or filter because they
* relate to a deleted context or a context which is kept alive
* @param {boolean} [sPathPrefix]
* If supplied, only caches having a resource path starting with <code>sPathPrefix</code> are
* checked
* @returns {boolean}
* <code>true</code> if this binding has pending changes
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#hasPendingChangesInDependents
* @private
*/
/**
* Method not supported
*
* @returns {boolean}
* @throws {Error}
*
* @deprecated As of version 1.37.0, calling this method is not supported
* @public
* @since 1.37.0
* @ui5-not-supported
*/
// @override sap.ui.model.Binding#isInitial
ODataBinding.prototype.isInitial = function () {
throw new Error("Unsupported operation: isInitial");
};
/**
* Returns whether the binding points to metadata.
*
* @returns {boolean} - Whether the binding points to metadata
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#isMeta
* @private
*/
/**
* Returns whether the binding is absolute or quasi-absolute.
*
* @returns {boolean} Whether the binding is absolute or quasi-absolute
*
* @private
*/
ODataBinding.prototype.isRoot = function () {
return !this.bRelative || this.oContext && !this.oContext.getBinding;
};
/**
* Tells whether the binding's root binding is suspended.
*
* @returns {boolean} Whether the binding's root binding is suspended
*
* @private
*/
ODataBinding.prototype.isRootBindingSuspended = function () {
var oRootBinding = this.getRootBinding();
return oRootBinding && oRootBinding.isSuspended();
};
/**
* Whether the binding is transient (relative to a transient context).
*
* @returns {boolean} Whether the binding is transient
*
* @private
*/
ODataBinding.prototype.isTransient = function () {
return this.bRelative && this.oContext?.getPath().includes("($uid=");
};
/**
* Creates a lock for a group with this binding as owner.
*
* @param {string} [sGroupId]
* The group ID; defaults to this binding's (update) group ID
* @param {boolean} [bLocked]
* Whether the created lock is locked
* @param {boolean} [bModifying]
* Whether the reason for the group lock is a modifying request
* @param {function} [fnCancel]
* Function that is called when the group lock is canceled
* @returns {sap.ui.model.odata.v4.lib._GroupLock}
* The group lock
*
* @private
* @see sap.ui.model.odata.v4.ODataModel#lockGroup
*/
ODataBinding.prototype.lockGroup = function (sGroupId, bLocked, bModifying, fnCancel) {
sGroupId ??= (bModifying ? this.getUpdateGroupId() : this.getGroupId());
return this.oModel.lockGroup(sGroupId, this, bLocked, bModifying, fnCancel);
};
/**
* Reacts on a delete of an entity via the model. If a context of this binding has the given
* canonical path it is destroyed.
*
* @param {string} sCanonicalPath
* The canonical path of the entity (as a context path with the leading "/")
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#onDelete
* @private
*/
/**
* Prepares the binding for a deep create if there is a transient parent context. The default
* implementation only checks whether a cache may be created, and keeps the mQueryOptions for
* a later cache creation when below a transient context.
*
* @param {sap.ui.model.Context} [oContext]
* The parent context or <code>undefined</code> for absolute bindings
* @param {object} mQueryOptions
* The binding's cache query options
* @returns {boolean}
* Whether the binding must not create a cache, because the context is virtual or transient or
* below a transient context, or there are no query options for a cache
*
* @private
*/
ODataBinding.prototype.prepareDeepCreate = function (oContext, mQueryOptions) {
if (oContext) {
if (oContext.iIndex === Context.VIRTUAL) {
return true; // virtual parent => no cache
}
if (oContext.getPath().includes("($uid=")) {
// below a transient context => no cache, but keep the query options for a later
// creation in #adjustPredicate
this.mCacheQueryOptions = mQueryOptions;
return true;
}
}
return !mQueryOptions;
};
/**
* Returns a sync promise that is resolved when this binding is ready to be used (that is, when
* its cache has been determined).
*
* @returns {sap.ui.base.SyncPromise<void>}
* A sync promise that is resolved without a defined result when this binding is ready
*
* @private
*/
ODataBinding.prototype.ready = function () {
return this.oCachePromise;
};
/**
* Refreshes the binding. Prompts the model to retrieve data from the server using the given
* group ID and notifies the control that new data is available.
*
* Refresh is supported for bindings which are not relative to an
* {@link sap.ui.model.odata.v4.Context}, as well as (since 1.122.0) for bindings with the
* <code>$$ownRequest</code> parameter (see {@link sap.ui.model.odata.v4.ODataModel#bindList}
* and {@link sap.ui.model.odata.v4.ODataModel#bindContext})
*
* Note: When calling {@link #refresh} multiple times, the result of the request initiated by
* the last call determines the binding's data; it is <b>independent</b> of the order of calls
* to {@link sap.ui.model.odata.v4.ODataModel#submitBatch} with the given group ID.
*
* If there are pending changes that cannot be ignored, an error is thrown. Use
* {@link #hasPendingChanges} to check if there are such pending changes. If there are, call
* {@link sap.ui.model.odata.v4.ODataModel#submitBatch} to submit the changes or
* {@link sap.ui.model.odata.v4.ODataModel#resetChanges} to reset the changes before calling
* {@link #refresh}.
*
* Use {@link #requestRefresh} if you want to wait for the refresh.
*
* @param {string|boolean} [sGroupId]
* The group ID to be used for refresh; if not specified, the binding's group ID is used, see
* {@link #getGroupId}. For suspended bindings, only the binding's group ID is supported
* because {@link #resume} uses the binding's group ID. A value of type boolean is not
* accepted and an error will be thrown (a forced refresh is not supported).
*
* Valid values are <code>undefined</code>, '$auto', '$auto.*', '$direct' or application group
* IDs as specified in {@link sap.ui.model.odata.v4.ODataModel}.
* @throws {Error} If
* <ul>
* <li> the given group ID is invalid,
* <li> refresh on this binding is not supported,
* <li> a group ID different from the binding's group ID is specified for a suspended
* binding,
* <li> a value of type <code>boolean</code> is given,
* <li> or there are pending changes that cannot be ignored.
* </ul>
* The following pending changes are ignored:
* <ul>
* <li> changes relating to a {@link sap.ui.model.odata.v4.Context#isKeepAlive kept-alive}
* context of this binding (since 1.97.0),
* <li> {@link sap.ui.model.odata.v4.Context#isTransient transient} contexts of a
* {@link #getRootBinding root binding} (since 1.98.0),
* <li> {@link sap.ui.model.odata.v4.Context#delete deleted} contexts (since 1.108.0).
* </ul>
*
* @public
* @see sap.ui.model.Binding#refresh
* @see #getRootBinding
* @see #suspend
* @since 1.37.0
*/
// @override sap.ui.model.Binding#refresh
ODataBinding.prototype.refresh = function (sGroupId) {
if (typeof sGroupId === "boolean") {
throw new Error("Unsupported parameter bForceUpdate");
}
this.requestRefresh(sGroupId).catch(this.oModel.getReporter());
};
/**
* Refreshes the binding. The refresh method itself only performs some validation checks and
* forwards to this method doing the actual work. Interaction between contexts also runs via
* these internal methods.
*
* @param {string} sResourcePathPrefix
* The resource path prefix which is used to delete the dependent caches and corresponding
* messages; may be "" but not <code>undefined</code>
* @param {string} [sGroupId]
* The group ID to be used for refresh
* @param {boolean} [bCheckUpdate]
* If <code>true</code>, a property binding is expected to check for updates
* @param {boolean} [bKeepCacheOnError]
* If <code>true</code>, the binding data remains unchanged if the refresh fails
* @param {boolean} [bSync]
* If <code>true</code>, the cache is created synchronously; cannot be combined with the
* ODataModel's <code>sharedRequests</code> constructor parameter or with the
* ODataListBinding's <code>$$sharedRequest</code> binding parameter
* @returns {sap.ui.base.SyncPromise<void>}
* A promise which is resolved without a defined result when the refresh is finished, or
* rejected when the refresh fails; the promise is resolved immediately on a suspended binding
* @throws {Error}
* If the binding's root binding is suspended and a group ID different from the binding's
* group ID is given
*
* @abstract
* @function
* @name sap.ui.model.odata.v4.ODataBinding#refreshInternal
* @private
*/
/**
* Refreshes the binding; expects it to be suspended.
*
* @param {string} sGroupId
* The group ID to be used for the refresh
* @throws {Error}
* If a group ID different from the binding's group ID is given
* @private
*/
ODataBinding.prototype.refreshSuspended = function (sGroupId) {
if (sGroupId && sGroupId !== this.getGroupId()) {
throw new Error(this + ": Cannot refresh a suspended binding with group ID '"
+ sGroupId + "' (own group ID is '" + this.getGroupId() + "')");
}
this.setResumeChangeReason(ChangeReaso