mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 17:17:09 +00:00
Merge remote-tracking branch 'refs/remotes/Simon/develop' into develop
This commit is contained in:
commit
2ae58966c5
@ -355,8 +355,8 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
s"$roleNamePrefix$prettySummary${entitlementSuffix(path)}"
|
||||
}
|
||||
// substring role name to avoid it have over the maximum length of db column.
|
||||
if(roleName.size > 64) {
|
||||
roleName = StringUtils.substring(roleName, 0, 53) + roleName.hashCode()
|
||||
if(roleName.size > 255) {
|
||||
roleName = StringUtils.substring(roleName, 0, 244) + roleName.hashCode()
|
||||
}
|
||||
Some(List(
|
||||
ApiRole.getOrCreateDynamicApiRole(roleName, bankId.isDefined)
|
||||
|
||||
@ -693,6 +693,9 @@ object ApiRole extends MdcLoggable{
|
||||
case class CanDeleteSystemLevelDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canDeleteSystemLevelDynamicEntity = CanDeleteSystemLevelDynamicEntity()
|
||||
|
||||
case class CanDeleteCascadeSystemDynamicEntity(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canDeleteCascadeSystemDynamicEntity = CanDeleteCascadeSystemDynamicEntity()
|
||||
|
||||
case class CanDeleteBankLevelDynamicEntity(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canDeleteBankLevelDynamicEntity = CanDeleteBankLevelDynamicEntity()
|
||||
|
||||
|
||||
@ -2538,6 +2538,14 @@ object ExampleValue {
|
||||
| "type": "integer",
|
||||
| "example": "698761728934",
|
||||
| "description": "description of **number** field, can be markdown text."
|
||||
| },
|
||||
| "metadata": {
|
||||
| "type": "json",
|
||||
| "example": {
|
||||
| "tags": ["important", "verified"],
|
||||
| "settings": {"color": "blue", "priority": 1}
|
||||
| },
|
||||
| "description": "description of **metadata** field (JSON object or array), can be markdown text."
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
|
||||
@ -2966,7 +2966,7 @@ object Glossary extends MdcLoggable {
|
||||
|
|
||||
|**Supported field types:**
|
||||
|
|
||||
|STRING, INTEGER, DOUBLE, BOOLEAN, DATE_WITH_DAY (format: yyyy-MM-dd), and reference types (foreign keys)
|
||||
|STRING, INTEGER, DOUBLE, BOOLEAN, DATE_WITH_DAY (format: yyyy-MM-dd), JSON (objects and arrays), and reference types (foreign keys)
|
||||
|
|
||||
|**The hasPersonalEntity flag:**
|
||||
|
|
||||
|
||||
@ -106,6 +106,7 @@ object Migration extends MdcLoggable {
|
||||
renameCustomerRoleNames()
|
||||
addUniqueIndexOnResourceUserUserId()
|
||||
addIndexOnMappedMetricUserId()
|
||||
alterRoleNameLength()
|
||||
}
|
||||
|
||||
private def dummyScript(): Boolean = {
|
||||
@ -545,6 +546,13 @@ object Migration extends MdcLoggable {
|
||||
MigrationOfUserIdIndexes.addIndexOnMappedMetricUserId(name)
|
||||
}
|
||||
}
|
||||
|
||||
private def alterRoleNameLength(): Boolean = {
|
||||
val name = nameOf(alterRoleNameLength)
|
||||
runOnce(name) {
|
||||
MigrationOfRoleNameFieldLength.alterRoleNameLength(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
package code.api.util.migration
|
||||
|
||||
import code.api.util.APIUtil
|
||||
import code.api.util.migration.Migration.{DbFunction, saveLog}
|
||||
import code.entitlement.MappedEntitlement
|
||||
import code.entitlementrequest.MappedEntitlementRequest
|
||||
import code.scope.MappedScope
|
||||
import net.liftweb.common.Full
|
||||
import net.liftweb.mapper.Schemifier
|
||||
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.{ZoneId, ZonedDateTime}
|
||||
|
||||
object MigrationOfRoleNameFieldLength {
|
||||
|
||||
val oneDayAgo = ZonedDateTime.now(ZoneId.of("UTC")).minusDays(1)
|
||||
val oneYearInFuture = ZonedDateTime.now(ZoneId.of("UTC")).plusYears(1)
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
|
||||
def alterRoleNameLength(name: String): Boolean = {
|
||||
val entitlementTableExists = DbFunction.tableExists(MappedEntitlement)
|
||||
val entitlementRequestTableExists = DbFunction.tableExists(MappedEntitlementRequest)
|
||||
val scopeTableExists = DbFunction.tableExists(MappedScope)
|
||||
|
||||
if (!entitlementTableExists || !entitlementRequestTableExists || !scopeTableExists) {
|
||||
val startDate = System.currentTimeMillis()
|
||||
val commitId: String = APIUtil.gitCommit
|
||||
val isSuccessful = false
|
||||
val endDate = System.currentTimeMillis()
|
||||
val comment: String =
|
||||
s"""One or more required tables do not exist:
|
||||
|entitlement table exists: $entitlementTableExists
|
||||
|entitlementrequest table exists: $entitlementRequestTableExists
|
||||
|scope table exists: $scopeTableExists
|
||||
|""".stripMargin
|
||||
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
|
||||
return isSuccessful
|
||||
}
|
||||
|
||||
val startDate = System.currentTimeMillis()
|
||||
val commitId: String = APIUtil.gitCommit
|
||||
var isSuccessful = false
|
||||
|
||||
val executedSql =
|
||||
DbFunction.maybeWrite(true, Schemifier.infoF _) {
|
||||
APIUtil.getPropsValue("db.driver") match {
|
||||
case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") =>
|
||||
() =>
|
||||
"""
|
||||
|ALTER TABLE mappedentitlement ALTER COLUMN mrolename varchar(255);
|
||||
|ALTER TABLE mappedentitlementrequest ALTER COLUMN mrolename varchar(255);
|
||||
|ALTER TABLE mappedscope ALTER COLUMN mrolename varchar(255);
|
||||
|""".stripMargin
|
||||
case _ =>
|
||||
() =>
|
||||
"""
|
||||
|ALTER TABLE mappedentitlement ALTER COLUMN mrolename TYPE varchar(255);
|
||||
|ALTER TABLE mappedentitlementrequest ALTER COLUMN mrolename TYPE varchar(255);
|
||||
|ALTER TABLE mappedscope ALTER COLUMN mrolename TYPE varchar(255);
|
||||
|""".stripMargin
|
||||
}
|
||||
}
|
||||
|
||||
val endDate = System.currentTimeMillis()
|
||||
val comment: String =
|
||||
s"""Executed SQL:
|
||||
|$executedSql
|
||||
|
|
||||
|Increased mrolename column length from 64 to 255 characters in three tables:
|
||||
| - mappedentitlement
|
||||
| - mappedentitlementrequest
|
||||
| - mappedscope
|
||||
|
|
||||
|This allows for longer dynamic entity names and role names.
|
||||
|""".stripMargin
|
||||
isSuccessful = true
|
||||
saveLog(name, commitId, isSuccessful, startDate, endDate, comment)
|
||||
isSuccessful
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ import code.api.dynamic.endpoint.helper.practise.{
|
||||
PractiseEndpoint
|
||||
}
|
||||
import code.api.dynamic.endpoint.helper.{CompiledObjects, DynamicEndpointHelper}
|
||||
import code.api.dynamic.entity.helper.DynamicEntityInfo
|
||||
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
|
||||
import code.api.util.APIUtil.{fullBoxOrException, _}
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.ApiTag._
|
||||
@ -2321,6 +2321,17 @@ trait APIMethods400 extends MdcLoggable {
|
||||
| "summary": {
|
||||
| "type": "string",
|
||||
| "example": "User received 'No such price' error using Stripe API"
|
||||
| },
|
||||
| "custom_metadata": {
|
||||
| "type": "json",
|
||||
| "example": {
|
||||
| "priority": "high",
|
||||
| "tags": ["support", "billing"],
|
||||
| "context": {
|
||||
| "page": "checkout",
|
||||
| "step": 3
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
| },
|
||||
@ -2513,6 +2524,17 @@ trait APIMethods400 extends MdcLoggable {
|
||||
| "summary": {
|
||||
| "type": "string",
|
||||
| "example": "User received 'No such price' error using Stripe API"
|
||||
| },
|
||||
| "custom_metadata": {
|
||||
| "type": "json",
|
||||
| "example": {
|
||||
| "priority": "high",
|
||||
| "tags": ["support", "billing"],
|
||||
| "context": {
|
||||
| "page": "checkout",
|
||||
| "step": 3
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
| },
|
||||
@ -2801,6 +2823,8 @@ trait APIMethods400 extends MdcLoggable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private def deleteDynamicEntityMethod(
|
||||
bankId: Option[String],
|
||||
dynamicEntityId: String,
|
||||
|
||||
@ -9,8 +9,9 @@ import code.api.util.APIUtil._
|
||||
import code.api.util.ApiRole
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.ApiTag._
|
||||
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, _}
|
||||
import code.api.util.ErrorMessages.{$UserNotLoggedIn, InvalidDateFormat, InvalidJsonFormat, UnknownError, DynamicEntityOperationNotAllowed, _}
|
||||
import code.api.util.FutureUtil.EndpointContext
|
||||
import code.api.util.Glossary
|
||||
import code.api.util.NewStyle.HttpCode
|
||||
import code.api.util.{APIUtil, CallContext, DiagnosticDynamicEntityCheck, ErrorMessages, NewStyle, RateLimitingUtil}
|
||||
import code.api.util.NewStyle.function.extractQueryParams
|
||||
@ -24,6 +25,7 @@ import code.api.v4_0_0.JSONFactory400.createCallsLimitJson
|
||||
import code.api.v5_0_0.JSONFactory500
|
||||
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
|
||||
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
|
||||
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
|
||||
import code.api.v6_0_0.JSONFactory600.{DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupMembershipJsonV600, GroupMembershipsJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
|
||||
import code.api.v6_0_0.OBPAPI6_0_0
|
||||
import code.metrics.APIMetrics
|
||||
@ -39,16 +41,23 @@ import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN}
|
||||
import code.views.Views
|
||||
import code.views.system.ViewDefinition
|
||||
import code.webuiprops.{MappedWebUiPropsProvider, WebUiPropsCommons}
|
||||
import code.dynamicEntity.DynamicEntityCommons
|
||||
import code.DynamicData.{DynamicData, DynamicDataProvider}
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model.{CustomerAttribute, _}
|
||||
import com.openbankproject.commons.model.enums.DynamicEntityOperation._
|
||||
import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion}
|
||||
import net.liftweb.common.{Empty, Full}
|
||||
import net.liftweb.common.{Empty, Failure, Full}
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import net.liftweb.http.provider.HTTPParam
|
||||
import net.liftweb.http.rest.RestHelper
|
||||
import net.liftweb.json.{Extraction, JsonParser}
|
||||
import net.liftweb.json.JsonAST.JValue
|
||||
import net.liftweb.mapper.{By, Descending, MaxRows, OrderBy}
|
||||
import net.liftweb.json.JsonAST.{JArray, JObject, JString, JValue}
|
||||
import net.liftweb.json.JsonDSL._
|
||||
import net.liftweb.mapper.{By, Descending, MaxRows, NullRef, OrderBy}
|
||||
import code.api.util.ExampleValue.dynamicEntityResponseBodyExample
|
||||
import net.liftweb.common.Box
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.UUID.randomUUID
|
||||
@ -2472,7 +2481,7 @@ trait APIMethods600 {
|
||||
Constant.HostName + "/" + code.model.dataAccess.AuthUser.validateUserPath.mkString("/") + "/" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8")
|
||||
case _ =>
|
||||
// Default to portal_external_url property if available, otherwise fall back to hostname
|
||||
APIUtil.getPropsValue("portal_external_url", Constant.HostName) + "/" + code.model.dataAccess.AuthUser.validateUserPath.mkString("/") + "/" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8")
|
||||
APIUtil.getPropsValue("portal_external_url", Constant.HostName) + "/user-validation?token=" + java.net.URLEncoder.encode(savedUser.uniqueId.get, "UTF-8")
|
||||
}
|
||||
|
||||
val textContent = Some(s"Welcome! Please validate your account by clicking the following link: $emailValidationLink")
|
||||
@ -3420,6 +3429,269 @@ trait APIMethods600 {
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getWebUiProps,
|
||||
implementedInApiVersion,
|
||||
nameOf(getWebUiProps),
|
||||
"GET",
|
||||
"/management/webui_props",
|
||||
"Get WebUiProps",
|
||||
s"""
|
||||
|
|
||||
|Get WebUiProps - properties that configure the Web UI behavior and appearance.
|
||||
|
|
||||
|Properties with names starting with "webui_" can be stored in the database and managed via API.
|
||||
|
|
||||
|**Data Sources:**
|
||||
|
|
||||
|1. **Explicit WebUiProps (Database)**: Custom values created/updated via the API and stored in the database.
|
||||
|
|
||||
|2. **Implicit WebUiProps (Configuration File)**: Default values defined in the `sample.props.template` configuration file.
|
||||
|
|
||||
|**Query Parameter:**
|
||||
|
|
||||
|* `what` (optional, string, default: "active")
|
||||
| - `active`: Returns explicit props from database + implicit (default) props from configuration file
|
||||
| - When both sources have the same property name, the database value takes precedence
|
||||
| - Implicit props are marked with `webUiPropsId = "default"`
|
||||
| - `database`: Returns only explicit props from the database
|
||||
| - `config`: Returns only implicit (default) props from configuration file
|
||||
|
|
||||
|**Examples:**
|
||||
|
|
||||
|Get database props combined with defaults (default behavior):
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props?what=active
|
||||
|
|
||||
|Get only database-stored props:
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props?what=database
|
||||
|
|
||||
|Get only default props from configuration:
|
||||
|${getObpApiRoot}/v6.0.0/management/webui_props?what=config
|
||||
|
|
||||
|For more details about WebUI Props, including how to set config file defaults and precedence order, see ${Glossary.getGlossaryItemLink("webui_props")}.
|
||||
|
|
||||
|""",
|
||||
EmptyBody,
|
||||
ListResult(
|
||||
"webui_props",
|
||||
(List(WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com", Some("web-ui-props-id"))))
|
||||
)
|
||||
,
|
||||
List(
|
||||
UserNotLoggedIn,
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagWebUiProps),
|
||||
Some(List(canGetWebUiProps))
|
||||
)
|
||||
|
||||
|
||||
lazy val getWebUiProps: OBPEndpoint = {
|
||||
case "management" :: "webui_props":: Nil JsonGet req => {
|
||||
cc => implicit val ec = EndpointContext(Some(cc))
|
||||
val what = ObpS.param("what").getOrElse("active")
|
||||
logger.info(s"========== GET /obp/v6.0.0/management/webui_props called with what=$what ==========")
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
_ <- NewStyle.function.tryons(s"""$InvalidFilterParameterFormat `what` must be one of: active, database, config. Current value: $what""", 400, callContext) {
|
||||
what match {
|
||||
case "active" | "database" | "config" => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
_ <- NewStyle.function.hasEntitlement("", u.userId, ApiRole.canGetWebUiProps, callContext)
|
||||
explicitWebUiProps <- Future{ MappedWebUiPropsProvider.getAll() }
|
||||
implicitWebUiProps = getWebUIPropsPairs.map(webUIPropsPairs=>WebUiPropsCommons(webUIPropsPairs._1, webUIPropsPairs._2, webUiPropsId= Some("default")))
|
||||
result = what match {
|
||||
case "database" =>
|
||||
// Return only database props
|
||||
explicitWebUiProps
|
||||
case "config" =>
|
||||
// Return only config file props
|
||||
implicitWebUiProps.distinct
|
||||
case "active" =>
|
||||
// Return database props + config props (removing duplicates, database takes precedence)
|
||||
val implicitWebUiPropsRemovedDuplicated = if(explicitWebUiProps.nonEmpty){
|
||||
val duplicatedProps : List[WebUiPropsCommons]= explicitWebUiProps.map(explicitWebUiProp => implicitWebUiProps.filter(_.name == explicitWebUiProp.name)).flatten
|
||||
implicitWebUiProps diff duplicatedProps
|
||||
} else {
|
||||
implicitWebUiProps.distinct
|
||||
}
|
||||
explicitWebUiProps ++ implicitWebUiPropsRemovedDuplicated
|
||||
}
|
||||
} yield {
|
||||
logger.info(s"========== GET /obp/v6.0.0/management/webui_props returning ${result.size} records ==========")
|
||||
result.foreach { prop =>
|
||||
logger.info(s" - name: ${prop.name}, value: ${prop.value}, webUiPropsId: ${prop.webUiPropsId}")
|
||||
}
|
||||
logger.info(s"========== END GET /obp/v6.0.0/management/webui_props ==========")
|
||||
(ListResult("webui_props", result), HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
getSystemDynamicEntities,
|
||||
implementedInApiVersion,
|
||||
nameOf(getSystemDynamicEntities),
|
||||
"GET",
|
||||
"/management/system-dynamic-entities",
|
||||
"Get System Dynamic Entities",
|
||||
s"""Get all System Dynamic Entities with record counts.
|
||||
|
|
||||
|Each dynamic entity in the response includes a `record_count` field showing how many data records exist for that entity.
|
||||
|
|
||||
|For more information see ${Glossary.getGlossaryItemLink(
|
||||
"Dynamic-Entities"
|
||||
)} """,
|
||||
EmptyBody,
|
||||
ListResult(
|
||||
"dynamic_entities",
|
||||
List(dynamicEntityResponseBodyExample)
|
||||
),
|
||||
List(
|
||||
$UserNotLoggedIn,
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagManageDynamicEntity, apiTagApi),
|
||||
Some(List(canGetSystemLevelDynamicEntities))
|
||||
)
|
||||
|
||||
lazy val getSystemDynamicEntities: OBPEndpoint = {
|
||||
case "management" :: "system-dynamic-entities" :: Nil JsonGet req => {
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
dynamicEntities <- Future(
|
||||
NewStyle.function.getDynamicEntities(None, false)
|
||||
)
|
||||
} yield {
|
||||
val listCommons: List[DynamicEntityCommons] = dynamicEntities.sortBy(_.entityName)
|
||||
val jObjectsWithCounts = listCommons.map { entity =>
|
||||
val recordCount = DynamicData.count(
|
||||
By(DynamicData.DynamicEntityName, entity.entityName),
|
||||
By(DynamicData.IsPersonalEntity, false),
|
||||
if (entity.bankId.isEmpty) NullRef(DynamicData.BankId) else By(DynamicData.BankId, entity.bankId.get)
|
||||
)
|
||||
entity.jValue.asInstanceOf[JObject] ~ ("record_count" -> recordCount)
|
||||
}
|
||||
(
|
||||
ListResult("dynamic_entities", jObjectsWithCounts),
|
||||
HttpCode.`200`(cc.callContext)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def unboxResult[T: Manifest](box: Box[T], entityName: String): T = {
|
||||
if (box.isInstanceOf[Failure]) {
|
||||
val failure = box.asInstanceOf[Failure]
|
||||
// change the internal db column name 'dynamicdataid' to entity's id name
|
||||
val msg = failure.msg.replace(
|
||||
DynamicData.DynamicDataId.dbColumnName,
|
||||
StringUtils.uncapitalize(entityName) + "Id"
|
||||
)
|
||||
val changedMsgFailure = failure.copy(msg = s"$InternalServerError $msg")
|
||||
fullBoxOrException[T](changedMsgFailure)
|
||||
}
|
||||
box.openOrThrowException(s"$UnknownError ")
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
deleteSystemDynamicEntityCascade,
|
||||
implementedInApiVersion,
|
||||
nameOf(deleteSystemDynamicEntityCascade),
|
||||
"DELETE",
|
||||
"/management/system-dynamic-entities/cascade/DYNAMIC_ENTITY_ID",
|
||||
"Delete System Level Dynamic Entity Cascade",
|
||||
s"""Delete a DynamicEntity specified by DYNAMIC_ENTITY_ID and all its data records.
|
||||
|
|
||||
|This endpoint performs a cascade delete:
|
||||
|1. Deletes all data records associated with the dynamic entity
|
||||
|2. Deletes the dynamic entity definition itself
|
||||
|
|
||||
|Use with caution - this operation cannot be undone.
|
||||
|
|
||||
|For more information see ${Glossary.getGlossaryItemLink(
|
||||
"Dynamic-Entities"
|
||||
)}/
|
||||
|
|
||||
|""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List(
|
||||
$UserNotLoggedIn,
|
||||
UserHasMissingRoles,
|
||||
UnknownError
|
||||
),
|
||||
List(apiTagManageDynamicEntity, apiTagApi),
|
||||
Some(List(canDeleteCascadeSystemDynamicEntity))
|
||||
)
|
||||
lazy val deleteSystemDynamicEntityCascade: OBPEndpoint = {
|
||||
case "management" :: "system-dynamic-entities" :: "cascade" :: dynamicEntityId :: Nil JsonDelete _ => {
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
deleteDynamicEntityCascadeMethod(None, dynamicEntityId, cc)
|
||||
}
|
||||
}
|
||||
|
||||
private def deleteDynamicEntityCascadeMethod(
|
||||
bankId: Option[String],
|
||||
dynamicEntityId: String,
|
||||
cc: CallContext
|
||||
) = {
|
||||
for {
|
||||
// Get the dynamic entity
|
||||
(entity, _) <- NewStyle.function.getDynamicEntityById(
|
||||
bankId,
|
||||
dynamicEntityId,
|
||||
cc.callContext
|
||||
)
|
||||
// Get all data records for this entity
|
||||
(box, _) <- NewStyle.function.invokeDynamicConnector(
|
||||
GET_ALL,
|
||||
entity.entityName,
|
||||
None,
|
||||
None,
|
||||
entity.bankId,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
cc.callContext
|
||||
)
|
||||
resultList: JArray = unboxResult(
|
||||
box.asInstanceOf[Box[JArray]],
|
||||
entity.entityName
|
||||
)
|
||||
// Delete all data records
|
||||
_ <- Future.sequence {
|
||||
resultList.arr.map { record =>
|
||||
val idFieldName = DynamicEntityHelper.createEntityId(entity.entityName)
|
||||
val recordId = (record \ idFieldName).asInstanceOf[JString].s
|
||||
Future {
|
||||
DynamicDataProvider.connectorMethodProvider.vend.delete(
|
||||
entity.bankId,
|
||||
entity.entityName,
|
||||
recordId,
|
||||
None,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete the dynamic entity definition
|
||||
deleted: Box[Boolean] <- NewStyle.function.deleteDynamicEntity(
|
||||
bankId,
|
||||
dynamicEntityId
|
||||
)
|
||||
} yield {
|
||||
(deleted, HttpCode.`200`(cc.callContext))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import java.util.regex.Pattern
|
||||
|
||||
import code.api.util.ErrorMessages.DynamicEntityInstanceValidateFail
|
||||
import code.api.util.{APIUtil, CallContext, NewStyle}
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
import com.openbankproject.commons.model.enums.{DynamicEntityFieldType, DynamicEntityOperation}
|
||||
import com.openbankproject.commons.model._
|
||||
@ -144,7 +145,7 @@ trait DynamicEntityT {
|
||||
}
|
||||
}
|
||||
|
||||
object ReferenceType {
|
||||
object ReferenceType extends MdcLoggable {
|
||||
|
||||
private def recoverFn(fieldName: String, value: String, entityName: String): PartialFunction[Throwable, String] = {
|
||||
case _: Throwable => s"entity '$entityName' not found by the value '$value', the field name is '$fieldName'."
|
||||
@ -360,14 +361,18 @@ object ReferenceType {
|
||||
} else {
|
||||
val dynamicEntityName = typeName.replace("reference:", "")
|
||||
val errorMsg = s"""$dynamicEntityName not found by the id value '$value', propertyName is '$propertyName'"""
|
||||
NewStyle.function.invokeDynamicConnector(DynamicEntityOperation.GET_ONE,dynamicEntityName, None, Some(value), None, None, None, false,callContext)
|
||||
.recover {
|
||||
case _: Throwable => errorMsg
|
||||
}
|
||||
.map {
|
||||
case (Full(_), _) => ""
|
||||
case _ => errorMsg
|
||||
logger.info(s"========== Validating reference field: propertyName='$propertyName', typeName='$typeName', dynamicEntityName='$dynamicEntityName', value='$value' ==========")
|
||||
|
||||
Future {
|
||||
val exists = code.DynamicData.MappedDynamicDataProvider.existsById(dynamicEntityName, value)
|
||||
if (exists) {
|
||||
logger.info(s"========== Reference validation SUCCESS: propertyName='$propertyName', dynamicEntityName='$dynamicEntityName', value='$value' ==========")
|
||||
""
|
||||
} else {
|
||||
logger.warn(s"========== Reference validation FAILED: propertyName='$propertyName', dynamicEntityName='$dynamicEntityName', value='$value' ==========")
|
||||
errorMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,17 @@ object MappedDynamicDataProvider extends DynamicDataProvider with CustomJsonForm
|
||||
saveOrUpdate(bankId, entityName, requestBody, userId, isPersonalEntity, dynamicData)
|
||||
}
|
||||
|
||||
// Separate method for reference validation - only checks ID and entity name exist
|
||||
def existsById(entityName: String, id: String): Boolean = {
|
||||
println(s"========== Reference validation: checking if DynamicDataId='$id' exists for DynamicEntityName='$entityName' ==========")
|
||||
val exists = DynamicData.count(
|
||||
By(DynamicData.DynamicDataId, id),
|
||||
By(DynamicData.DynamicEntityName, entityName)
|
||||
) > 0
|
||||
println(s"========== Reference validation result: exists=$exists ==========")
|
||||
exists
|
||||
}
|
||||
|
||||
override def get(bankId: Option[String],entityName: String, id: String, userId: Option[String], isPersonalEntity: Boolean): Box[DynamicDataT] = {
|
||||
if(bankId.isEmpty && !isPersonalEntity ){ //isPersonalEntity == false, get all the data, no need for specific userId.
|
||||
//forced the empty also to a error here. this is get Dynamic by Id, if it return Empty, better show the error in this level.
|
||||
|
||||
@ -139,7 +139,7 @@ class MappedEntitlement extends Entitlement
|
||||
object mEntitlementId extends MappedUUID(this)
|
||||
object mBankId extends UUIDString(this)
|
||||
object mUserId extends UUIDString(this)
|
||||
object mRoleName extends MappedString(this, 64)
|
||||
object mRoleName extends MappedString(this, 255)
|
||||
object mCreatedByProcess extends MappedString(this, 255)
|
||||
|
||||
object mGroupId extends MappedString(this, 255) {
|
||||
|
||||
@ -87,7 +87,7 @@ class MappedEntitlementRequest extends EntitlementRequest
|
||||
|
||||
object mUserId extends UUIDString(this)
|
||||
|
||||
object mRoleName extends MappedString(this, 64)
|
||||
object mRoleName extends MappedString(this, 255)
|
||||
|
||||
override def entitlementRequestId: String = mEntitlementRequestId.get.toString
|
||||
|
||||
|
||||
@ -85,7 +85,7 @@ class MappedScope extends Scope
|
||||
object mScopeId extends MappedUUID(this)
|
||||
object mBankId extends UUIDString(this)
|
||||
object mConsumerId extends UUIDString(this)
|
||||
object mRoleName extends MappedString(this, 64)
|
||||
object mRoleName extends MappedString(this, 255)
|
||||
|
||||
override def scopeId: String = mScopeId.get.toString
|
||||
override def bankId: String = mBankId.get
|
||||
|
||||
153
obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala
Normal file
153
obp-api/src/test/scala/code/api/v6_0_0/WebUiPropsTest.scala
Normal file
@ -0,0 +1,153 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2019, TESOBE GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole._
|
||||
import code.api.util.ErrorMessages._
|
||||
import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0
|
||||
import code.entitlement.Entitlement
|
||||
import code.webuiprops.WebUiPropsCommons
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.json.Serialization.write
|
||||
import org.scalatest.Tag
|
||||
|
||||
|
||||
class WebUiPropsTest extends V600ServerSetup {
|
||||
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getPermissions":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
|
||||
object ApiEndpoint extends Tag(nameOf(Implementations6_0_0.getWebUiProp))
|
||||
|
||||
val rightEntity = WebUiPropsCommons("webui_api_explorer_url", "https://apiexplorer.openbankproject.com")
|
||||
val anotherEntity = WebUiPropsCommons("webui_api_manager_url", "https://apimanager.openbankproject.com")
|
||||
val wrongEntity = WebUiPropsCommons("hello_api_explorer_url", "https://apiexplorer.openbankproject.com") // name not start with "webui_"
|
||||
|
||||
|
||||
feature("Get Single WebUiProp by Name v6.0.0") {
|
||||
|
||||
scenario("Get WebUiProp - successful case with explicit prop from database", VersionOfApi, ApiEndpoint) {
|
||||
// First create a webui prop
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
|
||||
When("We create a webui prop")
|
||||
val requestCreate = (v6_0_0_Request / "management" / "webui_props").POST <@(user1)
|
||||
val responseCreate = makePostRequest(requestCreate, write(rightEntity))
|
||||
Then("We should get a 201")
|
||||
responseCreate.code should equal(201)
|
||||
|
||||
When("We get the webui prop by name without active flag")
|
||||
val requestGet = (v6_0_0_Request / "webui-props" / rightEntity.name).GET
|
||||
val responseGet = makeGetRequest(requestGet)
|
||||
Then("We should get a 200")
|
||||
responseGet.code should equal(200)
|
||||
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
|
||||
webUiPropJson.name should equal(rightEntity.name)
|
||||
webUiPropJson.value should equal(rightEntity.value)
|
||||
}
|
||||
|
||||
scenario("Get WebUiProp - successful case with active=true returns explicit prop", VersionOfApi, ApiEndpoint) {
|
||||
// First create a webui prop
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
|
||||
When("We create a webui prop")
|
||||
val requestCreate = (v6_0_0_Request / "management" / "webui_props").POST <@(user1)
|
||||
val responseCreate = makePostRequest(requestCreate, write(anotherEntity))
|
||||
Then("We should get a 201")
|
||||
responseCreate.code should equal(201)
|
||||
|
||||
When("We get the webui prop by name with active=true")
|
||||
val requestGet = (v6_0_0_Request / "webui-props" / anotherEntity.name).GET.addQueryParameter("active", "true")
|
||||
val responseGet = makeGetRequest(requestGet)
|
||||
Then("We should get a 200")
|
||||
responseGet.code should equal(200)
|
||||
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
|
||||
webUiPropJson.name should equal(anotherEntity.name)
|
||||
webUiPropJson.value should equal(anotherEntity.value)
|
||||
}
|
||||
|
||||
scenario("Get WebUiProp - not found without active flag", VersionOfApi, ApiEndpoint) {
|
||||
When("We get a non-existent webui prop by name without active flag")
|
||||
val requestGet = (v6_0_0_Request / "webui-props" / "webui_non_existent_prop").GET
|
||||
val responseGet = makeGetRequest(requestGet)
|
||||
Then("We should get a 400")
|
||||
responseGet.code should equal(400)
|
||||
val error = responseGet.body.extract[ErrorMessage]
|
||||
error.message should include(WebUiPropsNotFoundByName)
|
||||
}
|
||||
|
||||
scenario("Get WebUiProp - with active=true returns implicit prop from config", VersionOfApi, ApiEndpoint) {
|
||||
// Test that we can get implicit props from sample.props.template when active=true
|
||||
When("We get a webui prop by name with active=true that exists in config but not in DB")
|
||||
// Use a prop that should exist in sample.props.template like webui_sandbox_introduction
|
||||
val requestGet = (v6_0_0_Request / "webui-props" / "webui_sandbox_introduction").GET.addQueryParameter("active", "true")
|
||||
val responseGet = makeGetRequest(requestGet)
|
||||
Then("We should get a 200 with implicit prop")
|
||||
responseGet.code should equal(200)
|
||||
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
|
||||
webUiPropJson.name should equal("webui_sandbox_introduction")
|
||||
webUiPropJson.webUiPropsId should equal(Some("default"))
|
||||
}
|
||||
|
||||
scenario("Get WebUiProp - invalid active parameter", VersionOfApi, ApiEndpoint) {
|
||||
When("We get a webui prop with invalid active parameter")
|
||||
val requestGet = (v6_0_0_Request / "webui-props" / "webui_api_explorer_url").GET.addQueryParameter("active", "invalid")
|
||||
val responseGet = makeGetRequest(requestGet)
|
||||
Then("We should get a 400")
|
||||
responseGet.code should equal(400)
|
||||
val error = responseGet.body.extract[ErrorMessage]
|
||||
error.message should include(InvalidFilterParameterFormat)
|
||||
}
|
||||
|
||||
scenario("Get WebUiProp - database prop takes precedence over config prop when active=true", VersionOfApi, ApiEndpoint) {
|
||||
// Create a webui prop that overrides a config value
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanCreateWebUiProps.toString)
|
||||
val customValue = WebUiPropsCommons("webui_get_started_text", "Custom Get Started Text")
|
||||
When("We create a webui prop that overrides config")
|
||||
val requestCreate = (v6_0_0_Request / "management" / "webui_props").POST <@(user1)
|
||||
val responseCreate = makePostRequest(requestCreate, write(customValue))
|
||||
Then("We should get a 201")
|
||||
responseCreate.code should equal(201)
|
||||
|
||||
When("We get the webui prop with active=true")
|
||||
val requestGet = (v6_0_0_Request / "webui-props" / customValue.name).GET.addQueryParameter("active", "true")
|
||||
val responseGet = makeGetRequest(requestGet)
|
||||
Then("We should get the database value, not the config value")
|
||||
responseGet.code should equal(200)
|
||||
val webUiPropJson = responseGet.body.extract[WebUiPropsCommons]
|
||||
webUiPropJson.name should equal(customValue.name)
|
||||
webUiPropJson.value should equal(customValue.value)
|
||||
webUiPropJson.webUiPropsId should not equal(Some("default"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -249,6 +249,17 @@ object DynamicEntityFieldType extends OBPEnumeration[DynamicEntityFieldType]{
|
||||
|
||||
override def wrongTypeMsg: String = s"the value's type should be $this, format is $dateFormat."
|
||||
}
|
||||
object json extends Value {
|
||||
val jValueType = classOf[JValue]
|
||||
override def isJValueValid(jValue: JValue): Boolean = {
|
||||
jValue match {
|
||||
case _: JObject => true
|
||||
case _: JArray => true
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
override def wrongTypeMsg: String = "the value's type should be a JSON object or array."
|
||||
}
|
||||
//object array extends Value{val jValueType = classOf[JArray]}
|
||||
//object `object` extends Value{val jValueType = classOf[JObject]} //TODO in the future, we consider support nested type
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user