From 423a6000b05e3b0a2140d31e0443665f6b502d7b Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 31 Dec 2025 08:16:59 +0100 Subject: [PATCH] Cache invalidation WIP --- .../src/main/scala/code/api/cache/Redis.scala | 31 +-- .../scala/code/api/constant/constant.scala | 204 ++++++++++++++---- .../main/scala/code/api/util/ApiRole.scala | 3 + .../scala/code/api/v6_0_0/APIMethods600.scala | 69 ++++++ .../code/api/v6_0_0/JSONFactory6.0.0.scala | 11 + 5 files changed, 264 insertions(+), 54 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index aa9fcb5c5..74313f4ec 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -2,6 +2,7 @@ package code.api.cache import code.api.JedisMethod import code.api.util.APIUtil +import code.api.Constant import code.util.Helper.MdcLoggable import com.openbankproject.commons.ExecutionContext.Implicits.global import redis.clients.jedis.{Jedis, JedisPool, JedisPoolConfig} @@ -58,8 +59,11 @@ object Redis extends MdcLoggable { // Redis startup health check private def performStartupHealthCheck(): Unit = { try { + val namespacePrefix = Constant.getGlobalCacheNamespacePrefix logger.info(s"Redis startup health check: connecting to $url:$port") - val testKey = "obp_startup_test" + logger.info(s"Global cache namespace prefix: '$namespacePrefix'") + + val testKey = s"${namespacePrefix}obp_startup_test" val testValue = s"OBP started at ${new java.util.Date()}" // Write test key with 1 hour TTL @@ -71,6 +75,7 @@ object Redis extends MdcLoggable { if (readResult.contains(testValue)) { logger.info(s"Redis health check PASSED - connected to $url:$port") logger.info(s" Pool: max=${poolConfig.getMaxTotal}, idle=${poolConfig.getMaxIdle}") + logger.info(s" Test key: $testKey") } else { logger.warn(s"WARNING: Redis health check FAILED - could not read back test key") } @@ -138,28 +143,28 @@ object Redis extends MdcLoggable { /** * this is the help method, which can be used to auto close all the jedisConnection - * - * @param method can only be "get" or "set" + * + * @param method can only be "get" or "set" * @param key the cache key - * @param ttlSeconds the ttl is option. - * if ttl == None, this means value will be cached forver + * @param ttlSeconds the ttl is option. + * if ttl == None, this means value will be cached forver * if ttl == Some(0), this means turn off the cache, do not use cache at all * if ttl == Some(Int), this mean the cache will be only cached for ttl seconds * @param value the cache value. - * + * * @return */ def use(method:JedisMethod.Value, key:String, ttlSeconds: Option[Int] = None, value:Option[String] = None) : Option[String] = { - + //we will get the connection from jedisPool later, and will always close it in the finally clause. var jedisConnection = None:Option[Jedis] - + if(ttlSeconds.equals(Some(0))){ // set ttl = 0, we will totally turn off the cache None }else{ try { jedisConnection = Some(jedisPool.getResource()) - + val redisResult = if (method ==JedisMethod.EXISTS) { jedisConnection.head.exists(key).toString }else if (method == JedisMethod.FLUSHDB) { @@ -175,13 +180,13 @@ object Redis extends MdcLoggable { } else if(method ==JedisMethod.SET && value.isDefined){ if (ttlSeconds.isDefined) {//if set ttl, call `setex` method to set the expired seconds. jedisConnection.head.setex(key, ttlSeconds.get, value.get).toString - } else {//if do not set ttl, call `set` method, the cache will be forever. + } else {//if do not set ttl, call `set` method, the cache will be forever. jedisConnection.head.set(key, value.get).toString } - } else {// the use()method parameters need to be set properly, it missing value in set, then will throw the exception. + } else {// the use()method parameters need to be set properly, it missing value in set, then will throw the exception. throw new RuntimeException("Please check the Redis.use parameters, if the method == set, the value can not be None !!!") } - //change the null to Option + //change the null to Option APIUtil.stringOrNone(redisResult) } catch { case e: Throwable => @@ -190,7 +195,7 @@ object Redis extends MdcLoggable { if (jedisConnection.isDefined && jedisConnection.get != null) jedisConnection.map(_.close()) } - } + } } /** diff --git a/obp-api/src/main/scala/code/api/constant/constant.scala b/obp-api/src/main/scala/code/api/constant/constant.scala index 4c16f99e9..73cee00a6 100644 --- a/obp-api/src/main/scala/code/api/constant/constant.scala +++ b/obp-api/src/main/scala/code/api/constant/constant.scala @@ -1,8 +1,10 @@ package code.api import code.api.util.{APIUtil, ErrorMessages} +import code.api.cache.Redis import code.util.Helper.MdcLoggable import com.openbankproject.commons.util.ApiStandards +import net.liftweb.util.Props // Note: Import this with: import code.api.Constant._ @@ -10,24 +12,24 @@ object Constant extends MdcLoggable { logger.info("Instantiating Constants") final val directLoginHeaderName = "directlogin" - + object Pagination { final val offset = 0 final val limit = 50 } - + final val shortEndpointTimeoutInMillis = APIUtil.getPropsAsLongValue(nameOfProperty = "short_endpoint_timeout", 1L * 1000L) final val mediumEndpointTimeoutInMillis = APIUtil.getPropsAsLongValue(nameOfProperty = "medium_endpoint_timeout", 7L * 1000L) final val longEndpointTimeoutInMillis = APIUtil.getPropsAsLongValue(nameOfProperty = "long_endpoint_timeout", 55L * 1000L) - + final val h2DatabaseDefaultUrlValue = "jdbc:h2:mem:OBPTest_H2_v2.1.214;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=10" final val HostName = APIUtil.getPropsValue("hostname").openOrThrowException(ErrorMessages.HostnameNotSpecified) final val CONNECTOR = APIUtil.getPropsValue("connector") final val openidConnectEnabled = APIUtil.getPropsAsBoolValue("openid_connect.enabled", false) - + final val bgRemoveSignOfAmounts = APIUtil.getPropsAsBoolValue("BG_remove_sign_of_amounts", false) - + final val ApiInstanceId = { val apiInstanceIdFromProps = APIUtil.getPropsValue("api_instance_id") if(apiInstanceIdFromProps.isDefined){ @@ -35,16 +37,106 @@ object Constant extends MdcLoggable { apiInstanceIdFromProps.head }else{ s"${apiInstanceIdFromProps.head}_${APIUtil.generateUUID()}" - } + } }else{ APIUtil.generateUUID() } } - + + /** + * Get the global cache namespace prefix for Redis keys. + * This prefix ensures that cache keys from different OBP instances and environments don't conflict. + * + * The prefix format is: {instance_id}_{environment}_ + * Examples: + * - "mybank_prod_" + * - "mybank_test_" + * - "mybank_dev_" + * - "abc123_staging_" + * + * @return A string prefix to be prepended to all Redis cache keys + */ + def getGlobalCacheNamespacePrefix: String = { + val instanceId = APIUtil.getPropsValue("api_instance_id").getOrElse("obp") + val environment = Props.mode match { + case Props.RunModes.Production => "prod" + case Props.RunModes.Staging => "staging" + case Props.RunModes.Development => "dev" + case Props.RunModes.Test => "test" + case _ => "unknown" + } + s"${instanceId}_${environment}_" + } + + /** + * Get the current version counter for a cache namespace. + * This allows for easy cache invalidation by incrementing the counter. + * + * The counter is stored in Redis with a key like: "mybank_prod_cache_version_rd_localised" + * If the counter doesn't exist, it defaults to 1. + * + * @param namespaceId The cache namespace identifier (e.g., "rd_localised", "rd_dynamic", "connector") + * @return The current version counter for that namespace + */ + def getCacheNamespaceVersion(namespaceId: String): Long = { + val versionKey = s"${getGlobalCacheNamespacePrefix}cache_version_${namespaceId}" + try { + Redis.use(JedisMethod.GET, versionKey, None, None) + .map(_.toLong) + .getOrElse { + // Initialize counter to 1 if it doesn't exist + Redis.use(JedisMethod.SET, versionKey, None, Some("1")) + 1L + } + } catch { + case _: Throwable => + // If Redis is unavailable, return 1 as default + 1L + } + } + + /** + * Increment the version counter for a cache namespace. + * This effectively invalidates all cached keys in that namespace by making them unreachable. + * + * Usage example: + * Before: mybank_prod_rd_localised_1_en_US_v4.0.0 + * After incrementing: mybank_prod_rd_localised_2_en_US_v4.0.0 + * (old keys with "_1_" are now orphaned and will be ignored) + * + * @param namespaceId The cache namespace identifier (e.g., "rd_localised", "rd_dynamic") + * @return The new version number, or None if increment failed + */ + def incrementCacheNamespaceVersion(namespaceId: String): Option[Long] = { + val versionKey = s"${getGlobalCacheNamespacePrefix}cache_version_${namespaceId}" + try { + val newVersion = Redis.use(JedisMethod.INCR, versionKey, None, None) + .map(_.toLong) + logger.info(s"Cache namespace version incremented: ${namespaceId} -> ${newVersion.getOrElse("unknown")}") + newVersion + } catch { + case e: Throwable => + logger.error(s"Failed to increment cache namespace version for ${namespaceId}: ${e.getMessage}") + None + } + } + + /** + * Build a versioned cache prefix with the namespace counter included. + * Format: {instance}_{env}_{prefix}_{version}_ + * + * @param basePrefix The base prefix name (e.g., "rd_localised", "rd_dynamic") + * @return Versioned prefix string (e.g., "mybank_prod_rd_localised_1_") + */ + def getVersionedCachePrefix(basePrefix: String): String = { + val version = getCacheNamespaceVersion(basePrefix) + s"${getGlobalCacheNamespacePrefix}${basePrefix}_${version}_" + } + final val localIdentityProvider = APIUtil.getPropsValue("local_identity_provider", HostName) - + final val mailUsersUserinfoSenderAddress = APIUtil.getPropsValue("mail.users.userinfo.sender.address", "sender-not-set") - + final val oauth2JwkSetUrl = APIUtil.getPropsValue(nameOfProperty = "oauth2.jwk_set.url") final val consumerDefaultLogoUrl = APIUtil.getPropsValue("consumer_default_logo_url") @@ -52,7 +144,7 @@ object Constant extends MdcLoggable { // This is the part before the version. Do not change this default! final val ApiPathZero = APIUtil.getPropsValue("apiPathZero", ApiStandards.obp.toString) - + final val CUSTOM_PUBLIC_VIEW_ID = "_public" final val SYSTEM_OWNER_VIEW_ID = "owner" // From this commit new owner views are system views final val SYSTEM_AUDITOR_VIEW_ID = "auditor" @@ -75,7 +167,7 @@ object Constant extends MdcLoggable { final val SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID = "InitiatePaymentsBerlinGroup" //This is used for the canRevokeAccessToViews_ and canGrantAccessToViews_ fields of SYSTEM_OWNER_VIEW_ID or SYSTEM_STANDARD_VIEW_ID. - final val DEFAULT_CAN_GRANT_AND_REVOKE_ACCESS_TO_VIEWS = + final val DEFAULT_CAN_GRANT_AND_REVOKE_ACCESS_TO_VIEWS = SYSTEM_OWNER_VIEW_ID:: SYSTEM_AUDITOR_VIEW_ID:: SYSTEM_ACCOUNTANT_VIEW_ID:: @@ -91,13 +183,13 @@ object Constant extends MdcLoggable { SYSTEM_READ_TRANSACTIONS_DETAIL_VIEW_ID:: SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID:: SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID:: - SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID :: + SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID :: SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID :: Nil - + //We allow CBS side to generate views by getBankAccountsForUser.viewsToGenerate filed. // viewsToGenerate can be any views, and OBP will check the following list, to make sure only allowed views are generated // If some views are not allowed, obp just log it, do not throw exceptions. - final val VIEWS_GENERATED_FROM_CBS_WHITE_LIST = + final val VIEWS_GENERATED_FROM_CBS_WHITE_LIST = SYSTEM_OWNER_VIEW_ID:: SYSTEM_ACCOUNTANT_VIEW_ID:: SYSTEM_AUDITOR_VIEW_ID:: @@ -110,39 +202,70 @@ object Constant extends MdcLoggable { SYSTEM_INITIATE_PAYMENTS_BERLIN_GROUP_VIEW_ID :: Nil //These are the default incoming and outgoing account ids. we will create both during the boot.scala. - final val INCOMING_SETTLEMENT_ACCOUNT_ID = "OBP-INCOMING-SETTLEMENT-ACCOUNT" - final val OUTGOING_SETTLEMENT_ACCOUNT_ID = "OBP-OUTGOING-SETTLEMENT-ACCOUNT" - final val ALL_CONSUMERS = "ALL_CONSUMERS" + final val INCOMING_SETTLEMENT_ACCOUNT_ID = "OBP-INCOMING-SETTLEMENT-ACCOUNT" + final val OUTGOING_SETTLEMENT_ACCOUNT_ID = "OBP-OUTGOING-SETTLEMENT-ACCOUNT" + final val ALL_CONSUMERS = "ALL_CONSUMERS" final val PARAM_LOCALE = "locale" final val PARAM_TIMESTAMP = "_timestamp_" + // Cache Namespace IDs - Single source of truth for all namespace identifiers + final val CALL_COUNTER_NAMESPACE = "call_counter" + final val RL_ACTIVE_NAMESPACE = "rl_active" + final val RD_LOCALISED_NAMESPACE = "rd_localised" + final val RD_DYNAMIC_NAMESPACE = "rd_dynamic" + final val RD_STATIC_NAMESPACE = "rd_static" + final val RD_ALL_NAMESPACE = "rd_all" + final val SWAGGER_STATIC_NAMESPACE = "swagger_static" + final val CONNECTOR_NAMESPACE = "connector" + final val METRICS_STABLE_NAMESPACE = "metrics_stable" + final val METRICS_RECENT_NAMESPACE = "metrics_recent" + final val ABAC_RULE_NAMESPACE = "abac_rule" - final val LOCALISED_RESOURCE_DOC_PREFIX = "rd_localised_" - final val DYNAMIC_RESOURCE_DOC_CACHE_KEY_PREFIX = "rd_dynamic_" - final val STATIC_RESOURCE_DOC_CACHE_KEY_PREFIX = "rd_static_" - final val ALL_RESOURCE_DOC_CACHE_KEY_PREFIX = "rd_all_" - final val STATIC_SWAGGER_DOC_CACHE_KEY_PREFIX = "swagger_static_" + // List of all versioned cache namespaces + final val ALL_CACHE_NAMESPACES = List( + CALL_COUNTER_NAMESPACE, + RL_ACTIVE_NAMESPACE, + RD_LOCALISED_NAMESPACE, + RD_DYNAMIC_NAMESPACE, + RD_STATIC_NAMESPACE, + RD_ALL_NAMESPACE, + SWAGGER_STATIC_NAMESPACE, + CONNECTOR_NAMESPACE, + METRICS_STABLE_NAMESPACE, + METRICS_RECENT_NAMESPACE, + ABAC_RULE_NAMESPACE + ) + + // Cache key prefixes with global namespace and versioning for easy invalidation + // Version counter allows invalidating entire cache namespaces by incrementing the counter + // Example: rd_localised_1_ → rd_localised_2_ (all old keys with _1_ become unreachable) + def LOCALISED_RESOURCE_DOC_PREFIX: String = getVersionedCachePrefix(RD_LOCALISED_NAMESPACE) + def DYNAMIC_RESOURCE_DOC_CACHE_KEY_PREFIX: String = getVersionedCachePrefix(RD_DYNAMIC_NAMESPACE) + def STATIC_RESOURCE_DOC_CACHE_KEY_PREFIX: String = getVersionedCachePrefix(RD_STATIC_NAMESPACE) + def ALL_RESOURCE_DOC_CACHE_KEY_PREFIX: String = getVersionedCachePrefix(RD_ALL_NAMESPACE) + def STATIC_SWAGGER_DOC_CACHE_KEY_PREFIX: String = getVersionedCachePrefix(SWAGGER_STATIC_NAMESPACE) final val CREATE_LOCALISED_RESOURCE_DOC_JSON_TTL: Int = APIUtil.getPropsValue(s"createLocalisedResourceDocJson.cache.ttl.seconds", "3600").toInt final val GET_DYNAMIC_RESOURCE_DOCS_TTL: Int = APIUtil.getPropsValue(s"dynamicResourceDocsObp.cache.ttl.seconds", "3600").toInt final val GET_STATIC_RESOURCE_DOCS_TTL: Int = APIUtil.getPropsValue(s"staticResourceDocsObp.cache.ttl.seconds", "3600").toInt final val SHOW_USED_CONNECTOR_METHODS: Boolean = APIUtil.getPropsAsBoolValue(s"show_used_connector_methods", false) - // Rate Limiting Cache Prefixes - final val CALL_COUNTER_PREFIX = "call_counter_" - final val RATE_LIMIT_ACTIVE_PREFIX = "rl_active_" + // Rate Limiting Cache Prefixes (with global namespace and versioning) + // Both call_counter and rl_active are versioned for consistent cache invalidation + def CALL_COUNTER_PREFIX: String = getVersionedCachePrefix(CALL_COUNTER_NAMESPACE) + def RATE_LIMIT_ACTIVE_PREFIX: String = getVersionedCachePrefix(RL_ACTIVE_NAMESPACE) final val RATE_LIMIT_ACTIVE_CACHE_TTL: Int = APIUtil.getPropsValue("rateLimitActive.cache.ttl.seconds", "3600").toInt - // Connector Cache Prefixes - final val CONNECTOR_PREFIX = "connector_" + // Connector Cache Prefixes (with global namespace and versioning) + def CONNECTOR_PREFIX: String = getVersionedCachePrefix(CONNECTOR_NAMESPACE) - // Metrics Cache Prefixes - final val METRICS_STABLE_PREFIX = "metrics_stable_" - final val METRICS_RECENT_PREFIX = "metrics_recent_" + // Metrics Cache Prefixes (with global namespace and versioning) + def METRICS_STABLE_PREFIX: String = getVersionedCachePrefix(METRICS_STABLE_NAMESPACE) + def METRICS_RECENT_PREFIX: String = getVersionedCachePrefix(METRICS_RECENT_NAMESPACE) + + // ABAC Cache Prefixes (with global namespace and versioning) + def ABAC_RULE_PREFIX: String = getVersionedCachePrefix(ABAC_RULE_NAMESPACE) - // ABAC Cache Prefixes - final val ABAC_RULE_PREFIX = "abac_rule_" - final val CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT = "can_see_transaction_other_bank_account" final val CAN_SEE_TRANSACTION_METADATA = "can_see_transaction_metadata" final val CAN_SEE_TRANSACTION_DESCRIPTION = "can_see_transaction_description" @@ -347,7 +470,7 @@ object Constant extends MdcLoggable { CAN_SEE_BANK_ACCOUNT_CURRENCY, CAN_SEE_TRANSACTION_STATUS ) - + final val SYSTEM_VIEW_PERMISSION_COMMON = List( CAN_SEE_TRANSACTION_THIS_BANK_ACCOUNT, CAN_SEE_TRANSACTION_OTHER_BANK_ACCOUNT, @@ -564,14 +687,14 @@ object RequestHeader { final lazy val `TPP-Signature-Certificate` = "TPP-Signature-Certificate" // Berlin Group /** - * The If-Modified-Since request HTTP header makes the request conditional: - * the server sends back the requested resource, with a 200 status, - * only if it has been last modified after the given date. - * If the resource has not been modified since, the response is a 304 without any body; - * the Last-Modified response header of a previous request contains the date of last modification. + * The If-Modified-Since request HTTP header makes the request conditional: + * the server sends back the requested resource, with a 200 status, + * only if it has been last modified after the given date. + * If the resource has not been modified since, the response is a 304 without any body; + * the Last-Modified response header of a previous request contains the date of last modification. * Unlike If-Unmodified-Since, If-Modified-Since can only be used with a GET or HEAD. * - * When used in combination with If-None-Match, it is ignored, unless the server doesn't support If-None-Match. + * When used in combination with If-None-Match, it is ignored, unless the server doesn't support If-None-Match. */ final lazy val `If-Modified-Since` = "If-Modified-Since" } @@ -605,4 +728,3 @@ object BerlinGroup extends Enumeration { val SMS_OTP, CHIP_OTP, PHOTO_OTP, PUSH_OTP = Value } } - diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index defdd4db8..a8ec0c221 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -416,6 +416,9 @@ object ApiRole extends MdcLoggable{ case class CanGetCacheNamespaces(requiresBankId: Boolean = false) extends ApiRole lazy val canGetCacheNamespaces = CanGetCacheNamespaces() + case class CanInvalidateCacheNamespace(requiresBankId: Boolean = false) extends ApiRole + lazy val canInvalidateCacheNamespace = CanInvalidateCacheNamespace() + case class CanDeleteCacheNamespace(requiresBankId: Boolean = false) extends ApiRole lazy val canDeleteCacheNamespace = CanDeleteCacheNamespace() diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 2bb695137..10457edd5 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -589,6 +589,75 @@ trait APIMethods600 { Some(List(canGetCurrentConsumer)) ) + staticResourceDocs += ResourceDoc( + invalidateCacheNamespace, + implementedInApiVersion, + nameOf(invalidateCacheNamespace), + "POST", + "/management/cache/namespaces/invalidate", + "Invalidate Cache Namespace", + """Invalidates a cache namespace by incrementing its version counter. + | + |This provides instant cache invalidation without deleting individual keys. + |Incrementing the version counter makes all keys with the old version unreachable. + | + |Available namespace IDs: call_counter, rl_active, rd_localised, rd_dynamic, + |rd_static, rd_all, swagger_static, connector, metrics_stable, metrics_recent, abac_rule + | + |Use after updating rate limits, translations, endpoints, or CBS data. + | + |Authentication is Required + |""", + InvalidateCacheNamespaceJsonV600(namespace_id = "rd_localised"), + InvalidatedCacheNamespaceJsonV600( + namespace_id = "rd_localised", + old_version = 1, + new_version = 2, + status = "invalidated" + ), + List( + InvalidJsonFormat, + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagCache, apiTagSystem, apiTagApi), + Some(List(canInvalidateCacheNamespace)) + ) + + lazy val invalidateCacheNamespace: OBPEndpoint = { + case "management" :: "cache" :: "namespaces" :: "invalidate" :: Nil JsonPost json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + postJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, callContext) { + json.extract[InvalidateCacheNamespaceJsonV600] + } + namespaceId = postJson.namespace_id + _ <- Helper.booleanToFuture( + s"Invalid namespace_id: $namespaceId. Valid values: ${Constant.ALL_CACHE_NAMESPACES.mkString(", ")}", + 400, + callContext + )(Constant.ALL_CACHE_NAMESPACES.contains(namespaceId)) + oldVersion = Constant.getCacheNamespaceVersion(namespaceId) + newVersionOpt = Constant.incrementCacheNamespaceVersion(namespaceId) + _ <- Helper.booleanToFuture( + s"Failed to increment cache namespace version for: $namespaceId", + 500, + callContext + )(newVersionOpt.isDefined) + } yield { + val result = InvalidatedCacheNamespaceJsonV600( + namespace_id = namespaceId, + old_version = oldVersion, + new_version = newVersionOpt.get, + status = "invalidated" + ) + (result, HttpCode.`200`(callContext)) + } + } + } + lazy val getCurrentConsumer: OBPEndpoint = { case "consumers" :: "current" :: Nil JsonGet _ => { cc => { diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index f275c5944..8ea1b07de 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -257,6 +257,17 @@ case class CacheNamespaceJsonV600( case class CacheNamespacesJsonV600(namespaces: List[CacheNamespaceJsonV600]) +case class InvalidateCacheNamespaceJsonV600( + namespace_id: String +) + +case class InvalidatedCacheNamespaceJsonV600( + namespace_id: String, + old_version: Long, + new_version: Long, + status: String +) + case class PostCustomerJsonV600( legal_name: String, customer_number: Option[String] = None,