-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Searchbar for one or more tenant. Delete project search bar from tenant page. --------- Co-authored-by: HelaKaraa <[email protected]> Co-authored-by: baudelotphilippe <[email protected]>
- Loading branch information
1 parent
c321007
commit 2f09781
Showing
29 changed files
with
1,045 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package fr.maif.izanami.datastores | ||
|
||
import fr.maif.izanami.env.Env | ||
import fr.maif.izanami.env.pgimplicits.EnhancedRow | ||
import fr.maif.izanami.utils.Datastore | ||
import play.api.libs.json.{JsObject} | ||
|
||
import scala.concurrent.Future | ||
|
||
class SearchDatastore(val env: Env) extends Datastore { | ||
def tenantSearch(tenant: String, username: String, query: String): Future[List[(String, JsObject, Double)]] = { | ||
env.postgresql.queryAll( | ||
s""" | ||
|WITH scored_projects AS ( | ||
| SELECT | ||
| p.name, | ||
| p.description, | ||
| izanami.SIMILARITY(p.name, $$1) as name_score, | ||
| izanami.SIMILARITY(p.description, $$1) as description_score | ||
| FROM projects p | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| LEFT JOIN users_projects_rights upr ON (utr.username=$$2 AND p.name=upr.project) | ||
| WHERE utr.level='ADMIN' | ||
| OR upr.level is not null | ||
| OR u.admin=true | ||
|), scored_features AS ( | ||
| SELECT | ||
| f.project, | ||
| f.name, | ||
| f.description, | ||
| izanami.SIMILARITY(f.name, $$1) as name_score, | ||
| izanami.SIMILARITY(f.description, $$1) as description_score | ||
| FROM scored_projects p, features f | ||
| WHERE f.project=p.name | ||
|), scored_keys AS ( | ||
| SELECT | ||
| k.name, | ||
| k.description, | ||
| izanami.SIMILARITY(k.name, $$1) as name_score, | ||
| izanami.SIMILARITY(k.description, $$1) as description_score | ||
| FROM apikeys k | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| LEFT JOIN users_keys_rights ukr ON (utr.username=$$2 AND k.name=ukr.apikey) | ||
| WHERE utr.level='ADMIN' | ||
| OR ukr.level is not null | ||
| OR u.admin=true | ||
|), scored_tags AS ( | ||
| SELECT | ||
| t.name, | ||
| t.description, | ||
| izanami.SIMILARITY(t.name, $$1) as name_score, | ||
| izanami.SIMILARITY(t.description, $$1) as description_score | ||
| FROM tags t | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| WHERE utr.level IS NOT NULL | ||
| OR u.admin=true | ||
|), scored_scripts AS ( | ||
| SELECT | ||
| s.id as name, | ||
| izanami.SIMILARITY(s.id, $$1) as name_score | ||
| FROM wasm_script_configurations s | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| WHERE utr.level IS NOT NULL | ||
| OR u.admin=true | ||
| ), scored_global_contexts AS ( | ||
| SELECT | ||
| c.parent, | ||
| c.name as name, | ||
| izanami.SIMILARITY(c.name, $$1) as name_score | ||
| FROM global_feature_contexts c | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| WHERE utr.level IS NOT NULL | ||
| OR u.admin=true | ||
| ), scored_local_contexts AS ( | ||
| SELECT | ||
| c.parent, | ||
| c.global_parent, | ||
| c.project, | ||
| c.name as name, | ||
| izanami.SIMILARITY(c.name, $$1) as name_score | ||
| FROM feature_contexts c | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| WHERE utr.level IS NOT NULL | ||
| OR u.admin=true | ||
| ), scored_webhooks AS ( | ||
| SELECT | ||
| w.name, | ||
| w.description, | ||
| izanami.SIMILARITY(w.name, $$1) as name_score, | ||
| izanami.SIMILARITY(w.description, $$1) as description_score | ||
| FROM webhooks w | ||
| LEFT JOIN izanami.users u ON u.username=$$2 | ||
| LEFT JOIN izanami.users_tenants_rights utr ON (utr.username=$$2 AND utr.tenant=$$3) | ||
| LEFT JOIN users_webhooks_rights uwr ON (utr.username=$$2 AND w.name=uwr.webhook) | ||
| WHERE utr.level='ADMIN' | ||
| OR uwr.level is not null | ||
| OR u.admin=true | ||
| ) | ||
|SELECT row_to_json(f.*) as json, GREATEST(f.name_score, f.description_score) AS match_score, 'feature' as _type, $$3 as tenant | ||
|FROM scored_features f | ||
|WHERE f.name_score > 0.2 OR f.description_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(p.*) as json, GREATEST(p.name_score, p.description_score) AS match_score, 'project' as _type, $$3 as tenant | ||
|FROM scored_projects p | ||
|WHERE p.name_score > 0.2 OR p.description_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(k.*) as json, GREATEST(k.name_score, k.description_score) AS match_score, 'key' as _type, $$3 as tenant | ||
|FROM scored_keys k | ||
|WHERE k.name_score > 0.2 OR k.description_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(t.*) as json, GREATEST(t.name_score, t.description_score) AS match_score, 'tag' as _type, $$3 as tenant | ||
|FROM scored_tags t | ||
|WHERE t.name_score > 0.2 OR t.description_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(s.*) as json, s.name_score AS match_score, 'script' as _type, $$3 as tenant | ||
|FROM scored_scripts s | ||
|WHERE s.name_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(gc.*) as json, gc.name_score AS match_score, 'global_context' as _type, $$3 as tenant | ||
|FROM scored_global_contexts gc | ||
|WHERE gc.name_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(lc.*) as json, lc.name_score AS match_score, 'local_context' as _type, $$3 as tenant | ||
|FROM scored_local_contexts lc | ||
|WHERE lc.name_score > 0.2 | ||
|UNION ALL | ||
|SELECT row_to_json(w.*) as json, GREATEST(w.name_score, w.description_score) AS match_score, 'webhook' as _type, $$3 as tenant | ||
|FROM scored_webhooks w | ||
|WHERE w.name_score > 0.2 OR w.description_score > 0.2 | ||
|ORDER BY match_score DESC LIMIT 10 | ||
|""".stripMargin, | ||
List(query, username, tenant), | ||
schemas = Set(tenant) | ||
) { r => | ||
{ | ||
for ( | ||
t <- r.optString("_type"); | ||
json <- r.optJsObject("json"); | ||
score <- r.optDouble("match_score") | ||
) yield { | ||
(t, json, score) | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
package fr.maif.izanami.web | ||
|
||
import fr.maif.izanami.env.Env | ||
import fr.maif.izanami.utils.syntax.implicits.{BetterJsValue, BetterSyntax} | ||
import fr.maif.izanami.web.PathElement.pathElementWrite | ||
import play.api.libs.json.{JsObject, Json, Writes} | ||
import play.api.mvc._ | ||
|
||
import scala.annotation.tailrec | ||
import scala.concurrent.{ExecutionContext, Future} | ||
|
||
class SearchController( | ||
val env: Env, | ||
val controllerComponents: ControllerComponents, | ||
val simpleAuthAction: AuthenticatedAction, | ||
val tenantRightAction: TenantRightsAction | ||
) extends BaseController { | ||
implicit val ec: ExecutionContext = env.executionContext | ||
|
||
def search(query: String): Action[AnyContent] = tenantRightAction.async { | ||
implicit request: UserRequestWithTenantRights[AnyContent] => | ||
{ | ||
val tenants = request.user.tenantRights.keySet | ||
Future | ||
.sequence( | ||
tenants | ||
.map(tenant => | ||
env.datastores.search | ||
.tenantSearch(tenant, request.user.username, query) | ||
.map(l => | ||
l.map(t => { | ||
(t._1, t._2, t._3, tenant) | ||
}) | ||
) | ||
) | ||
) | ||
.map(l => l.flatten.toList.sortBy(_._3)(Ordering.Double.TotalOrdering.reverse).take(10)) | ||
.flatMap(results => { | ||
Future.sequence(results.map { case (rowType, rowJson, _, tenant) => | ||
buildPath(rowType, rowJson, tenant) | ||
.map(pathElements => { | ||
val name = (rowJson \ "name").asOpt[String].getOrElse("") | ||
val jsonPath = | ||
Json.toJson(pathElements.prepended(TenantPathElement(tenant)))(Writes.seq(pathElementWrite)) | ||
Json.obj( | ||
"type" -> rowType, | ||
"name" -> name, | ||
"path" -> jsonPath, | ||
"tenant" -> tenant | ||
) | ||
}) | ||
}) | ||
}) | ||
.map(r => Ok(Json.toJson(r))) | ||
} | ||
} | ||
|
||
def searchForTenant(tenant: String, query: String): Action[AnyContent] = simpleAuthAction.async { | ||
implicit request: UserNameRequest[AnyContent] => | ||
{ | ||
env.datastores.search | ||
.tenantSearch(tenant, request.user, query) | ||
.flatMap(results => { | ||
Future.sequence(results.map { case (rowType, rowJson, _) => | ||
buildPath(rowType, rowJson, tenant) | ||
.map(pathElements => { | ||
val jsonPath = Json.toJson(pathElements)(Writes.seq(pathElementWrite)) | ||
val name = (rowJson \ "name").asOpt[String].getOrElse("") | ||
Json.obj( | ||
"type" -> rowType, | ||
"name" -> name, | ||
"path" -> jsonPath, | ||
"tenant" -> tenant | ||
) | ||
}) | ||
}) | ||
}) | ||
.map(res => Ok(Json.toJson(res))) | ||
} | ||
} | ||
|
||
def buildPath(rowType: String, rowJson: JsObject, tenant: String): Future[Seq[PathElement]] = { | ||
rowType match { | ||
case "feature" => Seq(ProjectPathElement((rowJson \ "project").as[String])).future | ||
case "global_context" => { | ||
(rowJson \ "parent") | ||
.asOpt[String] | ||
.map(parent => { | ||
parent.split("_").toSeq.drop(1).map(ctx => GlobalContextPathElement(ctx)) | ||
}) | ||
.getOrElse(Seq.empty) | ||
.future | ||
} | ||
case "local_context" => { | ||
(rowJson \ "global_parent") | ||
.asOpt[String] | ||
.map(parent => { | ||
val parts = parent.split("_").toSeq | ||
val project = (rowJson \ "project").as[String] | ||
parts | ||
.drop(1) | ||
.map(ctx => GlobalContextPathElement(ctx)) | ||
.appended(ProjectPathElement(project)) | ||
.future | ||
}) | ||
.orElse( | ||
(rowJson \ "parent") | ||
.asOpt[String] | ||
.map(parent => { | ||
val parts = parent.split("_") | ||
val project = parts.head | ||
|
||
// We look for shortes local context parent, since before him all contexts will be global | ||
env.datastores.featureContext | ||
.findLocalContexts(tenant, generateParentCandidates(parts.drop(1)).map(s => s"${project}_${s}")) | ||
.map(ctxs => { | ||
val parentLocalContext = ctxs.sortBy(_.length).headOption | ||
val parts: Seq[PathElement] = parentLocalContext | ||
.map(lc => { | ||
val shortestLocalContextParts = lc.split("_") | ||
val parentLocalContextName = shortestLocalContextParts.last | ||
val globalContextParts = | ||
generateParentCandidates(shortestLocalContextParts.dropRight(1).drop(1)) | ||
.map(name => GlobalContextPathElement(name)) | ||
.toSeq | ||
.appended(ProjectPathElement(project)) | ||
|
||
val localContextParts = parent | ||
.replace(lc, "") | ||
.split("_") | ||
.filter(_.nonEmpty) | ||
.map(str => LocalContextPathElement(str)) | ||
.toSeq | ||
.prepended(LocalContextPathElement(parentLocalContextName)) | ||
|
||
globalContextParts.concat(localContextParts) | ||
}) | ||
.getOrElse(Seq.empty) | ||
parts | ||
}) | ||
}) | ||
) | ||
.getOrElse(Future.successful(Seq())) | ||
} | ||
case _ => Seq.empty.future | ||
} | ||
} | ||
|
||
private def generateParentCandidates(parentParts: Seq[String]): Set[String] = { | ||
@tailrec | ||
def act(current: Seq[String], res: Seq[String], previousItems: Seq[String]): Seq[String] = { | ||
val newRes = if (previousItems.nonEmpty) res.appended(previousItems.mkString("_")) else res | ||
if (current.isEmpty) { | ||
newRes | ||
} else { | ||
act(current.drop(1), newRes, previousItems.appended(current.head)) | ||
} | ||
} | ||
|
||
act(parentParts, Seq(), Seq()).toSet | ||
} | ||
} | ||
|
||
sealed trait PathElement { | ||
def pathElementType: String | ||
def name: String | ||
} | ||
case class ProjectPathElement(name: String) extends PathElement { | ||
override def pathElementType: String = "project" | ||
} | ||
case class GlobalContextPathElement(name: String) extends PathElement { | ||
override def pathElementType: String = "global_context" | ||
} | ||
case class LocalContextPathElement(name: String) extends PathElement { | ||
override def pathElementType: String = "local_context" | ||
} | ||
case class TenantPathElement(name: String) extends PathElement { | ||
override def pathElementType: String = "tenant" | ||
} | ||
|
||
object PathElement { | ||
val pathElementWrite: Writes[PathElement] = { p => | ||
Json.obj( | ||
"type" -> p.pathElementType, | ||
"name" -> p.name | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.