Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
fr.maif.izanami.datastores.FeaturesDatastore.scala Maven / Gradle / Ivy
package fr.maif.izanami.datastores
import fr.maif.izanami.datastores.featureImplicits.FeatureRow
import fr.maif.izanami.env.Env
import fr.maif.izanami.env.PostgresqlErrors.{
FOREIGN_KEY_VIOLATION,
NOT_NULL_VIOLATION,
RELATION_DOES_NOT_EXISTS,
UNIQUE_VIOLATION
}
import fr.maif.izanami.env.pgimplicits.EnhancedRow
import fr.maif.izanami.errors._
import fr.maif.izanami.events.{EventService, SourceFeatureCreated, SourceFeatureDeleted, SourceFeatureUpdated}
import fr.maif.izanami.models.Feature.{
activationConditionRead,
activationConditionWrite,
legacyActivationConditionRead,
legacyCompatibleConditionWrites
}
import fr.maif.izanami.models._
import fr.maif.izanami.utils.Datastore
import fr.maif.izanami.utils.syntax.implicits.{BetterJsValue, BetterListEither, BetterSyntax}
import fr.maif.izanami.v1.V1FeatureEvents
import fr.maif.izanami.wasm.{WasmConfig, WasmConfigWithFeatures, WasmScriptAssociatedFeatures}
import fr.maif.izanami.web.ImportController.{Fail, ImportConflictStrategy, MergeOverwrite, Skip}
import io.otoroshi.wasm4s.scaladsl.WasmSourceKind
import io.vertx.core.json.{JsonArray, JsonObject}
import io.vertx.core.shareddata.ClusterSerializable
import io.vertx.pgclient.PgException
import io.vertx.sqlclient.{Row, SqlConnection}
import org.postgresql.xml.LegacyInsecurePGXmlFactoryFactory
import play.api.libs.json.{JsArray, JsObject, JsValue, Json}
import java.lang
import java.util.UUID
import scala.collection.immutable
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future
import scala.reflect.ClassTag
class FeaturesDatastore(val env: Env) extends Datastore {
def findActivationStrategiesForFeatureByName(
tenant: String,
name: String,
project: String
): Future[Option[Map[String, LightWeightFeature]]] = {
env.postgresql
.queryOne(
s"""SELECT f.id FROM features f where project=$$1 AND name=$$2""",
List(project, name),
schemas = Set(tenant)
) { r =>
r.optString("id")
}
.flatMap {
case None => None.future
case Some(id) => findActivationStrategiesForFeature(tenant, id)
}
}
def findActivationStrategiesForFeature(
tenant: String,
id: String
): Future[Option[Map[String, LightWeightFeature]]] = {
env.postgresql.queryRaw(
s"""SELECT
| f.id,
| f.name,
| f.enabled,
| f.project,
| f.conditions,
| f.description,
| f.metadata,
| f.script_config as config,
| COALESCE(json_agg(t.id) FILTER(WHERE t.id IS NOT NULL), '[]'::json) as tags,
| COALESCE(
| json_object_agg(
| fcs.context_path, json_build_object(
| 'enabled', fcs.enabled,
| 'conditions', fcs.conditions,
| 'config', fcs.script_config
| )
| ) FILTER(WHERE fcs.enabled IS NOT NULL), '{}'::json
| )
| AS overloads
|FROM features f
|LEFT JOIN feature_contexts_strategies fcs ON fcs.feature = f.name
|LEFT JOIN features_tags ft ON ft.feature = f.id
|LEFT JOIN tags t ON ft.tag = t.name
|WHERE f.id=$$1
|GROUP BY f.id""".stripMargin,
params = List(id),
schemas = Set(tenant)
) { rs =>
{
if (rs.isEmpty) {
None
} else {
rs.head
.optFeature()
.map(feature => {
val overloadByContext: Map[String, LightWeightFeature] = rs
.flatMap(r => {
r
.optJsObject("overloads")
.map(overloads => {
overloads.keys.map(context => {
(overloads \ context)
.asOpt[JsObject]
.flatMap(json => {
(json \ "enabled")
.asOpt[Boolean]
.map(enabled => {
val maybeConditions = (json \ "conditions")
.asOpt[JsArray]
.map(arr => arr.value.map(v => v.as[ActivationCondition]).toSet)
val maybeScriptName = (json \ "config").asOpt[String]
val r: (String, LightWeightFeature) = (
context,
maybeConditions
.map(conditions =>
Feature(
id = feature.id,
name = feature.name,
project = feature.project,
conditions = conditions,
enabled = enabled,
tags = feature.tags,
metadata = feature.metadata,
description = feature.description
)
)
.orElse(
maybeScriptName.map(scriptConfig =>
LightWeightWasmFeature(
id = feature.id,
name = feature.name,
project = feature.project,
wasmConfigName = scriptConfig,
enabled = enabled,
tags = feature.tags,
metadata = feature.metadata,
description = feature.description
)
)
)
.getOrElse(throw new RuntimeException("Bad feature format in DB"))
)
r
})
})
})
})
.getOrElse(Set())
})
.flatMap(_.toSeq)
.toMap
overloadByContext + ("" -> feature)
})
}
}
}
}
// TODO deduplicate
def findActivationStrategiesForFeatures(
tenant: String,
ids: Set[String]
): Future[Map[String, Map[String, LightWeightFeature]]] = {
env.postgresql
.queryAll(
s"""SELECT
| f.id,
| f.name,
| f.enabled,
| f.project,
| f.conditions,
| f.description,
| f.metadata,
| f.script_config as config,
| COALESCE(json_agg(t.id) FILTER(WHERE t.id IS NOT NULL), '[]'::json) as tags,
| COALESCE(
| json_object_agg(
| fcs.context_path, json_build_object(
| 'enabled', fcs.enabled,
| 'conditions', fcs.conditions
| )
| ) FILTER(WHERE fcs.enabled IS NOT NULL), '{}'::json
| )
| AS overloads
|FROM features f
|LEFT JOIN feature_contexts_strategies fcs ON fcs.feature = f.name
|LEFT JOIN features_tags ft ON ft.feature = f.id
|LEFT JOIN tags t ON ft.tag = t.name
|WHERE f.id=ANY($$1)
|GROUP BY f.id""".stripMargin,
params = List(ids.toArray),
schemas = Set(tenant)
) { r =>
{
val maybeTuple = r
.optFeature()
.map(feature => {
val overloadByContext: Map[String, LightWeightFeature] = r
.optJsObject("overloads")
.map(overloads => {
overloads.keys.map(context => {
(overloads \ context)
.asOpt[JsObject]
.flatMap(json => {
(json \ "enabled")
.asOpt[Boolean]
.map(enabled => {
val maybeConditions = (json \ "conditions")
.asOpt[JsArray]
.map(arr => arr.value.map(v => v.as[ActivationCondition]).toSet)
val maybeScriptName = (json \ "config").asOpt[String]
val r: (String, LightWeightFeature) = (
context,
maybeConditions
.map(conditions =>
Feature(
id = feature.id,
name = feature.name,
project = feature.project,
conditions = conditions,
enabled = enabled,
tags = feature.tags,
metadata = feature.metadata,
description = feature.description
)
)
.orElse(
maybeScriptName.map(scriptName =>
LightWeightWasmFeature(
id = feature.id,
name = feature.name,
project = feature.project,
wasmConfigName = scriptName,
enabled = enabled,
tags = feature.tags,
metadata = feature.metadata,
description = feature.description
)
)
)
.getOrElse(throw new RuntimeException("Bad feature format in DB"))
)
r
})
})
})
})
.getOrElse(Set())
.flatMap(_.toSeq)
.toMap
(feature.id, overloadByContext + ("" -> feature))
})
maybeTuple
}
}
.map(l => l.toMap)
}
def findFeatureMatching(
tenant: String,
pattern: String,
clientId: String,
count: Int,
page: Int
): Future[(Int, Seq[CompleteFeature])] = {
val countQuery = env.postgresql.queryOne(
s"""
|select count(f.id) as count
|from features f
|left join apikeys a on a.clientid=$$1
|left join apikeys_projects ap on (ap.project=f.project and ap.apikey=a.name)
|where f.id LIKE $$2
|and (f.conditions is null or f.conditions is json object)
|and (ap.project is not null or a.admin=true)
|""".stripMargin,
List(clientId, pattern.replaceAll("\\*", "%")),
schemas = Set(tenant)
) { r => r.optInt("count") }
val dataQuery = env.postgresql.queryAll(
s"""select f.*, s.config AS wasm, COALESCE(json_agg(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags
|from features f
|left join features_tags ft
|on ft.feature = f.id
|left join wasm_script_configurations s
|on s.id = f.script_config
|left join apikeys a on a.clientid=$$1
|left join apikeys_projects ap on (ap.project=f.project and ap.apikey=a.name)
|where f.id LIKE $$2
|and (f.conditions is null or f.conditions is json object)
|and (ap.project is not null or a.admin=true)
|group by f.id, wasm
|order by f.id
|limit $$3
|offset $$4""".stripMargin,
List(clientId, pattern.replaceAll("\\*", "%"), Integer.valueOf(count), Integer.valueOf((page - 1) * count)),
schemas = Set(tenant)
) { r => r.optCompleteFeature() }
for (
count <- countQuery;
features <- dataQuery
) yield {
(count.getOrElse(features.size), features)
}
}
def applyPatch(tenant: String, operations: Seq[FeaturePatch], user: String): Future[Unit] = {
val featureToUpdateIds = operations.collect {
case p: EnabledFeaturePatch => p.id
case p: ProjectFeaturePatch => p.id
}.toSet
findActivationStrategiesForFeatures(tenant, featureToUpdateIds)
.flatMap(oldFeatures => {
env.postgresql.executeInTransaction(
implicit conn => {
Future
.sequence(operations.map {
case EnabledFeaturePatch(value, id) => {
env.postgresql
.queryOne(
s"""UPDATE features SET enabled=$$1 WHERE id=$$2 RETURNING id, name, project, enabled""",
List(java.lang.Boolean.valueOf(value), id),
conn = Some(conn)
) { r =>
for (
id <- r.optString("id");
name <- r.optString("name");
project <- r.optString("project");
enabled <- r.optBoolean("enabled")
) yield (id, name, project, enabled)
}
.flatMap {
case Some((id, name, project, enabled)) =>
env.eventService.emitEvent(
channel = tenant,
event = SourceFeatureUpdated(
id = id,
project = project,
tenant = tenant,
user = user,
previous = FeatureWithOverloads(oldFeatures(id)),
feature = FeatureWithOverloads(oldFeatures(id)).setEnabling(value)
)
)
case None => Future.successful(())
}
}
case ProjectFeaturePatch(value, id) => {
env.postgresql
.queryOne(
s"""UPDATE features SET project=$$1 WHERE id=$$2 RETURNING id, name, project, enabled""",
List(value, id),
conn = Some(conn)
) { r =>
for (
id <- r.optString("id");
name <- r.optString("name");
project <- r.optString("project");
enabled <- r.optBoolean("enabled")
) yield (id, name, project, enabled)
}
.flatMap {
case Some((id, name, project, enabled)) =>
env.eventService.emitEvent(
channel = tenant,
event = SourceFeatureUpdated(
id = id,
project = project,
tenant = tenant,
user = user,
previous = FeatureWithOverloads(oldFeatures(id)),
feature = FeatureWithOverloads(oldFeatures(id)).setProject(value)
)
)(conn)
case None => Future.successful(())
}
}
case TagsFeaturePatch(value, id) =>
findById(tenant, id).flatMap {
case Right(Some(oldFeature)) =>
env.datastores.tags
.readTags(tenant, value)
.flatMap {
case tags if tags.size < value.size => {
val existingTagNames = tags.map(_.name).toSet
val tagsToCreate = value.diff(existingTagNames)
env.datastores.tags.createTags(
tagsToCreate.map(tag => TagCreationRequest(name = tag)).toList,
tenant,
conn = Some(conn)
)
}
case tags => Right(tags).toFuture
}
.flatMap(_ => {
env.postgresql
.queryOne(
s"""delete from features_tags where feature=$$1""",
List(id),
conn = Some(conn)
) { _ => Some(id) }
.flatMap(maybeId => insertIntoFeatureTags(tenant, id, value, Some(conn)))
})
case Left(err) => Future.successful(Left(err))
case Right(None) => Future.successful(Left(FeatureDoesNotExist(name = id)))
}
case RemoveFeaturePatch(id) => {
env.postgresql
.queryOne(
s"""DELETE FROM features WHERE id=$$1 RETURNING id, name, project, enabled""",
List(id),
conn = Some(conn)
) { r =>
for (
id <- r.optString("id");
name <- r.optString("name");
project <- r.optString("project");
enabled <- r.optBoolean("enabled")
) yield (id, name, project, enabled)
}
.flatMap {
case Some((id, name, project, enabled)) =>
env.eventService.emitEvent(
channel = tenant,
event = SourceFeatureDeleted(id = id, project = project, tenant = tenant, user = user)
)(conn)
case None => Future.successful(())
}
}
})
.map(_ => ())
},
schemas = Set(tenant)
)
})
}
def findByIdForKey(
tenant: String,
id: String,
contexts: Seq[String],
clientId: String,
clientSecret: String
): Future[Option[CompleteFeature]] = {
val possibleContextPaths = contexts
.foldLeft(Seq(): Seq[Seq[String]])((acc, next) => {
val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next))
acc.appended(newElement)
})
.map(_.mkString("_"))
val needContexts = contexts.nonEmpty
val params = if (needContexts) List(clientId, id, possibleContextPaths.toArray) else List(clientId, id)
env.postgresql
.queryAll(
s"""
|SELECT
| k.clientsecret,
| ${if (needContexts) s"fcs.context_path," else "null as context_path,"}
| json_build_object(
| 'name', f.name,
| 'project', f.project,
| 'description', f.description,
| 'id', f.id)::jsonb ||
| ${if (needContexts) s"""(CASE
| WHEN fcs.enabled IS NOT NULL THEN
| json_build_object(
| 'enabled', fcs.enabled,
| 'config', ow.config,
| 'conditions', fcs.conditions
| )::jsonb
| ELSE""" else ""}
| json_build_object(
| 'enabled', f.enabled,
| 'config', w.config,
| 'conditions', f.conditions
| )::jsonb
| ${if (needContexts) s"END)" else ""} as feature
|FROM features f
|${if (needContexts)
s"LEFT JOIN feature_contexts_strategies fcs ON fcs.feature=f.name AND fcs.context_path = ANY($$3) LEFT JOIN wasm_script_configurations ow ON fcs.script_config=ow.id"
else ""}
|INNER JOIN apikeys k ON (k.clientid=$$1 AND k.enabled=true)
|LEFT JOIN wasm_script_configurations w ON w.id=f.script_config
|LEFT JOIN apikeys_projects kp ON (kp.apikey=k.name AND kp.project=f.project)
|WHERE f.id=$$2
|AND (kp.apikey IS NOT NULL OR k.admin=TRUE)
|""".stripMargin,
params,
schemas = Set(tenant)
) { r =>
{
for (
_ <- r.optString("clientsecret")
.filter(hashed => clientSecret == hashed); // TODO put this check in the above query
jsonFeature <- r.optJsObject("feature");
js <- (jsonFeature \ "config")
.asOpt[JsValue]
.map(js => jsonFeature.as[JsObject] + ("wasmConfig" -> js))
.orElse(Some(jsonFeature));
feature <- Feature.readCompleteFeature(js).asOpt
) yield (r.optString("context_path"), feature)
}
}
.map(ls =>
ls.sortWith((f1, f2) => {
(f1, f2) match {
case ((None, _), _) => false
case (_, (None, _)) => true
case ((Some(ctx1), _), (Some(ctx2), _)) if ctx1.length > ctx2.length => true
case _ => false
}
}).headOption
.map(t => t._2)
)
}
def searchFeature(tenant: String, tags: Set[String]): Future[Seq[AbstractFeature]] = {
val hasTags = tags.nonEmpty
env.postgresql
.queryOne(
s"""
|select COALESCE(
| json_agg(row_to_json(f.*)::jsonb
| || (json_build_object('tags', (
| array(
| SELECT ft.tag
| FROM features_tags ft
| WHERE ft.feature = f.id
| GROUP BY ft.tag
| )
| ), 'wasmConfig', (
| select w.config FROM wasm_script_configurations w where w.id = f.script_config
| )))::jsonb)
| FILTER (WHERE f.id IS NOT NULL), '[]'
|) as "features"
|from features f${if (hasTags) {
s""", features_tags ft
|WHERE ft.feature = f.id
|AND ft.tag = ANY($$1)"""
} else ""}
|""".stripMargin,
if (hasTags) List(tags.toArray) else List(),
schemas = Set(tenant)
) { r =>
r.optJsArray("features")
.map(arr => arr.value.toSeq.map(js => Feature.readFeature(js).asOpt).flatMap(_.toSeq))
}
.map(o => o.getOrElse(Seq()))
}
def readScriptConfig(tenant: String, path: String): Future[Option[WasmConfig]] = {
env.postgresql
.queryOne(
s"""
|SELECT config
|FROM wasm_script_configurations
|WHERE config #>> '{source,path}' = $$1
|""".stripMargin,
List(path),
schemas = Set(tenant)
) { row => { row.optJsObject("config") } }
.map(o => o.map(jsObj => jsObj.as[WasmConfig](WasmConfig.format)))
}
def findFeaturesProjects(tenant: String, ids: Set[String]): Future[Seq[String]] = {
env.postgresql.queryAll(
s"""
|SELECT DISTINCT project FROM features WHERE id=ANY($$1)
|""".stripMargin,
List(ids.toArray),
schemas = Set(tenant)
) { r => r.optString("project") }
}
def findById(tenant: String, id: String): Future[Either[IzanamiError, Option[CompleteFeature]]] = {
env.postgresql
.queryOne(
s"""select f.*, s.config AS wasm, COALESCE(json_agg(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags
|from features f
|left join features_tags ft
|on ft.feature = f.id
|left join wasm_script_configurations s
|on s.id = f.script_config
|where f.id = $$1
|group by f.id, wasm""".stripMargin,
List(id),
schemas = Set(tenant)
) { row => row.optCompleteFeature() }
.map(o => Right(o))
.recover {
case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant))
case _ => Left(InternalServerError())
}
}
def findByIdForKeyWithoutCheck(
tenant: String,
id: String,
clientId: String
): Future[Either[IzanamiError, Option[CompleteFeature]]] = {
env.postgresql
.queryOne(
s"""select (ap.project IS NOT NULL OR k.admin=TRUE) AS authorized, f.*, s.config AS wasm, COALESCE(json_agg(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags
|from features f
|left join features_tags ft
|on ft.feature = f.id
|left join wasm_script_configurations s
|on s.id = f.script_config
|inner join apikeys k
|on k.clientid=$$2
|left join apikeys_projects ap
|on (ap.apikey=k.name AND ap.project=f.project)
|where f.id = $$1
|group by f.id, k.admin, wasm, ap.project""".stripMargin,
List(id, clientId),
schemas = Set(tenant)
) { row =>
{
row
.optBoolean("authorized")
.map(authorized => {
if (authorized) {
row.optCompleteFeature().toRight(InternalServerError())
} else {
Left(NotEnoughRights())
}
})
}
}
.map {
case Some(Right(feature)) => Right(Some(feature))
case Some(Left(error)) => Left(error)
case None => Right(None)
}
.recover {
case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant))
case _ => Left(InternalServerError())
}
}
def doFindByRequestForKey(
tenant: String,
request: FeatureRequest,
clientId: String,
clientSecret: String,
conditions: Boolean
): Future[Either[IzanamiError, Map[UUID, Map[String, Iterable[(Option[String], CompleteFeature)]]]]] = {
val possibleContextPaths = request.context
.foldLeft(Seq(): Seq[Seq[String]])((acc, next) => {
val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next))
acc.appended(newElement)
})
.map(_.mkString("_"))
val needTags = request.allTagsIn.nonEmpty || request.noTagIn.nonEmpty || request.oneTagIn.nonEmpty;
val needContexts = request.context.nonEmpty || conditions
val params = if (needContexts && !conditions) {
List(clientId, clientSecret, request.projects.toArray, request.features.toArray, possibleContextPaths.toArray)
} else {
List(clientId, clientSecret, request.projects.toArray, request.features.toArray)
}
env.postgresql
.queryAll(
s"""
|SELECT
| f.id,
| p.id as pid,
| f.enabled,
| f.name,
| f.project,
| f.conditions,
| f.description,
| f.script_config,
| f.metadata,
| w.config as wasm
| ${if (needContexts) """,
| COALESCE(json_object_agg(fcs.context_path, json_build_object(
| 'id', f.id,
| 'name', f.name,
| 'project', f.project,
| 'description', f.description,
| 'enabled', fcs.enabled,
| 'conditions', fcs.conditions,
| 'context', fcs.context,
| 'wasmConfig', ow.config,
| 'context_path', fcs.context_path)) FILTER(WHERE fcs.enabled IS NOT NULL), '{}'::json) AS overloads
| """ else ""}
| ${if (needTags) ",COALESCE(json_agg(t.id) FILTER(WHERE t.id IS NOT NULL), '[]') as tags" else ""}
| FROM projects p
| LEFT JOIN features f on f.project = p.name
| ${if (needTags) """
| LEFT JOIN features_tags ft ON f.id=ft.feature
| LEFT JOIN tags t ON t.name=ft.tag""" else ""}
| ${if (needContexts) s"""
| LEFT JOIN feature_contexts_strategies fcs ON fcs.feature=f.name ${if (!conditions)
s"AND fcs.context_path = ANY($$5)"
else ""}
| LEFT JOIN wasm_script_configurations ow ON fcs.script_config=ow.id""".stripMargin
else ""}
| LEFT JOIN wasm_script_configurations w ON w.id=f.script_config
| INNER JOIN apikeys k ON (k.clientid=$$1 AND k.clientsecret=$$2 AND k.enabled=true)
| LEFT JOIN apikeys_projects kp ON (kp.apikey=k.name AND kp.project=p.name)
| WHERE (f.project = p.name OR f.name IS NULL)
| AND (kp.apikey IS NOT NULL OR k.admin=TRUE)
| AND (p.id=ANY($$3) OR f.id=ANY($$4))
| GROUP BY f.id, pid, w.config
|""".stripMargin,
params,
schemas = Set(tenant)
) { r =>
{
r.optCompleteFeature()
.filter(f => {
if (needTags) {
val tags = f.tags.map(t => UUID.fromString(t))
val specificFeatureRequest = request.features.contains(f.id)
val allTagsInOk = request.allTagsIn.subsetOf(tags)
val oneTagInOk = request.oneTagIn.isEmpty || request.oneTagIn.exists(u => tags.contains(u))
val noTagsInOk = !request.noTagIn.exists(u => tags.contains(u))
specificFeatureRequest || (allTagsInOk && oneTagInOk && noTagsInOk)
} else {
true
}
})
.flatMap(f => {
if (needContexts) {
r.optJsObject("overloads")
.map(jsObject => {
val objByContext = jsObject.as[Map[String, JsObject]]
val overloadByPath: Map[Option[String], CompleteFeature] = objByContext
.map { case (ctx, jsObject) => (ctx, Feature.readCompleteFeature(jsObject).asOpt) }
.filter {
case (_, None) => false
case _ => true
}
.map { case (ctx, optionF) => (Some(ctx), optionF.get) }
(r.optUUID("pid").get, (f.id, overloadByPath + (None -> f)))
})
} else {
Some((r.optUUID("pid").get, (f.id, Map(None -> f))))
}
})
}
}
.map(l => {
val featureByProjects = l.groupBy(t => t._1).map { case (k, v) => (k, v.map(t => t._2).toMap) }
Right(featureByProjects)
})
.recover {
case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(InvalidCredentials())
case _ => Left(InternalServerError())
}
}
def findByRequestForKey(
tenant: String,
request: FeatureRequest,
clientId: String,
clientSecret: String
): Future[Either[IzanamiError, Map[UUID, Seq[CompleteFeature]]]] = {
doFindByRequestForKey(
tenant,
request,
clientId,
clientSecret,
conditions = false
).map {
case Left(err) => Left(err)
case Right(l) => {
Right(l.map {
case (projectId, featuresById) => {
(
projectId,
featuresById.map {
case (id, featuresWithContext) => {
featuresWithContext.toSeq
.sortWith {
case ((firstContext, feature), (secondContext, feature2)) => {
(firstContext, secondContext) match {
case (None, _) => false
case (_, None) => true
case (Some(ctx1), Some(ctx2)) if ctx1.length > ctx2.length => true
case _ => false
}
}
}
.head
._2
}
}.toSeq
)
}
})
}
}
}
def findByRequestV2(
tenant: String,
request: FeatureRequest,
contexts: Seq[String],
user: String
): Future[Map[UUID, Seq[CompleteFeature]]] = {
val possibleContextPaths = contexts
.foldLeft(Seq(): Seq[Seq[String]])((acc, next) => {
val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next))
acc.appended(newElement)
})
.map(seq => seq.mkString("_"))
env.postgresql
.queryAll(
s"""
|WITH filtered_features AS (
| SELECT
| f.id,
| p.id as pid,
| f.enabled,
| f.name,
| f.project,
| f.conditions,
| f.script_config,
| f.description,
| w.config,
| fcs.enabled as overload_enabled,
| fcs.conditions as overload_conditions,
| fcs.context as overload_context,
| ow.config as overload_config,
| fcs.context_path,
| COALESCE(json_agg(t.id) FILTER(WHERE t.id IS NOT NULL), '[]'::json) as tags
| FROM
| izanami.sessions s
| JOIN izanami.users u ON s.username=u.username
| LEFT JOIN izanami.users_tenants_rights utr ON utr.username=u.username
| LEFT JOIN users_projects_rights upr ON upr.username=s.username,
| projects p
| LEFT JOIN features f on f.project = p.name
| LEFT JOIN features_tags ft ON f.id=ft.feature
| LEFT JOIN tags t ON t.name=ft.tag
| LEFT JOIN feature_contexts_strategies fcs ON fcs.feature=f.name AND fcs.context_path = ANY($$4)
| LEFT JOIN wasm_script_configurations ow ON fcs.script_config=ow.id
| LEFT JOIN wasm_script_configurations w ON w.id=f.script_config
| WHERE s.username=$$1
| AND (f.project = p.name OR f.name IS NULL)
| AND (
| (upr.project=p.name AND upr.username=s.username)
| OR (u.admin OR utr.level = 'ADMIN')
| )
| AND (p.id=ANY($$2) OR f.id=ANY($$3))
| GROUP BY f.id, pid, w.config, ow.config, fcs.enabled, fcs.conditions, fcs.context, fcs.context_path
|) SELECT filtered_features.pid AS project_id,
| COALESCE(json_agg(json_build_object(
| 'name', filtered_features.name,
| 'project', filtered_features.project,
| 'tags', filtered_features.tags,
| 'context', filtered_features.context_path,
| 'description', filtered_features.description,
| 'id', filtered_features.id)::jsonb ||
| (CASE
| WHEN filtered_features.overload_enabled IS NOT NULL THEN
| json_build_object(
| 'enabled', filtered_features.overload_enabled,
| 'config', filtered_features.overload_config,
| 'conditions', filtered_features.overload_conditions
| )::jsonb
| ELSE
| json_build_object(
| 'enabled', filtered_features.enabled,
| 'config', filtered_features.config,
| 'conditions', filtered_features.conditions
| )::jsonb
| END)
| ) FILTER(WHERE filtered_features.name IS NOT NULL), '[]'::json) as features
|FROM filtered_features
|GROUP BY filtered_features.pid;
|""".stripMargin,
List(
user,
request.projects.toArray,
request.features.toArray,
possibleContextPaths.toArray
),
schemas = Set(tenant)
) { r =>
{
r.optUUID("project_id")
.map(p => {
val tuple: (UUID, Seq[CompleteFeature]) = (
p,
r.optJsArray("features")
.toSeq
.flatMap(maybeArray => maybeArray.value)
.groupBy(jsObj => (jsObj \ "id").as[String])
.values
.map(featureDuplicates =>
featureDuplicates
.sortWith((first, second) => {
def contextSize(jsValue: JsValue): Int =
(jsValue \ "context").asOpt[String].map(_.split("_")).map(_.length).getOrElse(0)
val firstContextSize = contextSize(first)
val secondContextSize = contextSize(second)
if (firstContextSize == 0) true
else if (secondContextSize == 0) false
else if (firstContextSize < secondContextSize) false
else true
})
.head
)
.flatMap(f => {
Feature
.readCompleteFeature(
(f \ "config").asOpt[JsValue].map(js => f.as[JsObject] + ("wasmConfig" -> js)).getOrElse(f)
)
.asOpt
.toSeq
})
.filter(f =>
request.features.contains(f.id) || request.allTagsIn.subsetOf(f.tags.map(UUID.fromString))
)
.filter(f =>
request.features.contains(f.id) || request.oneTagIn.isEmpty || request.oneTagIn
.exists(u => f.tags.contains(u.toString))
)
.filter(f =>
request.features.contains(f.id) || !request.noTagIn.exists(u => f.tags.contains(u.toString))
)
.toSeq
)
tuple
})
}
}
.map(_.toMap)
}
def createFeaturesAndProjects(
tenant: String,
features: Iterable[CompleteFeature],
conflictStrategy: ImportConflictStrategy,
user: String,
conn: Option[SqlConnection]
): Future[Either[List[IzanamiError], Unit]] = {
// TODO return seq[Error] instead of a single one
if (features.isEmpty) {
Future.successful(Right(()))
} else {
def callback(conn: SqlConnection): Future[Either[List[IzanamiError], Unit]] = {
env.datastores.projects
.createProjects(tenant, features.map(_.project).toSet, conflictStrategy, user, conn = conn.some)
.flatMap {
case Left(error) => Future.successful(Left(List(error)))
case _ => createBulk(tenant, features, conflictStrategy, conn, user)
}
}
conn.map(callback).getOrElse(env.postgresql.executeInTransaction(conn => callback(conn)))
}
}
def createBulk(
tenant: String,
features: Iterable[CompleteFeature],
conflictStrategy: ImportConflictStrategy,
conn: SqlConnection,
user: String
): Future[Either[List[IzanamiError], Unit]] = {
def insertFeatures[T <: ClusterSerializable](
params: (
Array[String],
Array[String],
Array[String],
Array[java.lang.Boolean],
Array[T],
Array[Object],
Array[String]
)
): Future[Either[InternalServerError, List[(String, String)]]] = {
env.postgresql
.queryAll(
s"""INSERT INTO features (id, name, project, enabled, conditions, metadata, description)
|VALUES (unnest($$1::text[]), unnest($$2::text[]), unnest($$3::text[]), unnest($$4::boolean[]), unnest($$5::jsonb[]), unnest($$6::jsonb[]), unnest($$7::text[]))
${conflictStrategy match {
case Fail => ""
case Skip => " ON CONFLICT DO NOTHING"
case MergeOverwrite =>
""" ON CONFLICT (name, project) DO UPDATE SET id=excluded.id, name=excluded.name, project=excluded.project, enabled=excluded.enabled, conditions=excluded.conditions, metadata=excluded.metadata, description=excluded.description, script_config=null
|""".stripMargin
}}
returning id, project""".stripMargin,
params.productIterator.toList.map(a => a.asInstanceOf[AnyRef]),
conn = Some(conn),
schemas = Set(tenant)
) { row =>
for (
id <- row.optString("id");
project <- row.optString("project")
) yield (id, project)
}
.map(ls => Right(ls))
.recover { case ex =>
logger.error("Failed to insert feature", ex)
Left(InternalServerError())
}
}
val wasmConfigs = features
.map {
case Feature(_, _, _, _, _, _, _, _) => None
case CompleteWasmFeature(_, _, _, _, wasmConfig, _, _, _) => Some(wasmConfig)
case s: SingleConditionFeature => None
}
.flatMap(o => o.toList)
def unzip7[A: ClassTag, B: ClassTag, C: ClassTag, D: ClassTag, E: ClassTag, F: ClassTag, G: ClassTag](
l: Iterable[(A, B, C, D, E, F, G)]
): (Array[A], Array[B], Array[C], Array[D], Array[E], Array[F], Array[G]) = {
l.foldLeft(Tuple7(Array[A](), Array[B](), Array[C](), Array[D](), Array[E](), Array[F](), Array[G]())) {
case (res, (e1, e2, e3, e4, e5, e6, e7)) =>
(
res._1.appended(e1),
res._2.appended(e2),
res._3.appended(e3),
res._4.appended(e4),
res._5.appended(e5),
res._6.appended(e6),
res._7.appended(e7)
)
}
}
val (modernFeatures, wasmFeatures, legacyFeatures): (
ArrayBuffer[Feature],
ArrayBuffer[CompleteWasmFeature],
ArrayBuffer[SingleConditionFeature]
) = (ArrayBuffer(), ArrayBuffer(), ArrayBuffer())
features.foreach {
case f @ Feature(_, _, _, _, _, _, _, _) =>
modernFeatures.addOne(f)
case wf @ CompleteWasmFeature(_, _, _, _, _, _, _, _) =>
wasmFeatures.addOne(wf)
case s: SingleConditionFeature =>
legacyFeatures.addOne(s)
}
val legacyFeatureParams = unzip7(legacyFeatures.map {
case SingleConditionFeature(id, name, project, conditions, enabled, tags, metadata, description) =>
(
Option(id).getOrElse(UUID.randomUUID().toString),
name,
project,
java.lang.Boolean.valueOf(enabled),
new JsonObject(Json.toJson(conditions).toString()),
metadata.vertxJsValue,
description
)
})
val modernFeatureParams = unzip7(
modernFeatures.map { case Feature(id, name, project, conditions, enabled, tags, metadata, description) =>
(
Option(id).getOrElse(UUID.randomUUID().toString),
name,
project,
java.lang.Boolean.valueOf(enabled),
new JsonArray(Json.toJson(conditions).toString()),
metadata.vertxJsValue,
description
)
}
)
val wasmFeatureParams = unzip7(
wasmFeatures.map {
case CompleteWasmFeature(id, name, project, enabled, wasmConfig, tags, metadata, description) =>
(
Option(id).getOrElse(UUID.randomUUID().toString),
name,
project,
java.lang.Boolean.valueOf(enabled),
wasmConfig.name,
metadata.vertxJsValue,
description
)
}
)
createWasmScripts(tenant, wasmConfigs.toList, conflictStrategy, conn.some)
.flatMap {
case Left(err) => Left(List(err)).future
case Right(_) => {
Future
.sequence(
List(
insertFeatures(modernFeatureParams),
insertFeatures(legacyFeatureParams),
env.postgresql
.queryAll(
s"""INSERT INTO features (id, name, project, enabled, script_config, metadata, description)
|VALUES (unnest($$1::TEXT[]), unnest($$2::TEXT[]), unnest($$3::TEXT[]), unnest($$4::BOOLEAN[]), unnest($$5::TEXT[]), unnest($$6::JSONB[]), unnest($$7::TEXT[]))
|returning id, project""".stripMargin,
wasmFeatureParams.productIterator.toList.map(a => a.asInstanceOf[AnyRef]),
conn = conn.some,
schemas = Set(tenant)
) { row =>
for (
id <- row.optString("id");
project <- row.optString("project")
) yield (id, project)
}
.map(ls => Right(ls))
.recover {
case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS =>
Left(TenantDoesNotExists(tenant))
case ex =>
logger.error("Failed to insert feature", ex)
Left(InternalServerError())
}
)
)
.map(eithers => eithers.toEitherList.map(l => l.flatten))
}
}
.flatMap {
case Left(errors) => Future.successful(Left(errors))
case Right(ids) =>
Future
.sequence(features.map(f => insertIntoFeatureTags(tenant, f.id, f.tags, conn.some)))
.map(eithers => eithers.toEitherList.map(_ => features))
}
.flatMap {
case Left(errors) => Future.successful(Left(errors))
case Right(features) => {
Future
.sequence(
features.map(f =>
env.eventService.emitEvent(
channel = tenant,
event = SourceFeatureCreated(
id = f.id,
project = f.project,
tenant = tenant,
user = user,
feature = FeatureWithOverloads(f.toLightWeightFeature)
)
)(conn)
)
)
.map(_ => Right(()))
}
}
}
def create(
tenant: String,
project: String,
feature: CompleteFeature,
user: String
): Future[Either[IzanamiError, String]] = {
env.postgresql.executeInTransaction(
implicit conn => doCreate(tenant, project, feature, conn, user),
schemas = Set(tenant)
)
}
private def doCreate(
tenant: String,
project: String,
feature: CompleteFeature,
conn: SqlConnection,
user: String
): Future[Either[IzanamiError, String]] = {
(feature match {
case Feature(_, _, _, _, _, _, _, _) => Future(Right(()))
case CompleteWasmFeature(_, _, _, _, wasmConfig, _, _, _) =>
createWasmScriptIfNeeded(tenant, wasmConfig, conn = Some(conn))
case s: SingleConditionFeature => Future(Right(()))
}).flatMap {
case Left(err) => Left(err).future
case Right(_) => {
insertFeature(tenant, project, feature, user)(conn)
.flatMap(eitherId => {
eitherId.fold(
err => Future.successful(Left(err)),
id => insertIntoFeatureTags(tenant, id, feature.tags, Some(conn)).map(either => either.map(_ => id))
)
})
}
}
}
def readLocalScripts(tenant: String): Future[Seq[WasmConfig]] = {
env.postgresql.queryAll(
s"""
|SELECT config FROM wasm_script_configurations
|""".stripMargin,
List(),
schemas = Set(tenant)
) { r => r.optJsObject("config").map(js => js.as(WasmConfig.format)) }
}
def deleteLocalScript(tenant: String, name: String): Future[Either[IzanamiError, Unit]] = {
env.postgresql
.queryOne(
s"""
|DELETE FROM wasm_script_configurations WHERE id=$$1
|""".stripMargin,
List(name),
schemas = Set(tenant)
) { r => Some(()) }
.map(_ => Right(()))
.recover {
case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => Left(FeatureDependsOnThisScript())
}
}
def readLocalScriptsWithAssociatedFeatures(tenant: String): Future[Seq[WasmConfigWithFeatures]] = {
env.postgresql.queryAll(
s"""
|SELECT c.config, json_agg(json_build_object('id', features.id, 'name', features.name, 'project', features.project)) as features
|FROM wasm_script_configurations c
|LEFT JOIN features ON features.script_config=c.id
|GROUP BY c.config
|""".stripMargin,
List(),
schemas = Set(tenant)
) { r =>
{
r.optJsObject("config")
.map(js => js.as(WasmConfig.format))
.flatMap(config => {
r.optJsArray("features")
.map(arr => {
val features = arr.value
.map(jsValue => {
for {
name <- (jsValue \ "name").asOpt[String]
id <- (jsValue \ "id").asOpt[String]
project <- (jsValue \ "project").asOpt[String]
} yield WasmScriptAssociatedFeatures(name = name, project = project, id = id)
})
.filter(o => o.isDefined)
.map(o => o.get)
.toSeq
WasmConfigWithFeatures(wasmConfig = config, features = features)
})
})
}
}
}
def readAllLocalScripts(): Future[Seq[WasmConfig]] = {
env.datastores.tenants
.readTenants()
.flatMap(tenants => {
Future.sequence(tenants.map(tenant => {
env.postgresql.queryAll(
s"""
|SELECT config FROM "${tenant.name}".wasm_script_configurations
|""".stripMargin,
List()
) { r => r.optJsObject("config").map(js => js.as(WasmConfig.format)) }
}))
})
.map(os => os.flatten)
}
def createWasmScriptIfNeeded(
tenant: String,
wasmConfig: WasmConfig,
conn: Option[SqlConnection]
): Future[Either[IzanamiError, String]] = {
wasmConfig.source.kind match {
case WasmSourceKind.Unknown => throw new RuntimeException("Unknown wasm script")
case WasmSourceKind.Local => Right(wasmConfig.source.path).future
case _ =>
env.postgresql
.queryOne(
s"""INSERT INTO wasm_script_configurations (id, config) VALUES ($$1,$$2) RETURNING id""",
List(wasmConfig.name, Json.toJson(wasmConfig)(WasmConfig.format).vertxJsValue),
conn = conn,
schemas = Set(tenant)
) { row => row.optString("id") }
.map(o => o.toRight(InternalServerError()))
.recover {
case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION =>
Left(WasmScriptAlreadyExists(wasmConfig.source.path))
}
.flatMap(either => {
// TODO this should be elsewhere
wasmConfig.source.getWasm()(env.wasmIntegration.context, env.executionContext).map(_ => either)
})
}
}
def readWasmScript(tenant: String, name: String): Future[Option[WasmConfig]] = {
env.postgresql.queryOne(
s"""
|SELECT config
|FROM wasm_script_configurations
|WHERE id=$$1
|""".stripMargin,
List(name),
schemas = Set(tenant)
) { r => r.optJsObject("config").map(js => js.as(WasmConfig.format)) }
}
def createWasmScripts(
tenant: String,
wasmConfigs: List[WasmConfig],
conflictStrategy: ImportConflictStrategy,
conn: Option[SqlConnection]
): Future[Either[IzanamiError, Set[String]]] = {
if (wasmConfigs.isEmpty) {
Future.successful(Right(Set()))
} else {
val (ids, scripts) = wasmConfigs
.filter(w => w.source.kind != WasmSourceKind.Local && w.source.kind != WasmSourceKind.Unknown)
.map(w => (w.name, Json.toJson(w)(WasmConfig.format).vertxJsValue))
.unzip
val localScriptIds = wasmConfigs.filter(w => w.source.kind == WasmSourceKind.Local).map(w => w.name)
env.postgresql
.queryRaw(
s"""
|INSERT INTO wasm_script_configurations(id, config)
|VALUES (unnest($$1::TEXT[]), unnest($$2::JSONB[]))
|${conflictStrategy match {
case Fail => ""
case MergeOverwrite =>
"""
|ON CONFLICT(id) DO UPDATE SET config = excluded.config
|""".stripMargin
case Skip => " ON CONFLICT(id) DO NOTHING "
}}
|returning id
|""".stripMargin,
List(ids.toArray, scripts.toArray),
schemas = Set(tenant),
conn = conn
) { rs => rs.flatMap(_.optString("id")).toSet }
.map(ids => {
ids.foreach(id =>
wasmConfigs.find(w => w.name == id).get.source.getWasm()(env.wasmIntegration.context, env.executionContext)
)
Right(ids.concat(localScriptIds))
})
.recover {
case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION =>
Left(WasmScriptAlreadyExists("")) // TODO specify script name
}
}
}
def updateWasmScript(
tenant: String,
script: String,
wasmConfig: WasmConfig
): Future[Unit] = {
env.postgresql
.queryOne(
s"""UPDATE wasm_script_configurations SET id=$$1, config=$$2 WHERE id=$$3 RETURNING id""",
List(
wasmConfig.name,
wasmConfig.json.vertxJsValue,
script
),
schemas = Set(tenant)
) { row => row.optString("id") }
.map(o => ())
}
private def insertFeature(
tenant: String,
project: String,
feature: CompleteFeature,
user: String,
importConflictStrategy: ImportConflictStrategy = Fail
)(implicit
conn: SqlConnection
): Future[Either[IzanamiError, String]] = {
val (request, params) = feature match {
case SingleConditionFeature(id, name, project, conditions, enabled, _, metadata, description) =>
(
s"""INSERT INTO features (id, name, project, enabled, conditions, metadata, description)
|VALUES ($$1, $$2, $$3, $$4, $$5, $$6, $$7)
|returning id""".stripMargin,
List(
Option(id).getOrElse(UUID.randomUUID().toString),
name,
project,
java.lang.Boolean.valueOf(enabled),
new JsonObject(Json.toJson(conditions).toString()),
metadata.vertxJsValue,
description
)
)
case Feature(id, name, project, conditions, enabled, _, metadata, description) =>
(
s"""INSERT INTO features (id, name, project, enabled, conditions, metadata, description)
|VALUES ($$1, $$2, $$3, $$4, $$5, $$6, $$7)
|returning id""".stripMargin,
List(
Option(id).getOrElse(UUID.randomUUID().toString),
name,
project,
java.lang.Boolean.valueOf(enabled),
new JsonArray(Json.toJson(conditions).toString()),
metadata.vertxJsValue,
description
)
)
case CompleteWasmFeature(id, name, project, enabled, config, _, metadata, description) =>
(
s"""INSERT INTO features (id, name, project, enabled, script_config, metadata, description)
|VALUES ($$1, $$2, $$3, $$4, $$5, $$6, $$7)
|returning id""".stripMargin,
List(
Option(id).getOrElse(UUID.randomUUID().toString),
name,
project,
java.lang.Boolean.valueOf(enabled),
config.name,
metadata.vertxJsValue,
description
)
)
}
env.postgresql
.queryOne(
request,
params,
conn = Some(conn),
schemas = Set(tenant)
) { row => row.optString("id") }
.map(_.toRight(InternalServerError()))
.recover {
case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => Left(ProjectDoesNotExists(project))
case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant))
case ex =>
logger.error("Failed to insert feature", ex)
Left(InternalServerError())
}
.flatMap {
case Left(error) => Future.successful(Left(error))
case Right(id) =>
env.eventService
.emitEvent(
channel = tenant,
event = SourceFeatureCreated(
id = id,
project = project,
tenant = tenant,
user = user,
feature = FeatureWithOverloads(feature.toLightWeightFeature)
)
)(conn)
.map(_ => Right(id))
}
}
def update(
tenant: String,
id: String,
feature: CompleteFeature,
user: String
): Future[Either[IzanamiError, String]] = {
// TODO allow updating metadata
findActivationStrategiesForFeature(tenant = tenant, id = id)
.map(o => o.map(f => FeatureWithOverloads(f)))
.flatMap {
case None => Future.successful(Left(FeatureDoesNotExist(id)))
case Some(oldFeature) => {
env.postgresql.executeInTransaction(
conn => {
val (request, params) = feature match {
case SingleConditionFeature(id, name, project, conditions, enabled, tags, metadata, description) =>
(
s"""update features
|SET name=$$1, enabled=$$2, conditions=$$3, script_config=NULL, description=$$5, project=$$6 WHERE id=$$4 returning id""".stripMargin,
List(
name,
java.lang.Boolean.valueOf(enabled),
new JsonObject(Json.toJson(conditions).toString()),
id,
description,
project
)
)
case Feature(_, name, project, conditions, enabled, _, _, description) =>
(
s"""update features
|SET name=$$1, enabled=$$2, conditions=$$3, script_config=NULL, description=$$5, project=$$6 WHERE id=$$4 returning id""".stripMargin,
List(
name,
java.lang.Boolean.valueOf(enabled),
new JsonArray(Json.toJson(conditions).toString()),
id,
description,
project
)
)
case CompleteWasmFeature(_, name, project, enabled, wasmConfig, _, _, description) =>
(
s"""update features
|SET name=$$1, enabled=$$2, script_config=$$4, conditions=NULL, description=$$5, project=$$6 WHERE id=$$3 returning id""".stripMargin,
List(
name,
java.lang.Boolean.valueOf(enabled),
id,
wasmConfig.name,
description,
project
)
)
}
(feature match {
case feat @ CompleteWasmFeature(_, _, _, _, wasmConfig, _, _, _)
if wasmConfig.source.kind != WasmSourceKind.Local =>
createWasmScriptIfNeeded(tenant, wasmConfig, Some(conn))
case _ => Future(())
})
.flatMap(_ =>
env.postgresql.queryRaw(
s"""
|DELETE FROM feature_contexts_strategies fc USING features f
|WHERE fc.feature=f.name
|AND fc.project=f.project
|AND f.id=$$1
|AND f.project != $$2
|AND fc.local_context IS NOT NULL
|""".stripMargin,
List(id, feature.project),
conn = Some(conn)
) { _ => Some(()) }
)
.flatMap(_ =>
env.postgresql
.queryOne(
request,
params,
conn = Some(conn)
) { row => row.optString("id") }
.map(maybeId => maybeId.toRight(InternalServerError()))
.recover {
case f: PgException if f.getSqlState == NOT_NULL_VIOLATION => Left(MissingFeatureFields())
case _ => Left(InternalServerError())
}
.flatMap(either => {
either.fold(
err => Future.successful(Left(err)),
id => {
env.postgresql
.queryOne(
s"""delete from features_tags where feature=$$1""",
List(id),
conn = Some(conn)
) { _ => Some(id) }
.flatMap(_ =>
insertIntoFeatureTags(tenant, id, feature.tags, Some(conn))
.map(either => either.map(_ => id))
)
}
)
})
)
.flatMap {
case Right(_) if !oldFeature.baseFeature().hasSameActivationStrategy(feature) =>
env.eventService
.emitEvent(
channel = tenant,
event = SourceFeatureUpdated(
id = id,
project = feature.project,
tenant = tenant,
user = user,
previous = oldFeature,
feature = oldFeature.setFeature(feature.toLightWeightFeature)
)
)(conn)
.map(_ => Right(id))
case Right(_) => Future.successful(Right(id))
case Left(err) => Future.successful(Left(err))
}
},
schemas = Set(tenant)
)
}
}
}
def insertIntoFeatureTags(
tenant: String,
id: String,
tags: Set[String],
conn: Option[SqlConnection]
): Future[Either[IzanamiError, Unit]] = {
if (tags.isEmpty) {
Future.successful(Right(()))
} else {
env.postgresql
.queryOne(
s"""
|INSERT INTO features_tags (feature, tag)
|VALUES ($$1, unnest($$2::TEXT[])) returning *""".stripMargin,
List(id, tags.toArray),
conn = conn,
schemas = Set(tenant)
) { _ => Some(()) }
.map { _.toRight(InternalServerError()) }
.recover {
case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant))
case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION =>
Left(TagDoesNotExists(tags.map(t => t).mkString(",")))
case ex =>
logger.error("Failed to update feature/tag mapping table", ex)
Left(InternalServerError())
}
}
}
def delete(tenant: String, id: String, user: String): Future[Either[IzanamiError, String]] = {
env.postgresql.executeInTransaction(conn =>
env.postgresql
.queryOne(
s"""DELETE FROM features WHERE id=$$1 returning id, project""",
List(id),
schemas = Set(tenant),
conn = Some(conn)
) { row =>
for (
id <- row.optString("id");
project <- row.optString("project")
) yield (id, project)
}
.map { _.toRight(InternalServerError()) }
.recover {
case ex: PgException if ex.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant))
case _ => Left(InternalServerError())
}
.flatMap {
case l @ Left(err) => Future.successful(Left(err))
case Right((id, project)) =>
env.eventService
.emitEvent(
channel = tenant,
event = SourceFeatureDeleted(id = id, project = project, tenant = tenant, user = user)
)(conn)
.map(_ => Right(id))
}
)
}
}
object featureImplicits {
implicit class FeatureRow(val row: Row) extends AnyVal {
// TODO deduplicate with below
def optCompleteFeature(): Option[CompleteFeature] = {
val tags =
row.optJsArray("tags").map(array => array.value.map(v => v.as[String]).toSet).getOrElse(Set())
val maybeClassicalConditions = row
.optJsArray("contextual_conditions")
.orElse(row.optJsArray("conditions"))
.map(arr => arr.value.map(v => v.as[ActivationCondition]).toSet)
lazy val maybeLegacyConditions = row
.optJsObject("contextual_conditions")
.orElse(row.optJsObject("conditions"))
.map(v => v.as[LegacyCompatibleCondition])
lazy val maybeWasmConfig = row
.optJsObject("contextual_wasm")
.orElse(row.optJsObject("wasm"))
.map(jsObject => jsObject.as[WasmConfig](WasmConfig.format))
for (
name <- row.optString("name");
id <- row.optString("id");
description <- row.optString("description");
project <- row.optString("project");
enabled <- row.optBoolean("contextual_enabled").orElse(row.optBoolean("enabled"));
metadata <- row.optJsObject("metadata")
)
yield (maybeClassicalConditions, maybeLegacyConditions, maybeWasmConfig) match {
case (Some(classicalConditions), _, _) => {
Feature(
id = id,
name = name,
project = project,
enabled = enabled,
conditions = classicalConditions,
metadata = metadata,
tags = tags,
description = description
)
}
case (_, Some(legacyCompatibleCondition), _) => {
SingleConditionFeature(
id = id,
name = name,
project = project,
enabled = enabled,
condition = legacyCompatibleCondition,
metadata = metadata,
tags = tags,
description = description
)
}
case (_, _, Some(wasmConfig)) => {
CompleteWasmFeature(
id = id,
name = name,
project = project,
enabled = enabled,
wasmConfig = wasmConfig,
metadata = metadata,
tags = tags,
description = description
)
}
case _ => throw new RuntimeException("Failed to read feature " + id)
}
}
def optFeature(): Option[LightWeightFeature] = {
val tags =
row.optJsArray("tags").map(array => array.value.map(v => v.as[String]).toSet).getOrElse(Set())
val maybeClassicalConditions = row
.optJsArray("contextual_conditions")
.orElse(row.optJsArray("conditions"))
.map(arr => arr.value.map(v => v.as[ActivationCondition]).toSet)
lazy val maybeLegacyConditions = row
.optJsObject("contextual_conditions")
.orElse(row.optJsObject("conditions"))
.map(v => v.as[LegacyCompatibleCondition])
lazy val maybeWasmName = row.optString("config")
for (
name <- row.optString("name");
id <- row.optString("id");
description <- row.optString("description");
project <- row.optString("project");
enabled <- row.optBoolean("contextual_enabled").orElse(row.optBoolean("enabled"));
metadata <- row.optJsObject("metadata")
)
yield (maybeClassicalConditions, maybeLegacyConditions, maybeWasmName) match {
case (Some(classicalConditions), _, _) => {
Feature(
id = id,
name = name,
project = project,
enabled = enabled,
conditions = classicalConditions,
metadata = metadata,
tags = tags,
description = description
)
}
case (_, Some(legacyCompatibleCondition), _) => {
SingleConditionFeature(
id = id,
name = name,
project = project,
enabled = enabled,
condition = legacyCompatibleCondition,
metadata = metadata,
tags = tags,
description = description
)
}
case (_, _, Some(wasmConfig)) => {
LightWeightWasmFeature(
id = id,
name = name,
project = project,
enabled = enabled,
wasmConfigName = wasmConfig,
metadata = metadata,
tags = tags,
description = description
)
}
case _ => throw new RuntimeException("Failed to read feature " + id)
}
}
}
}