diff --git a/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala b/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala
index 73e473ba7a4..12c742a1e38 100644
--- a/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala
+++ b/amber/src/main/scala/org/apache/texera/web/TexeraWebApplication.scala
@@ -39,11 +39,6 @@ import org.apache.texera.web.resource.dashboard.admin.execution.AdminExecutionRe
import org.apache.texera.web.resource.dashboard.admin.user.AdminUserResource
import org.apache.texera.web.resource.dashboard.hub.HubResource
import org.apache.texera.web.resource.dashboard.user.UserResource
-import org.apache.texera.web.resource.dashboard.user.project.{
- ProjectAccessResource,
- ProjectResource,
- PublicProjectResource
-}
import org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource
import org.apache.texera.web.resource.dashboard.user.workflow.{
WorkflowAccessResource,
@@ -145,14 +140,11 @@ class TexeraWebApplication
environment.jersey.register(classOf[UserConfigResource])
environment.jersey.register(classOf[FeedbackResource])
environment.jersey.register(classOf[AdminUserResource])
- environment.jersey.register(classOf[PublicProjectResource])
environment.jersey.register(classOf[WorkflowAccessResource])
environment.jersey.register(classOf[WorkflowResource])
environment.jersey.register(classOf[HubResource])
environment.jersey.register(classOf[UserResource])
environment.jersey.register(classOf[WorkflowVersionResource])
- environment.jersey.register(classOf[ProjectResource])
- environment.jersey.register(classOf[ProjectAccessResource])
environment.jersey.register(classOf[WorkflowExecutionsResource])
environment.jersey.register(classOf[DashboardResource])
environment.jersey.register(classOf[GmailResource])
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
index 3d78eef3035..b3808f7cd08 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DashboardResource.scala
@@ -38,7 +38,6 @@ object DashboardResource {
case class DashboardClickableFileEntry(
resourceType: String,
workflow: Option[DashboardWorkflow] = None,
- project: Option[Project] = None,
dataset: Option[DashboardDataset] = None
)
@@ -54,7 +53,7 @@ object DashboardResource {
The following class describe the available params from the frontend for full text search.
* @param user The authenticated user performing the search.
* @param keywords A list of search keywords. The API will return resources that match any of these keywords.
- * @param resourceType The type of the resources to include in the search results. Acceptable values are "workflow", "project", "file" and "" (for all types).
+ * @param resourceType The type of the resources to include in the search results. Acceptable values are "workflow", "file" and "" (for all types).
* @param creationStartDate The start of the date range for the creation time filter. It should be provided in 'yyyy-MM-dd' format.
* @param creationEndDate The end of the date range for the creation time filter. It should be provided in 'yyyy-MM-dd' format.
* @param modifiedStartDate The start of the date range for the modification time filter. It should be provided in 'yyyy-MM-dd' format.
@@ -62,7 +61,6 @@ object DashboardResource {
* @param owners A list of owner names to include in the search results.
* @param workflowIDs A list of workflow IDs to include in the search results.
* @param operators A list of operators to include in the search results.
- * @param projectIds A list of project IDs to include in the search results.
* @param offset The number of initial results to skip. This is useful for implementing pagination.
* @param count The maximum number of results to return.
* @param orderBy The order in which to sort the results. Acceptable values are 'NameAsc', 'NameDesc', 'CreateTimeDesc', and 'EditTimeDesc'.
@@ -77,7 +75,6 @@ object DashboardResource {
@QueryParam("owner") owners: java.util.List[String] = new util.ArrayList(),
@QueryParam("id") workflowIDs: java.util.List[Integer] = new util.ArrayList(),
@QueryParam("operator") operators: java.util.List[String] = new util.ArrayList(),
- @QueryParam("projectId") projectIds: java.util.List[Integer] = new util.ArrayList(),
@QueryParam("datasetId") datasetIds: java.util.List[Integer] = new util.ArrayList(),
@QueryParam("start") @DefaultValue("0") offset: Int = 0,
@QueryParam("count") @DefaultValue("20") count: Int = 20,
@@ -95,15 +92,12 @@ object DashboardResource {
val query = params.resourceType match {
case SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE =>
WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic)
- case SearchQueryBuilder.PROJECT_RESOURCE_TYPE =>
- ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic)
case SearchQueryBuilder.DATASET_RESOURCE_TYPE =>
DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic)
case SearchQueryBuilder.ALL_RESOURCE_TYPE =>
val q1 = WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic)
- val q3 = ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic)
val q4 = DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic)
- q1.unionAll(q3).unionAll(q4)
+ q1.unionAll(q4)
case _ => throw new IllegalArgumentException(s"Unknown resource type: ${params.resourceType}")
}
@@ -118,8 +112,6 @@ object DashboardResource {
resourceType match {
case SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE =>
WorkflowSearchQueryBuilder.toEntry(uid, record)
- case SearchQueryBuilder.PROJECT_RESOURCE_TYPE =>
- ProjectSearchQueryBuilder.toEntry(uid, record)
case SearchQueryBuilder.DATASET_RESOURCE_TYPE =>
DatasetSearchQueryBuilder.toEntry(uid, record)
}
@@ -180,10 +172,10 @@ object DashboardResource {
class DashboardResource {
/**
- * This method performs a full-text search across all resources - workflows, projects, and files -
+ * This method performs a full-text search across all resources - workflows and datasets -
* that match the specified keywords.
* It supports advanced filters such as resource type, creation and modification dates, owner,
- * workflow IDs, operators, project IDs and allows to specify the number of results and their ordering.
+ * workflow IDs and operators, and allows to specify the number of results and their ordering.
*
* This method utilizes MySQL Boolean Full-Text Searches
* reference: https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala
deleted file mode 100644
index 5e247f87172..00000000000
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilder.scala
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard
-
-import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS}
-import org.apache.texera.dao.jooq.generated.tables.pojos.Project
-import org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry
-import org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils.{
- getContainsFilter,
- getDateFilter,
- getFullTextSearchFilter
-}
-import org.jooq.impl.DSL
-import org.jooq.{Condition, GroupField, Record, TableLike}
-
-import scala.jdk.CollectionConverters.CollectionHasAsScala
-
-object ProjectSearchQueryBuilder extends SearchQueryBuilder {
-
- override val mappedResourceSchema: UnifiedResourceSchema = UnifiedResourceSchema(
- resourceType = DSL.inline(SearchQueryBuilder.PROJECT_RESOURCE_TYPE),
- name = PROJECT.NAME,
- description = PROJECT.DESCRIPTION,
- creationTime = PROJECT.CREATION_TIME,
- lastModifiedTime = PROJECT.CREATION_TIME,
- pid = PROJECT.PID,
- ownerId = PROJECT.OWNER_ID,
- projectColor = PROJECT.COLOR
- )
-
- override protected def constructFromClause(
- uid: Integer,
- params: DashboardResource.SearchQueryParams,
- includePublic: Boolean = false
- ): TableLike[_] = {
- PROJECT
- .leftJoin(PROJECT_USER_ACCESS)
- .on(PROJECT_USER_ACCESS.PID.eq(PROJECT.PID))
- .where(PROJECT_USER_ACCESS.UID.eq(uid))
- }
-
- override protected def constructWhereClause(
- uid: Integer,
- params: DashboardResource.SearchQueryParams
- ): Condition = {
- val splitKeywords = params.keywords.asScala
- .flatMap(_.split("[+\\-()<>~*@\"]"))
- .filter(_.nonEmpty)
- .toSeq
-
- getDateFilter(
- params.creationStartDate,
- params.creationEndDate,
- PROJECT.CREATION_TIME
- )
- .and(getContainsFilter(params.projectIds, PROJECT.PID))
- .and(
- getFullTextSearchFilter(splitKeywords, List(PROJECT.NAME, PROJECT.DESCRIPTION))
- )
- }
-
- override protected def getGroupByFields: Seq[GroupField] = Seq.empty
-
- override def toEntryImpl(
- uid: Integer,
- record: Record
- ): DashboardResource.DashboardClickableFileEntry = {
- val dp = record.into(PROJECT).into(classOf[Project])
- DashboardClickableFileEntry(SearchQueryBuilder.PROJECT_RESOURCE_TYPE, project = Some(dp))
- }
-}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
index e19755e6116..ce2e1cd34d8 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
@@ -35,7 +35,6 @@ object SearchQueryBuilder {
.createDSLContext()
val FILE_RESOURCE_TYPE = "file"
val WORKFLOW_RESOURCE_TYPE = "workflow"
- val PROJECT_RESOURCE_TYPE = "project"
val DATASET_RESOURCE_TYPE = "dataset"
val ALL_RESOURCE_TYPE = ""
}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
index aa3681ae1fb..d1cb9aabe7c 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
@@ -63,13 +63,9 @@ object UnifiedResourceSchema {
ownerId: Field[Integer] = DSL.cast(null, classOf[Integer]),
wid: Field[Integer] = DSL.cast(null, classOf[Integer]),
workflowUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum]),
- projectsOfWorkflow: Field[String] = DSL.inline(""),
uid: Field[Integer] = DSL.cast(null, classOf[Integer]),
userName: Field[String] = DSL.inline(""),
userEmail: Field[String] = DSL.inline(""),
- pid: Field[Integer] = DSL.cast(null, classOf[Integer]),
- projectOwnerId: Field[Integer] = DSL.cast(null, classOf[Integer]),
- projectColor: Field[String] = DSL.inline(""),
did: Field[Integer] = DSL.cast(null, classOf[Integer]),
datasetStoragePath: Field[String] = DSL.cast(null, classOf[String]),
repositoryName: Field[String] = DSL.inline(""),
@@ -90,13 +86,9 @@ object UnifiedResourceSchema {
ownerId -> ownerId.as(resourceOwnerIdAlias),
wid -> wid.as("wid"),
workflowUserAccess -> workflowUserAccess.as("workflow_privilege"),
- projectsOfWorkflow -> projectsOfWorkflow.as("projects"),
uid -> uid.as("uid"),
userName -> userName.as("userName"),
userEmail -> userEmail.as("email"),
- pid -> pid.as("pid"),
- projectOwnerId -> projectOwnerId.as("owner_uid"),
- projectColor -> projectColor.as("color"),
did -> did.as("did"),
datasetStoragePath -> datasetStoragePath.as("dataset_storage_path"),
repositoryName -> repositoryName.as("repository_name"),
@@ -114,7 +106,7 @@ object UnifiedResourceSchema {
* Refer to /sql/texera_ddl.sql to understand what each attribute is
*
* Attributes common across all resource types:
- * - `resourceType`: The type of the resource (e.g., project, workflow, file) as a `String`.
+ * - `resourceType`: The type of the resource (e.g., workflow, dataset) as a `String`.
* - `name`: The name of the resource as a `String`.
* - `description`: A textual description of the resource as a `String`.
* - `creationTime`: The timestamp when the resource was created, as a `Timestamp`.
@@ -124,16 +116,10 @@ object UnifiedResourceSchema {
* Attributes specific to workflows:
* - `wid`: Workflow ID, as an `Integer`.
* - `workflowUserAccess`: Access privileges associated with the workflow, as a `PrivilegeEnum`.
- * - `projectsOfWorkflow`: IDs of projects associated with the workflow, concatenated as a `String`.
* - `uid`: User ID associated with the workflow, as an `Integer`.
* - `userName`: Name of the user associated with the workflow, as a `String`.
* - `userEmail`: Email of the user associated with the workflow, as a `String`.
*
- * Attributes specific to projects:
- * - `pid`: Project ID, as an `Integer`.
- * - `projectOwnerId`: ID of the project owner, as an `Integer`.
- * - `projectColor`: Color associated with the project, as a `String`.
- *
* Attributes specific to files:
* - `fid`: File ID, as an `Integer`.
* - `fileUploadTime`: Timestamp when the file was uploaded, as a `Timestamp`.
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
index b44ccaf30cc..d063dfac585 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilder.scala
@@ -25,7 +25,6 @@ import org.apache.texera.web.resource.dashboard.DashboardResource.DashboardClick
import org.apache.texera.web.resource.dashboard.FulltextSearchQueryUtils._
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow
import org.jooq.impl.DSL
-import org.jooq.impl.DSL.groupConcatDistinct
import org.jooq.{Condition, GroupField, Record, TableLike}
import scala.jdk.CollectionConverters.CollectionHasAsScala
@@ -53,7 +52,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
uid = WORKFLOW_OF_USER.UID,
ownerId = WORKFLOW_OF_USER.UID,
userName = USER.NAME,
- projectsOfWorkflow = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID),
workflowCoverImage = DSL.max(WORKFLOW_COVER_IMAGE.IMAGE).as("workflow_cover_image")
)
}
@@ -71,11 +69,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
.on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID))
.leftJoin(USER)
.on(USER.UID.eq(WORKFLOW_OF_USER.UID))
- .leftJoin(WORKFLOW_OF_PROJECT)
- .on(WORKFLOW_OF_PROJECT.WID.eq(WORKFLOW.WID))
- .leftJoin(PROJECT_USER_ACCESS)
- .on(PROJECT_USER_ACCESS.PID.eq(WORKFLOW_OF_PROJECT.PID))
- .and(if (uid == null) DSL.falseCondition() else PROJECT_USER_ACCESS.UID.eq(uid))
.leftJoin(WORKFLOW_COVER_IMAGE)
.on(WORKFLOW_COVER_IMAGE.WID.eq(WORKFLOW.WID))
@@ -83,8 +76,7 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
if (uid == null) {
condition = WORKFLOW.IS_PUBLIC.eq(true)
} else {
- val privateAccessCondition =
- WORKFLOW_USER_ACCESS.UID.eq(uid).or(PROJECT_USER_ACCESS.UID.isNotNull)
+ val privateAccessCondition = WORKFLOW_USER_ACCESS.UID.eq(uid)
if (includePublic) {
condition = privateAccessCondition.or(WORKFLOW.IS_PUBLIC.eq(true))
} else {
@@ -122,8 +114,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
.and(getContainsFilter(params.owners, USER.EMAIL))
// Apply operators filter
.and(getOperatorsFilter(params.operators, WORKFLOW.CONTENT))
- // Apply projectId filter
- .and(getContainsFilter(params.projectIds, WORKFLOW_OF_PROJECT.PID))
// Apply fulltext search filter
.and(
getFullTextSearchFilter(
@@ -150,7 +140,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
uid: Integer,
record: Record
): DashboardResource.DashboardClickableFileEntry = {
- val pidField = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)
val dw = DashboardWorkflow(
record.into(WORKFLOW_OF_USER).getUid == uid,
Option(record.get(WORKFLOW_USER_ACCESS.PRIVILEGE, classOf[PrivilegeEnum]))
@@ -158,16 +147,6 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
.getOrElse(PrivilegeEnum.NONE.toString),
record.into(USER).getName,
record.into(WORKFLOW).into(classOf[Workflow]),
- if (record.get(pidField) == null) {
- List[Integer]()
- } else {
- record
- .get(pidField)
- .asInstanceOf[String]
- .split(',')
- .map(number => Integer.valueOf(number))
- .toList
- },
record.into(USER).getUid,
Option(record.get("workflow_cover_image", classOf[String]))
)
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala
index 33ba16c6e14..09e3bfc8482 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/HubResource.scala
@@ -494,7 +494,6 @@ class HubResource {
DashboardClickableFileEntry(
resourceType = entityType.value,
workflow = Some(w),
- project = None,
dataset = None
)
}
@@ -503,7 +502,6 @@ class HubResource {
DashboardClickableFileEntry(
resourceType = entityType.value,
workflow = None,
- project = None,
dataset = Some(d)
)
}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala
deleted file mode 100644
index a2a7c45715b..00000000000
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResource.scala
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard.user.project
-
-import io.dropwizard.auth.Auth
-import org.apache.texera.auth.SessionUser
-import org.apache.texera.dao.SqlServer
-import org.apache.texera.dao.jooq.generated.Tables.{PROJECT_USER_ACCESS, USER}
-import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
-import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectDao, ProjectUserAccessDao, UserDao}
-import org.apache.texera.dao.jooq.generated.tables.pojos.ProjectUserAccess
-import org.apache.texera.web.model.common.AccessEntry
-import org.apache.texera.web.resource.dashboard.user.project.ProjectAccessResource.{
- userHasWriteAccess
-}
-import org.jooq.DSLContext
-
-import java.util
-import javax.annotation.security.RolesAllowed
-import javax.ws.rs._
-import javax.ws.rs.core.MediaType
-
-object ProjectAccessResource {
- private def context: DSLContext =
- SqlServer
- .getInstance()
- .createDSLContext()
-
- def userHasWriteAccess(pid: Integer, uid: Integer): Boolean = {
- getProjectAccessPrivilege(pid, uid) == PrivilegeEnum.WRITE
- }
-
- def getProjectAccessPrivilege(pid: Integer, uid: Integer): PrivilegeEnum = {
- Option(
- context
- .select(PROJECT_USER_ACCESS.PRIVILEGE)
- .from(PROJECT_USER_ACCESS)
- .where(
- PROJECT_USER_ACCESS.PID
- .eq(pid)
- .and(PROJECT_USER_ACCESS.UID.eq(uid))
- )
- .fetchOneInto(classOf[PrivilegeEnum])
- ).getOrElse(PrivilegeEnum.NONE)
- }
-}
-
-@Produces(Array(MediaType.APPLICATION_JSON))
-@RolesAllowed(Array("REGULAR", "ADMIN"))
-@Path("/access/project")
-class ProjectAccessResource() {
- private def context: DSLContext =
- SqlServer
- .getInstance()
- .createDSLContext()
- private def userDao = new UserDao(context.configuration())
- private def projectDao = new ProjectDao(context.configuration)
- private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration)
-
- /**
- * This method returns the owner of a project
- *
- * @param pid , project id
- * @return ownerEmail, the owner's email
- */
- @GET
- @Path("/owner/{pid}")
- def getOwner(@PathParam("pid") pid: Integer): String = {
- userDao.fetchOneByUid(projectDao.fetchOneByPid(pid).getOwnerId).getEmail
- }
-
- /**
- * Returns information about all current shared access of the given project
- *
- * @param pid project id
- * @return a List of email/permission pair
- */
- @GET
- @Path("/list/{pid}")
- def getAccessList(
- @PathParam("pid") pid: Integer
- ): util.List[AccessEntry] = {
- context
- .select(
- USER.EMAIL,
- USER.NAME,
- PROJECT_USER_ACCESS.PRIVILEGE
- )
- .from(PROJECT_USER_ACCESS)
- .join(USER)
- .on(USER.UID.eq(PROJECT_USER_ACCESS.UID))
- .where(
- PROJECT_USER_ACCESS.PID
- .eq(pid)
- .and(PROJECT_USER_ACCESS.UID.notEqual(projectDao.fetchOneByPid(pid).getOwnerId))
- )
- .fetchInto(classOf[AccessEntry])
- }
-
- /**
- * This method shares a project to a user with a specific access type
- *
- * @param pid the given project
- * @param email the email which the access is given to
- * @param privilege the type of Access given to the target user
- * @return rejection if user not permitted to share the project or Success Message
- */
- @PUT
- @Path("/grant/{pid}/{email}/{privilege}")
- def grantAccess(
- @PathParam("pid") pid: Integer,
- @PathParam("email") email: String,
- @PathParam("privilege") privilege: String,
- @Auth user: SessionUser
- ): Unit = {
- if (!userHasWriteAccess(pid, user.getUid)) {
- throw new ForbiddenException(s"You do not have permission to modify project $pid")
- }
-
- val targetUser = userDao.fetchOneByEmail(email)
- if (targetUser == null || targetUser.getIsPlaceholder) {
- throw new BadRequestException(s"No registered user with email $email")
- }
- projectUserAccessDao.merge(
- new ProjectUserAccess(
- targetUser.getUid,
- pid,
- PrivilegeEnum.valueOf(privilege)
- )
- )
- }
-
- /**
- * Revoke a user's access to a file
- *
- * @param pid the id of the file
- * @param email the email of target user whose access is about to be revoked
- * @return A successful resp if granted, failed resp otherwise
- */
- @DELETE
- @Path("/revoke/{pid}/{email}")
- def revokeAccess(
- @PathParam("pid") pid: Integer,
- @PathParam("email") email: String,
- @Auth user: SessionUser
- ): Unit = {
- if (!userHasWriteAccess(pid, user.getUid)) {
- throw new ForbiddenException(s"You do not have permission to modify project $pid")
- }
-
- context
- .delete(PROJECT_USER_ACCESS)
- .where(
- PROJECT_USER_ACCESS.UID
- .eq(userDao.fetchOneByEmail(email).getUid)
- .and(PROJECT_USER_ACCESS.PID.eq(pid))
- )
- .execute()
- }
-}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala
deleted file mode 100644
index be72fd21d02..00000000000
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResource.scala
+++ /dev/null
@@ -1,337 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard.user.project
-
-import io.dropwizard.auth.Auth
-import org.apache.commons.lang3.StringUtils
-import org.apache.texera.auth.SessionUser
-import org.apache.texera.dao.SqlServer
-import org.apache.texera.dao.jooq.generated.Tables._
-import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
-import org.apache.texera.dao.jooq.generated.tables.daos.{
- ProjectDao,
- ProjectUserAccessDao,
- WorkflowOfProjectDao
-}
-import org.apache.texera.dao.jooq.generated.tables.pojos._
-import org.apache.texera.web.resource.dashboard.DashboardResource
-import org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams
-import org.apache.texera.web.resource.dashboard.user.project.ProjectResource._
-import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess
-import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow
-
-import java.sql.Timestamp
-import java.util
-import javax.annotation.security.RolesAllowed
-import javax.ws.rs._
-import javax.ws.rs.core.MediaType
-import scala.jdk.CollectionConverters.IterableHasAsScala
-
-/**
- * This file handles various request related to projects.
- * It sends mysql queries to the MysqlDB regarding the 'user_project',
- * 'workflow_of_project', and 'file_of_project' Tables
- * The details of these tables can be found in /sql/texera_ddl.sql
- */
-
-object ProjectResource {
- private def context =
- SqlServer
- .getInstance()
- .createDSLContext()
- private def userProjectDao = new ProjectDao(context.configuration)
- private def workflowOfProjectDao = new WorkflowOfProjectDao(context.configuration)
- private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration)
-
- /**
- * This method is used to insert any CSV files created from ResultExportService
- * handleCSVRequest function into all project(s) that the workflow belongs to.
- *
- * No insertion occurs if the workflow does not belong to any projects.
- *
- * @param uid user ID
- * @param wid workflow ID
- * @param fileName name of exported file
- * @return String containing status of adding exported file to project(s)
- */
- def addExportedFileToProject(uid: Integer, wid: Integer, fileName: String): String = {
- // get map of PIDs and project names
- val pidMap = context
- .select(WORKFLOW_OF_PROJECT.PID, PROJECT.NAME)
- .from(WORKFLOW_OF_PROJECT)
- .leftJoin(PROJECT)
- .on(WORKFLOW_OF_PROJECT.PID.eq(PROJECT.PID))
- .where(WORKFLOW_OF_PROJECT.WID.eq(wid))
- .fetch()
- .intoMap(WORKFLOW_OF_PROJECT.PID, PROJECT.NAME)
-
- if (pidMap.size() > 0) { // workflow belongs to project(s)
- // generate string for ResultExportResponse
- if (pidMap.size() == 1) {
- s"and added to project: ${pidMap.values().toArray()(0)}"
- } else {
- s"and added to projects: ${pidMap.values().asScala.mkString(", ")}"
- }
- } else { // workflow does not belong to a project
- ""
- }
- }
-
- private def workflowOfProjectExists(wid: Integer, pid: Integer): Boolean = {
- workflowOfProjectDao.existsById(
- context
- .newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID)
- .values(wid, pid)
- )
- }
-
- case class DashboardProject(
- pid: Integer,
- name: String,
- description: String,
- ownerID: Integer,
- creationTime: Timestamp,
- color: String,
- accessLevel: String
- )
-}
-
-@Path("/project")
-@RolesAllowed(Array("REGULAR", "ADMIN"))
-@Produces(Array(MediaType.APPLICATION_JSON))
-class ProjectResource {
-
- /**
- * This method returns the specified project
- *
- * @param pid project id
- * @return project specified by the project id
- */
- @GET
- @Path("/{pid}")
- def getProject(@PathParam("pid") pid: Integer): Project = {
- userProjectDao.fetchOneByPid(pid)
- }
-
- /**
- * This method returns the list of projects owned by the session user.
- *
- * @param user the session user
- * @return a list of projects belonging to owner
- */
- @GET
- @Path("/list")
- def getProjectList(@Auth user: SessionUser): util.List[DashboardProject] = {
- context
- .selectDistinct(
- PROJECT.PID,
- PROJECT.NAME,
- PROJECT.DESCRIPTION,
- PROJECT.OWNER_ID,
- PROJECT.CREATION_TIME,
- PROJECT.COLOR,
- PROJECT_USER_ACCESS.PRIVILEGE
- )
- .from(PROJECT_USER_ACCESS)
- .join(PROJECT)
- .on(PROJECT_USER_ACCESS.PID.eq(PROJECT.PID))
- .where(PROJECT.OWNER_ID.eq(user.getUid).or(PROJECT_USER_ACCESS.UID.eq(user.getUid)))
- .fetchInto(classOf[DashboardProject])
- }
-
- /**
- * This method returns a list of DashboardWorkflow objects, which represents
- * all the workflows that are part of the specified project.
- *
- * @param pid project ID
- * @param user the session user
- * @return list of DashboardWorkflow objects
- */
- @GET
- @Path("/{pid}/workflows")
- def listProjectWorkflows(
- @PathParam("pid") pid: Integer,
- @Auth user: SessionUser
- ): List[DashboardWorkflow] = {
- val result = DashboardResource.searchAllResources(
- user,
- SearchQueryParams(resourceType = "workflow", projectIds = util.Arrays.asList(pid))
- )
- result.results.map(_.workflow.get)
- }
-
- /**
- * This method inserts a new project into the database belonging to the session user
- * and with the specified name.
- *
- * @param user the session user
- * @param name project name
- */
- @POST
- @Path("/create/{name}")
- def createProject(
- @Auth user: SessionUser,
- @PathParam("name") name: String
- ): Project = {
- val project = new Project(null, name, null, user.getUid, null, null)
- try {
- userProjectDao.insert(project)
- projectUserAccessDao.merge(
- new ProjectUserAccess(user.getUid, project.getPid, PrivilegeEnum.WRITE)
- )
- } catch {
- case _: Throwable =>
- throw new BadRequestException("Cannot create a new project with provided name.");
- }
- userProjectDao.fetchOneByPid(project.getPid)
- }
-
- /**
- * This method adds a mapping between the specified workflow to the specified project into the database.
- *
- * @param pid project ID
- * @param wid workflow ID
- */
- @POST
- @Path("/{pid}/workflow/{wid}/add")
- def addWorkflowToProject(
- @PathParam("pid") pid: Integer,
- @PathParam("wid") wid: Integer,
- @Auth user: SessionUser
- ): Unit = {
- if (!hasReadAccess(wid, user.getUid)) {
- throw new ForbiddenException("No sufficient access privilege to workflow.")
- }
-
- if (!workflowOfProjectExists(wid, pid)) {
- workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid))
- }
- }
-
- /**
- * This method updates the project name of the specified, existing project
- *
- * @param pid project ID
- * @param name new name
- */
- @POST
- @Path("/{pid}/rename/{name}")
- def updateProjectName(
- @PathParam("pid") pid: Integer,
- @PathParam("name") name: String
- ): Unit = {
- val userProject: Project = userProjectDao.fetchOneByPid(pid)
- if (StringUtils.isBlank(name)) {
- throw new BadRequestException("Cannot rename project to empty or blank name.")
- }
-
- try {
- userProject.setName(name)
- userProjectDao.update(userProject)
- } catch {
- case _: Throwable => throw new BadRequestException("Cannot rename project to provided name.");
- }
- }
-
- /**
- * This method updates the description of a specified, existing project
- *
- * @param pid project ID
- */
- @POST
- @Path("/{pid}/update/description")
- @Consumes(Array(MediaType.TEXT_PLAIN))
- def updateProjectDescription(
- @PathParam("pid") pid: Integer,
- description: String
- ): Unit = {
- val userProject: Project = userProjectDao.fetchOneByPid(pid)
- try {
- userProject.setDescription(description)
- userProjectDao.update(userProject)
- } catch {
- case _: Throwable =>
- throw new BadRequestException("Cannot update project description to provided text.");
- }
- }
-
- /**
- * This method updates a project's color.
- *
- * @param pid id of project to be updated
- * @param colorHex new HEX formatted color to be set
- */
- @POST
- @Path("/{pid}/color/{colorHex}/add")
- def updateProjectColor(
- @PathParam("pid") pid: Integer,
- @PathParam("colorHex") colorHex: String,
- @Auth sessionUser: SessionUser
- ): Unit = {
- val userProject: Project = userProjectDao.fetchOneByPid(pid)
- if (
- colorHex == null || colorHex.length != 6 && colorHex.length != 3 || !colorHex.matches(
- "^[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}$"
- )
- ) {
- throw new BadRequestException("Cannot assign invalid HEX format color to project.")
- }
-
- userProject.setColor(colorHex)
- userProjectDao.update(userProject)
- }
-
- @POST
- @Path("/{pid}/color/delete")
- def deleteProjectColor(@PathParam("pid") pid: Integer): Unit = {
- val userProject: Project = userProjectDao.fetchOneByPid(pid)
- userProject.setColor(null)
- userProjectDao.update(userProject)
- }
-
- /**
- * This method deletes an existing project from the database
- *
- * @param pid projectID
- */
- @DELETE
- @Path("/delete/{pid}")
- def deleteProject(@PathParam("pid") pid: Integer): Unit = {
- userProjectDao.deleteById(pid)
- }
-
- /**
- * This method deletes an existing mapping between a workflow and project from
- * the database
- *
- * @param pid project ID
- * @param wid workflow ID
- */
- @DELETE
- @Path("/{pid}/workflow/{wid}/delete")
- def deleteWorkflowFromProject(
- @PathParam("pid") pid: Integer,
- @PathParam("wid") wid: Integer
- ): Unit = {
- workflowOfProjectDao.deleteById(
- context.newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID).values(wid, pid)
- )
- }
-}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala
deleted file mode 100644
index 6983bf13ee0..00000000000
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResource.scala
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard.user.project
-
-import io.dropwizard.auth.Auth
-import org.apache.texera.auth.SessionUser
-import org.apache.texera.dao.SqlServer
-import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PUBLIC_PROJECT, USER}
-import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
-import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectUserAccessDao, PublicProjectDao}
-import org.apache.texera.dao.jooq.generated.tables.pojos.{ProjectUserAccess, PublicProject}
-import org.jooq.DSLContext
-
-import java.sql.Timestamp
-import java.util
-import javax.annotation.security.RolesAllowed
-import javax.ws.rs._
-
-case class DashboardPublicProject(
- pid: Integer,
- name: String,
- owner: String,
- creationTime: Timestamp
-) {}
-
-@Path("/public/project")
-class PublicProjectResource {
-
- private def context: DSLContext =
- SqlServer
- .getInstance()
- .createDSLContext()
- private def publicProjectDao = new PublicProjectDao(context.configuration)
- private def projectUserAccessDao = new ProjectUserAccessDao(context.configuration)
-
- @GET
- @RolesAllowed(Array("ADMIN"))
- @Path("/type/{pid}")
- def getType(@PathParam("pid") pid: Integer): String = {
- if (publicProjectDao.fetchOneByPid(pid) == null)
- "Private"
- else
- "Public"
- }
-
- @PUT
- @RolesAllowed(Array("ADMIN"))
- @Path("/public/{pid}")
- def makePublic(@PathParam("pid") pid: Integer, @Auth user: SessionUser): Unit = {
- publicProjectDao.insert(new PublicProject(pid, user.getUid))
- }
-
- @PUT
- @RolesAllowed(Array("ADMIN"))
- @Path("/private/{pid}")
- def makePrivate(@PathParam("pid") pid: Integer): Unit = {
- publicProjectDao.deleteById(pid)
- }
-
- @PUT
- @RolesAllowed(Array("REGULAR", "ADMIN"))
- @Path("/add")
- def addPublicProjects(checkedList: util.List[Integer], @Auth user: SessionUser): Unit = {
- checkedList.forEach(pid => {
- projectUserAccessDao.merge(
- new ProjectUserAccess(
- user.getUid,
- pid,
- PrivilegeEnum.READ
- )
- )
- })
- }
-
- @GET
- @RolesAllowed(Array("REGULAR", "ADMIN"))
- @Path("/list")
- def listPublicProjects(): util.List[DashboardPublicProject] = {
- context
- .select(PUBLIC_PROJECT.PID, PROJECT.NAME, USER.NAME, PROJECT.CREATION_TIME)
- .from(PUBLIC_PROJECT)
- .leftJoin(PROJECT)
- .on(PUBLIC_PROJECT.PID.eq(PROJECT.PID))
- .leftJoin(USER)
- .on(USER.UID.eq(PUBLIC_PROJECT.UID))
- .fetchInto(classOf[DashboardPublicProject])
- }
-}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala
index 4fd22f4260c..09353d257f9 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowAccessResource.scala
@@ -86,18 +86,7 @@ object WorkflowAccessResource {
.where(WORKFLOW_USER_ACCESS.WID.eq(wid).and(WORKFLOW_USER_ACCESS.UID.eq(uid)))
.fetchOneInto(classOf[WorkflowUserAccess])
if (access == null) {
- val projectAccess = context
- .select()
- .from(PROJECT_USER_ACCESS)
- .join(WORKFLOW_OF_PROJECT)
- .on(WORKFLOW_OF_PROJECT.PID.eq(PROJECT_USER_ACCESS.PID))
- .where(WORKFLOW_OF_PROJECT.WID.eq(wid).and(PROJECT_USER_ACCESS.UID.eq(uid)))
- .fetchOneInto(classOf[WorkflowUserAccess])
- if (projectAccess == null) {
- PrivilegeEnum.NONE
- } else {
- projectAccess.getPrivilege
- }
+ PrivilegeEnum.NONE
} else {
access.getPrivilege
}
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
index 72d70d5cf74..ccaf51e2672 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowResource.scala
@@ -31,7 +31,6 @@ import org.apache.texera.dao.jooq.generated.Tables._
import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
import org.apache.texera.dao.jooq.generated.tables.daos.{
WorkflowDao,
- WorkflowOfProjectDao,
WorkflowOfUserDao,
WorkflowUserAccessDao
}
@@ -39,10 +38,9 @@ import org.apache.texera.dao.jooq.generated.tables.pojos._
import org.apache.texera.service.util.LargeBinaryManager
import org.apache.texera.web.resource.dashboard.hub.EntityType
import org.apache.texera.web.resource.dashboard.hub.HubResource.recordCloneAction
-import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource._
-import org.jooq.impl.DSL.{groupConcatDistinct, noCondition, max}
-import org.jooq.{Condition, DSLContext, Record10, Result, SelectOnConditionStep}
+import org.jooq.impl.DSL.{noCondition, max}
+import org.jooq.{Condition, DSLContext, Record9, Result, SelectOnConditionStep}
import java.sql.Timestamp
import java.util
@@ -75,7 +73,6 @@ object WorkflowResource {
new WorkflowUserAccessDao(
context.configuration()
)
- private def workflowOfProjectDao = new WorkflowOfProjectDao(context.configuration)
/** Max length of a stored cover-image data URL. */
private val COVER_IMAGE_MAX_CHARS: Int = 4 * 1024 * 1024
@@ -111,20 +108,11 @@ object WorkflowResource {
)
}
- private def workflowOfProjectExists(wid: Integer, pid: Integer): Boolean = {
- workflowOfProjectDao.existsById(
- context
- .newRecord(WORKFLOW_OF_PROJECT.WID, WORKFLOW_OF_PROJECT.PID)
- .values(wid, pid)
- )
- }
-
case class DashboardWorkflow(
isOwner: Boolean,
accessLevel: String,
ownerName: String,
workflow: Workflow,
- projectIDs: List[Integer],
ownerId: Integer,
coverImage: Option[String]
)
@@ -140,7 +128,7 @@ object WorkflowResource {
readonly: Boolean
)
- case class WorkflowIDs(wids: List[Integer], pid: Option[Integer])
+ case class WorkflowIDs(wids: List[Integer])
private def updateWorkflowField(
workflow: Workflow,
@@ -192,7 +180,7 @@ object WorkflowResource {
}
}
- def baseWorkflowSelect(): SelectOnConditionStep[Record10[
+ def baseWorkflowSelect(): SelectOnConditionStep[Record9[
Integer,
String,
String,
@@ -201,7 +189,6 @@ object WorkflowResource {
PrivilegeEnum,
Integer,
String,
- String,
String
]] = {
context
@@ -214,7 +201,6 @@ object WorkflowResource {
WORKFLOW_USER_ACCESS.PRIVILEGE,
WORKFLOW_OF_USER.UID,
USER.NAME,
- groupConcatDistinct(WORKFLOW_OF_PROJECT.PID).as("projects"),
max(WORKFLOW_COVER_IMAGE.IMAGE).as("cover_image")
)
.from(WORKFLOW)
@@ -224,14 +210,12 @@ object WorkflowResource {
.on(WORKFLOW_OF_USER.WID.eq(WORKFLOW.WID))
.leftJoin(USER)
.on(USER.UID.eq(WORKFLOW_OF_USER.UID))
- .leftJoin(WORKFLOW_OF_PROJECT)
- .on(WORKFLOW.WID.eq(WORKFLOW_OF_PROJECT.WID))
.leftJoin(WORKFLOW_COVER_IMAGE)
.on(WORKFLOW.WID.eq(WORKFLOW_COVER_IMAGE.WID))
}
def mapWorkflowEntries(
- workflowEntries: Result[Record10[
+ workflowEntries: Result[Record9[
Integer,
String,
String,
@@ -240,7 +224,6 @@ object WorkflowResource {
PrivilegeEnum,
Integer,
String,
- String,
String
]],
uid: Integer
@@ -258,9 +241,6 @@ object WorkflowResource {
.toString,
workflowRecord.into(USER).getName,
workflowRecord.into(WORKFLOW).into(classOf[Workflow]),
- if (workflowRecord.component9() == null) List[Integer]()
- else
- workflowRecord.component9().split(',').map(str => Integer.valueOf(str)).toList,
workflowRecord.into(WORKFLOW_OF_USER).getUid,
Option(workflowRecord.get("cover_image", classOf[String]))
)
@@ -496,7 +476,6 @@ class WorkflowResource extends LazyLogging {
}
val resultWorkflows: ListBuffer[DashboardWorkflow] = ListBuffer()
- val addToProject = workflowIDs.pid.nonEmpty
// then start a transaction and do the duplication
try {
context.transaction { txConfig =>
@@ -514,19 +493,6 @@ class WorkflowResource extends LazyLogging {
),
sessionUser
)
- // if workflows also need to be added to the project
- if (addToProject) {
- val newWid = newWorkflow.workflow.getWid
- if (!hasReadAccess(newWid, user.getUid)) {
- throw new ForbiddenException("No sufficient access privilege to workflow.")
- }
- val pid = workflowIDs.pid.get
- if (!workflowOfProjectExists(newWid, pid)) {
- workflowOfProjectDao.insert(new WorkflowOfProject(newWid, pid))
- } else {
- throw new BadRequestException("Workflow already exists in the project")
- }
- }
resultWorkflows += newWorkflow
}
}
@@ -590,7 +556,6 @@ class WorkflowResource extends LazyLogging {
PrivilegeEnum.WRITE.toString,
user.getName,
workflowDao.fetchOneByWid(workflow.getWid),
- List[Integer](),
user.getUid,
None
)
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala
deleted file mode 100644
index 0a05846dda1..00000000000
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/ProjectSearchQueryBuilderSpec.scala
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard
-
-import org.apache.texera.dao.jooq.generated.Tables.PROJECT
-import org.apache.texera.dao.jooq.generated.tables.pojos.Project
-import org.jooq.impl.{DSL => JDSL}
-import org.jooq.{Record, SQLDialect}
-
-import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.matchers.should.Matchers
-
-import java.sql.Timestamp
-
-class ProjectSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
-
- private val ctx = JDSL.using(SQLDialect.POSTGRES)
-
- private val ownerUid: Integer = Integer.valueOf(42)
- private val callerUid: Integer = Integer.valueOf(43)
- private val pid: Integer = Integer.valueOf(7)
- private val createdAt = new Timestamp(1700000000123L)
-
- // In-memory record shaped like the one toEntryImpl receives (keyed by the
- // original PROJECT fields). Values are distinct so a wrong-column read fails.
- private def translatedRecord(
- description: String = "proj-description",
- color: String = "aabbcc"
- ): Record = {
- val record = ctx.newRecord(
- PROJECT.PID,
- PROJECT.NAME,
- PROJECT.DESCRIPTION,
- PROJECT.OWNER_ID,
- PROJECT.CREATION_TIME,
- PROJECT.COLOR
- )
- record.set(PROJECT.PID, pid)
- record.set(PROJECT.NAME, "proj-name")
- record.set(PROJECT.DESCRIPTION, description)
- record.set(PROJECT.OWNER_ID, ownerUid)
- record.set(PROJECT.CREATION_TIME, createdAt)
- record.set(PROJECT.COLOR, color)
- record
- }
-
- private def projectOf(record: Record, uid: Integer): Project =
- ProjectSearchQueryBuilder.toEntryImpl(uid, record).project.get
-
- "toEntryImpl" should "copy every project column into the POJO" in {
- val p = projectOf(translatedRecord(), ownerUid)
- p.getPid shouldBe pid
- p.getName shouldBe "proj-name"
- p.getDescription shouldBe "proj-description"
- p.getOwnerId shouldBe ownerUid
- p.getCreationTime shouldBe createdAt
- p.getColor shouldBe "aabbcc"
- }
-
- it should "pass a NULL description and a NULL color through as null" in {
- // description and color are the only nullable project columns.
- val p = projectOf(translatedRecord(description = null, color = null), ownerUid)
- p.getDescription shouldBe null
- p.getColor shouldBe null
- p.getPid shouldBe pid
- }
-
- it should "tag the entry as a project and leave the other payload slots empty" in {
- // searchAllResources matches on resourceType with no default branch, so a
- // wrong tag is a runtime MatchError.
- val entry = ProjectSearchQueryBuilder.toEntryImpl(ownerUid, translatedRecord())
- entry.resourceType shouldBe "project"
- entry.project should not be None
- entry.workflow shouldBe None
- entry.dataset shouldBe None
- }
-
- it should "produce the same entry regardless of the caller's uid" in {
- // Unlike the workflow arm there is no ownership flag to compute.
- val record = translatedRecord()
- ProjectSearchQueryBuilder.toEntryImpl(ownerUid, record) shouldBe
- ProjectSearchQueryBuilder.toEntryImpl(callerUid, record)
- }
-
- // An aliased field renders as the bare alias on its own, so render a SELECT.
- private lazy val renderedSchema: String = ctx.renderInlined(
- JDSL.select(ProjectSearchQueryBuilder.mappedResourceSchema.allFields: _*)
- )
-
- "mappedResourceSchema" should "project the literal 'project' as the resourceType column" in {
- renderedSchema should include("'project' as \"resourceType\"")
- }
-
- it should "alias PROJECT.CREATION_TIME as both the creation and last-modified time" in {
- // Deliberate: the project table has no last-modified column. Without this
- // alias projects would NULL-sink in every sort by edit time.
- val creationTime = ctx.renderInlined(PROJECT.CREATION_TIME)
- renderedSchema should include(s"""$creationTime as "resourceCreationTime"""")
- renderedSchema should include(s"""$creationTime as "resourceLastModifiedTime"""")
- }
-
- it should "project PROJECT.COLOR as the color column" in {
- // The frontend reads the project colour swatch from this alias.
- val color = ctx.renderInlined(PROJECT.COLOR)
- renderedSchema should include(s"""$color as "color"""")
- }
-
- it should "project the project id and owner through the shared slots" in {
- // The owner rides the generic resourceOwnerId slot; the project-specific
- // "owner_uid" slot stays NULL and is deliberately not pinned here.
- renderedSchema should include(s"""${ctx.renderInlined(PROJECT.PID)} as "pid"""")
- renderedSchema should include(
- s"""${ctx.renderInlined(PROJECT.OWNER_ID)} as "resourceOwnerId""""
- )
- }
-
- it should "project the name and description through the shared full-text slots" in {
- // Also the two columns the keyword filter targets.
- renderedSchema should include(s"""${ctx.renderInlined(PROJECT.NAME)} as "resourceName"""")
- renderedSchema should include(
- s"""${ctx.renderInlined(PROJECT.DESCRIPTION)} as "resourceDescription""""
- )
- }
-}
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala
index 9d5f1e0b0bd..63ab264edb7 100644
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala
+++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchemaSpec.scala
@@ -77,11 +77,10 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
private def translatedOriginals(schema: UnifiedResourceSchema): Seq[Field[_]] =
translatedPairs(schema).map(_._1)
- // Sentinels for the three slots that have no convenient distinct table
+ // Sentinels for the two slots that have no convenient distinct table
// column of the right type; every other slot uses a real generated column so
- // that all 24 originals render differently from one another.
+ // that all 20 originals render differently from one another.
private val sentinelResourceType: Field[String] = JDSL.inline("s-resource-type")
- private val sentinelProjects: Field[String] = JDSL.inline("s-projects")
private val sentinelStoragePath: Field[String] = JDSL.inline("s-storage-path")
private val sentinelSchema: UnifiedResourceSchema = UnifiedResourceSchema(
@@ -94,13 +93,9 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
ownerId = WORKFLOW_OF_USER.UID,
wid = WORKFLOW.WID,
workflowUserAccess = WORKFLOW_USER_ACCESS.PRIVILEGE,
- projectsOfWorkflow = sentinelProjects,
uid = USER.UID,
userName = USER.NAME,
userEmail = USER.EMAIL,
- pid = PROJECT.PID,
- projectOwnerId = PROJECT.OWNER_ID,
- projectColor = PROJECT.COLOR,
did = DATASET.DID,
datasetStoragePath = sentinelStoragePath,
repositoryName = DATASET.REPOSITORY_NAME,
@@ -122,13 +117,9 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
"resourceOwnerId" -> WORKFLOW_OF_USER.UID,
"wid" -> WORKFLOW.WID,
"workflow_privilege" -> WORKFLOW_USER_ACCESS.PRIVILEGE,
- "projects" -> sentinelProjects,
"uid" -> USER.UID,
"userName" -> USER.NAME,
"email" -> USER.EMAIL,
- "pid" -> PROJECT.PID,
- "owner_uid" -> PROJECT.OWNER_ID,
- "color" -> PROJECT.COLOR,
"did" -> DATASET.DID,
"dataset_storage_path" -> sentinelStoragePath,
"repository_name" -> DATASET.REPOSITORY_NAME,
@@ -141,8 +132,8 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
// -- apply(): the projection ------------------------------------------------
- "apply" should "expose all 24 slots as aliases, in the order the UNION ALL depends on" in {
- sentinelSchema.allFields should have size 24
+ "apply" should "expose all 20 slots as aliases, in the order the UNION ALL depends on" in {
+ sentinelSchema.allFields should have size 20
sentinelSchema.allFields.map(_.getName) shouldBe expectedProjection.map(_._1)
}
@@ -162,7 +153,7 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
// about datasets still union with one that does: the column count and
// types have to line up.
val defaults = UnifiedResourceSchema()
- defaults.allFields should have size 24
+ defaults.allFields should have size 20
val rendered = ctx.renderInlined(JDSL.select(defaults.allFields: _*))
rendered should include("'' as \"resourceType\"")
rendered should include("cast(null as timestamp) as \"resourceCreationTime\"")
@@ -197,12 +188,12 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
}
it should "collapse the all-defaults projection down to one alias per distinct default" in {
- // 24 slots, but only six structurally distinct default expressions, so the
+ // 20 slots, but only six structurally distinct default expressions, so the
// de-dup collapses the map to six entries. Worth pinning because it is
// surprising, and because it is what makes the keep-first rule observable at
- // all: allFields stays at 24 while the translation map does not.
+ // all: allFields stays at 20 while the translation map does not.
val defaults = UnifiedResourceSchema()
- defaults.allFields should have size 24
+ defaults.allFields should have size 20
translatedAliases(defaults) shouldBe Seq(
"resourceType", // DSL.inline("")
"resourceCreationTime", // cast(null as timestamp)
@@ -213,7 +204,7 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
)
}
- it should "keep every distinct original when the caller supplies 24 distinct Fields" in {
+ it should "keep every distinct original when the caller supplies 20 distinct Fields" in {
// Nothing to collapse here, which is the control case for the two tests
// above: the shrinkage they observe comes from duplicate originals only.
translatedAliases(sentinelSchema) shouldBe expectedProjection.map(_._1)
@@ -221,15 +212,12 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
it should "drop exactly the duplicated slots of the production workflow projection" in {
val workflowSchema = WorkflowSearchQueryBuilder.mappedResourceSchema
- workflowSchema.allFields should have size 24
+ workflowSchema.allFields should have size 20
val aliases = translatedAliases(workflowSchema)
// `uid` duplicates ownerId (WORKFLOW_OF_USER.UID); the rest are slots the
// builder left at their default, and the defaults collide by type.
workflowSchema.allFields.map(_.getName).diff(aliases) shouldBe Seq(
"uid",
- "owner_uid",
- "color",
- "did",
"repository_name",
"is_dataset_downloadable",
"cover_image"
@@ -241,7 +229,7 @@ class UnifiedResourceSchemaSpec extends AnyFlatSpec with Matchers {
"jOOQ Field equality" should "be structural, which is what makes the de-dup collapse anything" in {
// If jOOQ ever switched to identity equality, translatedFieldSet would keep
- // all 24 slots and translateRecord would start reading duplicated columns —
+ // all 20 slots and translateRecord would start reading duplicated columns —
// the tests above would flip, and this one says why.
JDSL.cast(null, classOf[Integer]) shouldBe JDSL.cast(null, classOf[Integer])
JDSL.inline("") shouldBe JDSL.inline("")
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala
index 59dd8d9e19f..240e581165d 100644
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala
+++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/WorkflowSearchQueryBuilderSpec.scala
@@ -35,11 +35,6 @@ import org.scalatest.matchers.should.Matchers
* which is what makes them reachable from a spec at all.
*
* Breakage this catches:
- * - the `projects` aggregate lookup breaking (a Field looked up by structural
- * equality): `record.get(pidField)` would start returning null and every
- * workflow would silently report zero projects instead of failing;
- * - the comma-split branch losing/reordering ids, or the NULL branch no
- * longer yielding an empty list;
* - a NULL `workflow_user_access.privilege` (a left join miss for public
* workflows) no longer degrading to NONE — an NPE or a wrong grant;
* - the ownership flag comparing the wrong uid, or owner id/name being read off
@@ -63,10 +58,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
// jOOQ render context (Postgres dialect to match production renderers).
private val ctx = JDSL.using(SQLDialect.POSTGRES)
- // toEntryImpl re-creates this aggregate locally and then looks it up with
- // `record.get(pidField)`; the lookup only resolves because jOOQ compares
- // QueryParts structurally. The first test pins that assumption.
- private val pidField = JDSL.groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)
private val coverField = JDSL.max(WORKFLOW_COVER_IMAGE.IMAGE).as("workflow_cover_image")
private val ownerUid: Integer = Integer.valueOf(42)
@@ -84,7 +75,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
private def translatedRecord(
uidValue: Integer = ownerUid,
privilege: PrivilegeEnum = PrivilegeEnum.WRITE,
- projects: String = "3,1,2",
cover: String = "cover-b64"
): Record = {
val record = ctx.newRecord(
@@ -94,7 +84,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
WORKFLOW_OF_USER.UID,
WORKFLOW_USER_ACCESS.PRIVILEGE,
USER.NAME,
- pidField,
coverField
)
record.set(WORKFLOW.WID, wid)
@@ -103,7 +92,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
record.set(WORKFLOW_OF_USER.UID, uidValue)
record.set(WORKFLOW_USER_ACCESS.PRIVILEGE, privilege)
record.set(USER.NAME, "owner-name")
- record.set(pidField, projects)
record.set(coverField, cover)
record
}
@@ -111,46 +99,9 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
private def workflowOf(record: Record, uid: Integer): DashboardWorkflow =
WorkflowSearchQueryBuilder.toEntryImpl(uid, record).workflow.get
- // -- the aggregate-field lookup --------------------------------------------
-
- "toEntryImpl" should "resolve the project aggregate through structural Field equality" in {
- // toEntryImpl builds a *fresh* groupConcatDistinct instance rather than
- // reusing the one in mappedResourceSchema, so the whole projects feature
- // rides on jOOQ treating two structurally identical aggregates as equal.
- val record = translatedRecord(projects = "5,6")
- JDSL.groupConcatDistinct(WORKFLOW_OF_PROJECT.PID) shouldBe pidField
- record.get(JDSL.groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)) shouldBe "5,6"
- }
-
- // -- projectsOfWorkflow: both branches -------------------------------------
-
- it should "split the comma-joined aggregate into Integers, preserving the aggregate's order" in {
- // Deliberately unsorted so an accidental `.sorted` / `.reverse` fails.
- workflowOf(translatedRecord(projects = "3,1,2"), ownerUid).projectIDs shouldBe
- List(Integer.valueOf(3), Integer.valueOf(1), Integer.valueOf(2))
- }
-
- it should "handle a single-project aggregate (no separator present)" in {
- workflowOf(translatedRecord(projects = "8"), ownerUid).projectIDs shouldBe
- List(Integer.valueOf(8))
- }
-
- it should "return an empty project list when the aggregate is NULL" in {
- // A workflow that belongs to no project left-joins to a NULL aggregate.
- workflowOf(translatedRecord(projects = null), ownerUid).projectIDs shouldBe empty
- }
-
- // Deliberately NOT asserted: that a padded separator ("1, 2") or a leading comma
- // (",1") throws NumberFormatException. It does today — `Integer.valueOf` is applied
- // straight to the raw `split(',')` output, so such input aborts the whole search
- // request with a 500 instead of degrading. But production cannot produce it
- // (Postgres' string_agg is rendered with a bare ',' separator), and pinning the
- // throw would turn this suite red the moment someone hardens the parser with
- // `.map(_.trim).filter(_.nonEmpty)` — i.e. it would punish an improvement.
-
// -- privilege fallback -----------------------------------------------------
- it should "fall back to NONE when the workflow privilege is NULL" in {
+ "toEntryImpl" should "fall back to NONE when the workflow privilege is NULL" in {
// Public workflows the caller has no explicit grant on left-join to a NULL
// privilege; the DTO must still carry a usable access level.
workflowOf(translatedRecord(privilege = null), ownerUid).accessLevel shouldBe
@@ -204,7 +155,6 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
it should "tag the entry as a workflow and leave the other payload slots empty" in {
val entry = WorkflowSearchQueryBuilder.toEntryImpl(ownerUid, translatedRecord())
entry.resourceType shouldBe "workflow"
- entry.project shouldBe None
entry.dataset shouldBe None
entry.workflow should not be None
}
@@ -221,8 +171,5 @@ class WorkflowSearchQueryBuilderSpec extends AnyFlatSpec with Matchers {
JDSL.select(WorkflowSearchQueryBuilder.mappedResourceSchema.allFields: _*)
)
rendered should include("'workflow' as \"resourceType\"")
- // The projects column is the aggregate toEntryImpl re-creates locally; a
- // separator other than a bare ',' would break the split above.
- rendered should include(s"""${ctx.renderInlined(pidField)} as "projects"""")
}
}
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
index 5ec6b88ab2c..929f403b0a3 100644
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
+++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/file/WorkflowResourceSpec.scala
@@ -21,18 +21,12 @@ package org.apache.texera.web.resource.dashboard.file
import org.apache.texera.auth.SessionUser
import org.apache.texera.dao.MockTexeraDB
-import org.apache.texera.dao.jooq.generated.Tables.{USER, WORKFLOW, WORKFLOW_OF_PROJECT}
+import org.apache.texera.dao.jooq.generated.Tables.{USER, WORKFLOW}
import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum}
import org.apache.texera.dao.jooq.generated.tables.daos.{UserDao, WorkflowUserAccessDao}
-import org.apache.texera.dao.jooq.generated.tables.pojos.{
- Project,
- User,
- Workflow,
- WorkflowUserAccess
-}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{User, Workflow, WorkflowUserAccess}
import org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.CoverImageRequest
-import org.apache.texera.web.resource.dashboard.user.project.ProjectResource
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.{
DashboardWorkflow,
@@ -121,13 +115,6 @@ class WorkflowResourceSpec
workflow
}
- private val testProject1: Project = {
- val project = new Project()
- project.setName("test_project1")
- project.setDescription("this is project description")
- project
- }
-
private val exampleEmailAddress = "name@example.com"
private val exampleWord1 = "Lorem"
private val exampleWord2 = "Ipsum"
@@ -153,10 +140,6 @@ class WorkflowResourceSpec
new WorkflowResource()
}
- private val projectResource: ProjectResource = {
- new ProjectResource()
- }
-
private val dashboardResource: DashboardResource = {
new DashboardResource()
}
@@ -181,7 +164,7 @@ class WorkflowResourceSpec
var workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser1)
workflows.foreach(workflow =>
workflowResource.deleteWorkflow(
- WorkflowIDs(List(workflow.workflow.getWid), None),
+ WorkflowIDs(List(workflow.workflow.getWid)),
sessionUser1
)
)
@@ -189,18 +172,10 @@ class WorkflowResourceSpec
workflows = workflowResource.retrieveWorkflowsBySessionUser(sessionUser2)
workflows.foreach(workflow =>
workflowResource.deleteWorkflow(
- WorkflowIDs(List(workflow.workflow.getWid), None),
+ WorkflowIDs(List(workflow.workflow.getWid)),
sessionUser2
)
)
-
- // delete all projects in the database
- var projects = projectResource.getProjectList(sessionUser1)
- projects.forEach(project => projectResource.deleteProject(project.pid))
-
- projects = projectResource.getProjectList(sessionUser2)
- projects.forEach(project => projectResource.deleteProject(project.pid))
-
}
override protected def afterAll(): Unit = {
@@ -544,27 +519,6 @@ class WorkflowResourceSpec
assert(ownerFilter.toString == USER.EMAIL.eq("owner1").or(USER.EMAIL.eq("owner2")).toString)
}
- it should "return a proper condition for a single projectId" in {
- val projectIdList = new java.util.ArrayList[Integer](util.Arrays.asList(Integer.valueOf(1)))
- val projectFilter: Condition =
- FulltextSearchQueryUtils.getContainsFilter(projectIdList, WORKFLOW_OF_PROJECT.PID)
- assert(projectFilter.toString == WORKFLOW_OF_PROJECT.PID.eq(Integer.valueOf(1)).toString)
- }
-
- it should "return a proper condition for multiple projectIds" in {
- val projectIdList = new java.util.ArrayList[Integer](
- util.Arrays.asList(Integer.valueOf(1), Integer.valueOf(2))
- )
- val projectFilter: Condition =
- FulltextSearchQueryUtils.getContainsFilter(projectIdList, WORKFLOW_OF_PROJECT.PID)
- assert(
- projectFilter.toString == WORKFLOW_OF_PROJECT.PID
- .eq(Integer.valueOf(1))
- .or(WORKFLOW_OF_PROJECT.PID.eq(Integer.valueOf(2)))
- .toString
- )
- }
-
it should "return a proper condition for a single workflowID" in {
val workflowIdList = new java.util.ArrayList[Integer](util.Arrays.asList(Integer.valueOf(1)))
val workflowIdFilter: Condition =
@@ -666,10 +620,8 @@ class WorkflowResourceSpec
)
}
- "/search API" should "be able to search for resources in different tables" in {
+ "/search API" should "be able to search for resources by keyword" in {
- // create different types of resources, project, workflow, and file
- projectResource.createProject(sessionUser1, "test project1")
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
// search
val DashboardClickableFileEntryList =
@@ -677,88 +629,83 @@ class WorkflowResourceSpec
sessionUser1,
SearchQueryParams(getKeywordsArray("test"))
)
- assert(DashboardClickableFileEntryList.results.length == 2)
+ assert(DashboardClickableFileEntryList.results.length == 1)
}
it should "return all resources when no keyword provided" in {
- projectResource.createProject(sessionUser1, "test project1")
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
val DashboardClickableFileEntryList =
dashboardResource.searchAllResourcesCall(
sessionUser1,
SearchQueryParams(getKeywordsArray(""))
)
- assert(DashboardClickableFileEntryList.results.length == 2)
+ assert(DashboardClickableFileEntryList.results.length == 1)
}
it should "return multiple matching resources from a single resource type" in {
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
- projectResource.createProject(sessionUser1, "common project1")
- projectResource.createProject(sessionUser1, "common project2")
+ workflowResource.persistWorkflow(testWorkflow2, sessionUser1)
val DashboardClickableFileEntryList =
dashboardResource.searchAllResourcesCall(
sessionUser1,
- SearchQueryParams(getKeywordsArray("common"))
+ SearchQueryParams(getKeywordsArray("test"))
)
assert(DashboardClickableFileEntryList.results.length == 2)
}
it should "handle multiple keywords correctly" in {
- projectResource.createProject(sessionUser1, "test project1")
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
+ workflowResource.persistWorkflow(testWorkflow2, sessionUser1)
val DashboardClickableFileEntryList =
dashboardResource.searchAllResourcesCall(
sessionUser1,
- SearchQueryParams(getKeywordsArray("test", "project1"))
+ SearchQueryParams(getKeywordsArray("test", "workflow1"))
)
assert(
DashboardClickableFileEntryList.results.length == 1
- ) // should only return the project
+ ) // should only return test_workflow1
}
it should "filter results by different resourceType" in {
- // create different types of resources
- // 3 projects, 2 file, and 1 workflow,
- projectResource.createProject(sessionUser1, "test project1")
- projectResource.createProject(sessionUser1, "test project2")
- projectResource.createProject(sessionUser1, "test project3")
+ // create 3 workflows
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
+ workflowResource.persistWorkflow(testWorkflow2, sessionUser1)
+ workflowResource.persistWorkflow(testWorkflow3, sessionUser1)
// search resources with all resourceType
var DashboardClickableFileEntryList =
dashboardResource.searchAllResourcesCall(
sessionUser1,
SearchQueryParams(getKeywordsArray("test"))
)
- assert(DashboardClickableFileEntryList.results.length == 4)
+ assert(DashboardClickableFileEntryList.results.length == 3)
// filter resources by workflow
DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall(
sessionUser1,
SearchQueryParams(resourceType = "workflow", keywords = getKeywordsArray("test"))
)
- assert(DashboardClickableFileEntryList.results.length == 1)
+ assert(DashboardClickableFileEntryList.results.length == 3)
- // filter resources by project
+ // filter resources by dataset
DashboardClickableFileEntryList = dashboardResource.searchAllResourcesCall(
sessionUser1,
- SearchQueryParams(resourceType = "project", keywords = getKeywordsArray("test"))
+ SearchQueryParams(resourceType = "dataset", keywords = getKeywordsArray("test"))
)
- assert(DashboardClickableFileEntryList.results.length == 3)
+ assert(DashboardClickableFileEntryList.results.isEmpty)
}
it should "return resources that match any of all provided keywords" in {
// This test is designed to verify that the searchAllResources function correctly
// returns resources that match all of the provided keywords
- // Create different types of resources, a project, a workflow, and a file
- projectResource.createProject(sessionUser1, "test project")
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
+ workflowResource.persistWorkflow(testWorkflow2, sessionUser1)
// Perform search with multiple keywords
val DashboardClickableFileEntryList =
dashboardResource.searchAllResourcesCall(
sessionUser1,
- SearchQueryParams(keywords = getKeywordsArray("test", "project"))
+ SearchQueryParams(keywords = getKeywordsArray("test", "workflow2"))
)
// Assert that the search results include resources that match any of the provided keywords
@@ -768,8 +715,8 @@ class WorkflowResourceSpec
it should "not return resources that belong to a different user" in {
// This test is designed to verify that the searchAllResources function does not return resources that belong to a different user
- // Create a project for a different user (sessionUser2)
- projectResource.createProject(sessionUser2, "test project2")
+ // Create a workflow for a different user (sessionUser2)
+ workflowResource.persistWorkflow(testWorkflow1, sessionUser2)
// Perform search for resources using sessionUser1
val DashboardClickableFileEntryList =
@@ -778,7 +725,7 @@ class WorkflowResourceSpec
SearchQueryParams(keywords = getKeywordsArray("test"))
)
- // Assert that the search results do not include the project that belongs to the different user
+ // Assert that the search results do not include the workflow that belongs to the different user
// Assuming that DashboardClickableFileEntryList is a list of resources where each resource has a `user` property
assert(DashboardClickableFileEntryList.results.isEmpty)
}
@@ -786,10 +733,13 @@ class WorkflowResourceSpec
it should "paginate results correctly" in {
// This test is designed to verify that the pagination works correctly
- // Create 1 workflow, 10 projects
- workflowResource.persistWorkflow(testWorkflow1, sessionUser1)
- for (i <- 1 to 10) {
- projectResource.createProject(sessionUser1, s"test project $i")
+ // Create 11 workflows
+ for (i <- 1 to 11) {
+ val workflow = new Workflow()
+ workflow.setName(s"test_pagination_workflow$i")
+ workflow.setDescription("")
+ workflow.setContent(exampleContent)
+ workflowResource.persistWorkflow(workflow, sessionUser1)
}
// Request the first page of results (page size is 10)
@@ -1035,7 +985,7 @@ class WorkflowResourceSpec
"{\"operators\":[{\"operatorID\":\"op1\",\"operatorType\":\"CSVFileScan\"}]}"
).workflow.getWid
- val copies = workflowResource.duplicateWorkflow(WorkflowIDs(List(wid), None), sessionUser1)
+ val copies = workflowResource.duplicateWorkflow(WorkflowIDs(List(wid)), sessionUser1)
assert(copies.size == 1)
assert(copies.head.workflow.getName == "dup-src_copy")
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala
index 771250ced88..020f4f8ab6a 100644
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala
+++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/hub/HubResourceSpec.scala
@@ -897,7 +897,6 @@ class HubResourceSpec
entry.ownerName shouldBe "hub_owner"
entry.ownerId shouldBe Integer.valueOf(ownerUid)
entry.accessLevel shouldBe "WRITE"
- entry.projectIDs shouldBe empty
entry.coverImage shouldBe empty
}
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala
deleted file mode 100644
index eecb6ff3cbe..00000000000
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectAccessResourceSpec.scala
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard.user.project
-
-import org.apache.texera.auth.SessionUser
-import org.apache.texera.dao.MockTexeraDB
-import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS, USER}
-import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum}
-import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectUserAccessDao, UserDao}
-import org.apache.texera.dao.jooq.generated.tables.pojos.{ProjectUserAccess, User}
-import org.apache.texera.web.model.common.AccessEntry
-import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
-
-import javax.ws.rs.{BadRequestException, ForbiddenException}
-import scala.jdk.CollectionConverters._
-
-class ProjectAccessResourceSpec
- extends AnyFlatSpec
- with BeforeAndAfterAll
- with BeforeAndAfterEach
- with MockTexeraDB {
-
- private val ownerUid = 7101
- private val readerUid = 7102
- private val writerUid = 7103
-
- private var owner: User = _
- private var reader: User = _
- private var writer: User = _
- private var userDao: UserDao = _
- private var projectUserAccessDao: ProjectUserAccessDao = _
- private var projectResource: ProjectResource = _
- private var projectAccessResource: ProjectAccessResource = _
-
- override protected def beforeAll(): Unit = {
- initializeDBAndReplaceDSLContext()
- }
-
- override protected def beforeEach(): Unit = {
- userDao = new UserDao(getDSLContext.configuration())
- projectUserAccessDao = new ProjectUserAccessDao(getDSLContext.configuration())
- projectResource = new ProjectResource()
- projectAccessResource = new ProjectAccessResource()
-
- owner = createUser(ownerUid, "project_owner", "project-owner@test.com")
- reader = createUser(readerUid, "project_reader", "project-reader@test.com")
- writer = createUser(writerUid, "project_writer", "project-writer@test.com")
-
- cleanupTestData()
-
- userDao.insert(owner)
- userDao.insert(reader)
- userDao.insert(writer)
- }
-
- override protected def afterEach(): Unit = {
- cleanupTestData()
- }
-
- override protected def afterAll(): Unit = {
- closeConnectionPool()
- }
-
- private def createUser(uid: Int, name: String, email: String): User = {
- val user = new User
- user.setUid(uid)
- user.setName(name)
- user.setEmail(email)
- user.setPassword("password")
- user.setRole(UserRoleEnum.REGULAR)
- user
- }
-
- private def cleanupTestData(): Unit = {
- getDSLContext
- .deleteFrom(PROJECT_USER_ACCESS)
- .where(PROJECT_USER_ACCESS.UID.in(ownerUid, readerUid, writerUid))
- .execute()
-
- getDSLContext
- .deleteFrom(PROJECT)
- .where(PROJECT.OWNER_ID.eq(ownerUid))
- .execute()
-
- getDSLContext
- .deleteFrom(USER)
- .where(USER.UID.in(ownerUid, readerUid, writerUid))
- .execute()
- }
-
- "ProjectAccessResource.getProjectAccessPrivilege" should "return WRITE if granted" in {
- val project = projectResource.createProject(new SessionUser(owner), "write-project")
- val privilege = ProjectAccessResource.getProjectAccessPrivilege(project.getPid, ownerUid)
-
- assert(privilege == PrivilegeEnum.WRITE)
- assert(ProjectAccessResource.userHasWriteAccess(project.getPid, ownerUid))
- }
-
- it should "return READ if a project access row grants READ" in {
- val project = projectResource.createProject(new SessionUser(owner), "read-project")
- projectUserAccessDao.merge(
- new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ)
- )
-
- val privilege = ProjectAccessResource.getProjectAccessPrivilege(project.getPid, readerUid)
-
- assert(privilege == PrivilegeEnum.READ)
- assert(!ProjectAccessResource.userHasWriteAccess(project.getPid, readerUid))
- }
-
- it should "return NONE if the user only has access to another project" in {
- val sharedProject = projectResource.createProject(new SessionUser(owner), "shared-project")
- val privateProject = projectResource.createProject(new SessionUser(owner), "private-project")
- projectUserAccessDao.merge(
- new ProjectUserAccess(readerUid, sharedProject.getPid, PrivilegeEnum.READ)
- )
-
- val privilege =
- ProjectAccessResource.getProjectAccessPrivilege(privateProject.getPid, readerUid)
-
- assert(privilege == PrivilegeEnum.NONE)
- assert(!ProjectAccessResource.userHasWriteAccess(privateProject.getPid, readerUid))
- }
-
- it should "return WRITE and grant write access for a WRITE grantee" in {
- val project = projectResource.createProject(new SessionUser(owner), "writer-project")
- projectUserAccessDao.merge(
- new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE)
- )
-
- assert(
- ProjectAccessResource.getProjectAccessPrivilege(
- project.getPid,
- writerUid
- ) == PrivilegeEnum.WRITE
- )
- assert(ProjectAccessResource.userHasWriteAccess(project.getPid, writerUid))
- }
-
- "ProjectAccessResource.getOwner" should "return the owning user's email" in {
- val project = projectResource.createProject(new SessionUser(owner), "owned-project")
- assert(projectAccessResource.getOwner(project.getPid) == owner.getEmail)
- }
-
- "ProjectAccessResource.getAccessList" should "be empty when only the owner has access" in {
- val project = projectResource.createProject(new SessionUser(owner), "solo-project")
- // createProject grants the owner WRITE, but getAccessList excludes the owner.
- assert(projectAccessResource.getAccessList(project.getPid).asScala.isEmpty)
- }
-
- it should "list every grantee (excluding the owner) with their email, name and privilege" in {
- val project = projectResource.createProject(new SessionUser(owner), "shared-list-project")
- projectUserAccessDao.merge(new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ))
- projectUserAccessDao.merge(
- new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE)
- )
-
- val entries = projectAccessResource.getAccessList(project.getPid).asScala.toList
- assert(entries.size == 2)
- assert(!entries.map(_.email).contains(owner.getEmail)) // owner is excluded
- assert(entries.contains(AccessEntry(reader.getEmail, reader.getName, PrivilegeEnum.READ)))
- assert(entries.contains(AccessEntry(writer.getEmail, writer.getName, PrivilegeEnum.WRITE)))
- }
-
- "ProjectAccessResource.grantAccess" should "let a WRITE grantee grant READ access to another user" in {
- val project = projectResource.createProject(new SessionUser(owner), "grant-project")
- // writer is a non-owner WRITE grantee, so it is allowed to grant access.
- projectUserAccessDao.merge(
- new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE)
- )
-
- projectAccessResource.grantAccess(
- project.getPid,
- reader.getEmail,
- "READ",
- new SessionUser(writer)
- )
-
- assert(
- ProjectAccessResource.getProjectAccessPrivilege(
- project.getPid,
- readerUid
- ) == PrivilegeEnum.READ
- )
- }
-
- it should "reject granting to a placeholder account" in {
- val project = projectResource.createProject(new SessionUser(owner), "grant-placeholder-project")
- val placeholder = new User
- placeholder.setName("pj_placeholder")
- placeholder.setEmail("pj-placeholder@test.com")
- placeholder.setRole(UserRoleEnum.INACTIVE)
- placeholder.setIsPlaceholder(true)
- userDao.insert(placeholder)
- try {
- assertThrows[BadRequestException](
- projectAccessResource.grantAccess(
- project.getPid,
- "pj-placeholder@test.com",
- "READ",
- new SessionUser(owner)
- )
- )
- } finally {
- getDSLContext.deleteFrom(USER).where(USER.UID.eq(placeholder.getUid)).execute()
- }
- }
-
- it should "reject a user without write access with ForbiddenException" in {
- val project = projectResource.createProject(new SessionUser(owner), "grant-forbidden-project")
- // reader has no access to the project, so cannot grant.
- assertThrows[ForbiddenException](
- projectAccessResource.grantAccess(
- project.getPid,
- writer.getEmail,
- "READ",
- new SessionUser(reader)
- )
- )
- }
-
- "ProjectAccessResource.revokeAccess" should "remove a grantee's access" in {
- val project = projectResource.createProject(new SessionUser(owner), "revoke-project")
- projectUserAccessDao.merge(new ProjectUserAccess(readerUid, project.getPid, PrivilegeEnum.READ))
-
- projectAccessResource.revokeAccess(project.getPid, reader.getEmail, new SessionUser(owner))
-
- assert(
- ProjectAccessResource.getProjectAccessPrivilege(
- project.getPid,
- readerUid
- ) == PrivilegeEnum.NONE
- )
- }
-
- it should "reject a user without write access with ForbiddenException" in {
- val project = projectResource.createProject(new SessionUser(owner), "revoke-forbidden-project")
- projectUserAccessDao.merge(
- new ProjectUserAccess(writerUid, project.getPid, PrivilegeEnum.WRITE)
- )
-
- // reader has no write access, so cannot revoke the writer's access.
- assertThrows[ForbiddenException](
- projectAccessResource.revokeAccess(project.getPid, writer.getEmail, new SessionUser(reader))
- )
- }
-}
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala
deleted file mode 100644
index f97d3d45197..00000000000
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/ProjectResourceSpec.scala
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard.user.project
-
-import org.apache.texera.auth.SessionUser
-import org.apache.texera.dao.MockTexeraDB
-import org.apache.texera.dao.jooq.generated.Tables._
-import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum
-import org.apache.texera.dao.jooq.generated.tables.daos.{
- UserDao,
- WorkflowDao,
- WorkflowOfProjectDao,
- WorkflowOfUserDao,
- WorkflowUserAccessDao
-}
-import org.apache.texera.dao.jooq.generated.tables.pojos.{
- User,
- Workflow,
- WorkflowOfProject,
- WorkflowOfUser,
- WorkflowUserAccess
-}
-import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.matchers.should.Matchers
-import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
-
-import java.sql.Timestamp
-import java.util.UUID
-import javax.ws.rs.{BadRequestException, ForbiddenException}
-import scala.jdk.CollectionConverters._
-
-class ProjectResourceSpec
- extends AnyFlatSpec
- with Matchers
- with BeforeAndAfterAll
- with BeforeAndAfterEach
- with MockTexeraDB {
-
- private val ownerUid = 12000 + scala.util.Random.nextInt(1000)
- private val strangerUid = 14000 + scala.util.Random.nextInt(1000)
- private val testUids = Seq(ownerUid, strangerUid)
-
- private var owner: User = _
- private var stranger: User = _
- private var workflowOfProjectDao: WorkflowOfProjectDao = _
- private var userDao: UserDao = _
- private var workflowDao: WorkflowDao = _
- private var workflowOfUserDao: WorkflowOfUserDao = _
- private var workflowUserAccessDao: WorkflowUserAccessDao = _
- private var resource: ProjectResource = _
-
- override protected def beforeAll(): Unit = {
- initializeDBAndReplaceDSLContext()
- }
-
- override protected def beforeEach(): Unit = {
- workflowOfProjectDao = new WorkflowOfProjectDao(getDSLContext.configuration())
- userDao = new UserDao(getDSLContext.configuration())
- workflowDao = new WorkflowDao(getDSLContext.configuration())
- workflowOfUserDao = new WorkflowOfUserDao(getDSLContext.configuration())
- workflowUserAccessDao = new WorkflowUserAccessDao(getDSLContext.configuration())
- resource = new ProjectResource()
-
- owner = makeUser(ownerUid, "proj_owner")
- stranger = makeUser(strangerUid, "proj_stranger")
-
- cleanupTestData()
- userDao.insert(owner)
- userDao.insert(stranger)
- }
-
- override protected def afterEach(): Unit = {
- cleanupTestData()
- }
-
- override protected def afterAll(): Unit = {
- closeConnectionPool()
- }
-
- private def cleanupTestData(): Unit = {
- val ctx = getDSLContext
- // projects owned by our test users (pids are DB-generated)
- val pids = ctx
- .select(PROJECT.PID)
- .from(PROJECT)
- .where(PROJECT.OWNER_ID.in(testUids.map(Integer.valueOf): _*))
- .fetchInto(classOf[Integer])
- .asScala
- .toList
- // workflows our test users came to own
- val wids = ctx
- .select(WORKFLOW_OF_USER.WID)
- .from(WORKFLOW_OF_USER)
- .where(WORKFLOW_OF_USER.UID.in(testUids.map(Integer.valueOf): _*))
- .fetchInto(classOf[Integer])
- .asScala
- .toList
-
- if (pids.nonEmpty) {
- ctx.deleteFrom(WORKFLOW_OF_PROJECT).where(WORKFLOW_OF_PROJECT.PID.in(pids: _*)).execute()
- ctx.deleteFrom(PROJECT_USER_ACCESS).where(PROJECT_USER_ACCESS.PID.in(pids: _*)).execute()
- }
- ctx
- .deleteFrom(PROJECT_USER_ACCESS)
- .where(PROJECT_USER_ACCESS.UID.in(testUids.map(Integer.valueOf): _*))
- .execute()
- ctx.deleteFrom(PROJECT).where(PROJECT.OWNER_ID.in(testUids.map(Integer.valueOf): _*)).execute()
-
- if (wids.nonEmpty) {
- ctx.deleteFrom(WORKFLOW_OF_PROJECT).where(WORKFLOW_OF_PROJECT.WID.in(wids: _*)).execute()
- ctx.deleteFrom(WORKFLOW_USER_ACCESS).where(WORKFLOW_USER_ACCESS.WID.in(wids: _*)).execute()
- ctx.deleteFrom(WORKFLOW_OF_USER).where(WORKFLOW_OF_USER.WID.in(wids: _*)).execute()
- ctx.deleteFrom(WORKFLOW).where(WORKFLOW.WID.in(wids: _*)).execute()
- }
- ctx.deleteFrom(USER).where(USER.UID.in(testUids.map(Integer.valueOf): _*)).execute()
- }
-
- private def makeUser(uid: Int, name: String): User = {
- val user = new User
- user.setUid(Integer.valueOf(uid))
- user.setName(name)
- user.setEmail(s"$name@test.com")
- user.setPassword("password")
- user
- }
-
- private def session(user: User): SessionUser = new SessionUser(user)
-
- /** Seeds a workflow owned by the given user with WRITE access (so hasReadAccess passes). */
- private def seedWorkflow(uid: Int): Integer = {
- val wid = Integer.valueOf(16000 + scala.util.Random.nextInt(100000))
- val workflow = new Workflow
- workflow.setWid(wid)
- workflow.setName("wf_" + UUID.randomUUID().toString.substring(0, 8))
- workflow.setContent("""{"operators":[],"links":[]}""")
- workflow.setDescription("")
- workflow.setIsPublic(false)
- workflow.setCreationTime(new Timestamp(System.currentTimeMillis()))
- workflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis()))
- workflowDao.insert(workflow)
-
- val ownership = new WorkflowOfUser
- ownership.setUid(Integer.valueOf(uid))
- ownership.setWid(wid)
- workflowOfUserDao.insert(ownership)
-
- val access = new WorkflowUserAccess
- access.setUid(Integer.valueOf(uid))
- access.setWid(wid)
- access.setPrivilege(PrivilegeEnum.WRITE)
- workflowUserAccessDao.insert(access)
- wid
- }
-
- private def workflowOfProjectCount(wid: Integer, pid: Integer): Int =
- getDSLContext.fetchCount(
- WORKFLOW_OF_PROJECT,
- WORKFLOW_OF_PROJECT.WID.eq(wid).and(WORKFLOW_OF_PROJECT.PID.eq(pid))
- )
-
- behavior of "ProjectResource"
-
- it should "create a project owned by the user and make it retrievable with a WRITE access row" in {
- val created = resource.createProject(session(owner), "my_project")
-
- created.getName shouldBe "my_project"
- created.getOwnerId shouldBe Integer.valueOf(ownerUid)
- resource.getProject(created.getPid).getName shouldBe "my_project"
-
- val privilege = getDSLContext
- .select(PROJECT_USER_ACCESS.PRIVILEGE)
- .from(PROJECT_USER_ACCESS)
- .where(
- PROJECT_USER_ACCESS.PID
- .eq(created.getPid)
- .and(PROJECT_USER_ACCESS.UID.eq(Integer.valueOf(ownerUid)))
- )
- .fetchOne(0, classOf[PrivilegeEnum])
- privilege shouldBe PrivilegeEnum.WRITE
- }
-
- it should "list no projects for a user who owns none and all projects once created" in {
- resource.getProjectList(session(stranger)).asScala shouldBe empty
-
- resource.createProject(session(owner), "p1")
- resource.createProject(session(owner), "p2")
-
- resource.getProjectList(session(owner)).asScala.map(_.name).toSet shouldBe Set("p1", "p2")
- }
-
- it should "rename a project and reject a blank name" in {
- val pid = resource.createProject(session(owner), "before").getPid
-
- resource.updateProjectName(pid, "after")
- resource.getProject(pid).getName shouldBe "after"
-
- assertThrows[BadRequestException] {
- resource.updateProjectName(pid, " ")
- }
- // the rejected rename left the previous value intact
- resource.getProject(pid).getName shouldBe "after"
- }
-
- it should "update a project description via a re-read" in {
- val pid = resource.createProject(session(owner), "p").getPid
-
- resource.updateProjectDescription(pid, "a new description")
-
- resource.getProject(pid).getDescription shouldBe "a new description"
- }
-
- it should "add a workflow to a project, stay idempotent, and reject a user without access" in {
- val pid = resource.createProject(session(owner), "p").getPid
- val wid = seedWorkflow(ownerUid)
-
- resource.addWorkflowToProject(pid, wid, session(owner))
- workflowOfProjectCount(wid, pid) shouldBe 1
-
- // a second add for the same pair must not create a duplicate mapping
- resource.addWorkflowToProject(pid, wid, session(owner))
- workflowOfProjectCount(wid, pid) shouldBe 1
-
- // the stranger has no access to this workflow
- assertThrows[ForbiddenException] {
- resource.addWorkflowToProject(pid, wid, session(stranger))
- }
- }
-
- it should "remove a workflow-to-project mapping" in {
- val pid = resource.createProject(session(owner), "p").getPid
- val wid = seedWorkflow(ownerUid)
- resource.addWorkflowToProject(pid, wid, session(owner))
- workflowOfProjectCount(wid, pid) shouldBe 1
-
- resource.deleteWorkflowFromProject(pid, wid)
-
- workflowOfProjectCount(wid, pid) shouldBe 0
- }
-
- it should "accept both 3- and 6-digit hex colours and persist the last one" in {
- val pid = resource.createProject(session(owner), "p").getPid
-
- resource.updateProjectColor(pid, "AABBCC", session(owner))
- resource.getProject(pid).getColor shouldBe "AABBCC"
-
- // The shorthand form is legal too, and the value is stored verbatim rather than expanded.
- resource.updateProjectColor(pid, "f0a", session(owner))
- resource.getProject(pid).getColor shouldBe "f0a"
- }
-
- it should "reject colours that are not 3 or 6 hex digits, leaving the stored one intact" in {
- val pid = resource.createProject(session(owner), "p").getPid
- resource.updateProjectColor(pid, "123456", session(owner))
-
- // Wrong length, and a right-length value with a non-hex digit: this exercises both the
- // length and hex-digit validation branches in updateProjectColor.
- Seq("12345", "1234567", "GGGGGG", "12G", "").foreach { bad =>
- withClue(s"colour '$bad': ") {
- assertThrows[BadRequestException] {
- resource.updateProjectColor(pid, bad, session(owner))
- }
- }
- }
-
- resource.getProject(pid).getColor shouldBe "123456"
- }
-
- it should "reject a null colour before dereferencing it" in {
- val pid = resource.createProject(session(owner), "p").getPid
-
- // The null check has to come first; without it the length read is an NPE rather than a 400.
- assertThrows[BadRequestException] {
- resource.updateProjectColor(pid, null, session(owner))
- }
- }
-
- it should "clear a project's colour" in {
- val pid = resource.createProject(session(owner), "p").getPid
- resource.updateProjectColor(pid, "ABCDEF", session(owner))
-
- resource.deleteProjectColor(pid)
-
- resource.getProject(pid).getColor shouldBe null
- }
-
- it should "list only the workflows belonging to the given project" in {
- val pid = resource.createProject(session(owner), "p").getPid
- val other = resource.createProject(session(owner), "other").getPid
- val inProject = seedWorkflow(ownerUid)
- val elsewhere = seedWorkflow(ownerUid)
- resource.addWorkflowToProject(pid, inProject, session(owner))
- resource.addWorkflowToProject(other, elsewhere, session(owner))
-
- // Two projects each holding one workflow, so a filter that ignored the pid would return both.
- resource.listProjectWorkflows(pid, session(owner)).map(_.workflow.getWid) shouldBe List(
- inProject
- )
- resource.listProjectWorkflows(other, session(owner)).map(_.workflow.getWid) shouldBe List(
- elsewhere
- )
- }
-
- it should "return no workflows for a project that holds none" in {
- val pid = resource.createProject(session(owner), "empty").getPid
-
- resource.listProjectWorkflows(pid, session(owner)) shouldBe empty
- }
-
- it should "delete a project" in {
- val pid = resource.createProject(session(owner), "doomed").getPid
- resource.getProject(pid) should not be null
-
- resource.deleteProject(pid)
-
- resource.getProject(pid) shouldBe null
- }
-
- behavior of "ProjectResource.addExportedFileToProject"
-
- it should "return an empty status when the workflow belongs to no project" in {
- val wid = seedWorkflow(ownerUid)
- ProjectResource.addExportedFileToProject(Integer.valueOf(ownerUid), wid, "out.csv") shouldBe ""
- }
-
- it should "name the single project the workflow belongs to" in {
- val wid = seedWorkflow(ownerUid)
- val pid = resource.createProject(session(owner), "only_project").getPid
- workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid))
-
- ProjectResource.addExportedFileToProject(
- Integer.valueOf(ownerUid),
- wid,
- "out.csv"
- ) shouldBe "and added to project: only_project"
- }
-
- it should "list every project the workflow belongs to when there are several" in {
- val wid = seedWorkflow(ownerUid)
- val pid1 = resource.createProject(session(owner), "alpha").getPid
- val pid2 = resource.createProject(session(owner), "beta").getPid
- workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid1))
- workflowOfProjectDao.insert(new WorkflowOfProject(wid, pid2))
-
- val status = ProjectResource.addExportedFileToProject(Integer.valueOf(ownerUid), wid, "out.csv")
-
- status should startWith("and added to projects: ")
- status should include("alpha")
- status should include("beta")
- }
-}
diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala
deleted file mode 100644
index 4d532a9d44e..00000000000
--- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/project/PublicProjectResourceSpec.scala
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.texera.web.resource.dashboard.user.project
-
-import org.apache.texera.auth.SessionUser
-import org.apache.texera.dao.MockTexeraDB
-import org.apache.texera.dao.jooq.generated.Tables.{PROJECT, PROJECT_USER_ACCESS, PUBLIC_PROJECT}
-import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum}
-import org.apache.texera.dao.jooq.generated.tables.daos.{ProjectDao, UserDao}
-import org.apache.texera.dao.jooq.generated.tables.pojos.{Project, User}
-import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
-import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.matchers.should.Matchers
-
-import java.util
-import java.util.UUID
-import scala.jdk.CollectionConverters._
-
-class PublicProjectResourceSpec
- extends AnyFlatSpec
- with Matchers
- with BeforeAndAfterAll
- with BeforeAndAfterEach
- with MockTexeraDB {
-
- // MockTexeraDB gives each suite its own database, so a fixed uid is isolated and reproducible.
- private val testUid = 90001
- private var projectDao: ProjectDao = _
- private var sessionUser: SessionUser = _
- private val resource = new PublicProjectResource
-
- private def makeUser(uid: Int, name: String): User = {
- val user = new User
- user.setUid(uid)
- user.setName(name)
- user.setEmail(
- s"public_project_spec_${uid}_${UUID.randomUUID().toString.substring(0, 8)}@example.com"
- )
- user.setPassword("password")
- user.setRole(UserRoleEnum.ADMIN)
- user
- }
-
- override protected def beforeAll(): Unit = {
- initializeDBAndReplaceDSLContext()
- val user = makeUser(testUid, "public_project_owner")
- new UserDao(getDSLContext.configuration()).insert(user)
- sessionUser = new SessionUser(user)
- projectDao = new ProjectDao(getDSLContext.configuration())
- }
-
- override protected def afterAll(): Unit = closeConnectionPool()
-
- // Remove the per-test project rows (children before parents) after each test; the owner
- // user seeded once in beforeAll is intentionally kept for the whole suite.
- override protected def afterEach(): Unit = {
- getDSLContext.deleteFrom(PUBLIC_PROJECT).where(PUBLIC_PROJECT.UID.eq(testUid)).execute()
- getDSLContext
- .deleteFrom(PROJECT_USER_ACCESS)
- .where(PROJECT_USER_ACCESS.UID.eq(testUid))
- .execute()
- getDSLContext.deleteFrom(PROJECT).where(PROJECT.OWNER_ID.eq(testUid)).execute()
- }
-
- // Insert a project owned by the test user; the generated pid is populated on the pojo.
- private def seedProject(name: String): Project = {
- val project = new Project(null, name, null, Integer.valueOf(testUid), null, null)
- projectDao.insert(project)
- project
- }
-
- "getType" should "report a project without a public flag as Private" in {
- val project = seedProject("p_private")
- resource.getType(project.getPid) shouldBe "Private"
- }
-
- "makePublic" should "flag a project public so getType reports Public" in {
- val project = seedProject("p_makepublic")
- resource.makePublic(project.getPid, sessionUser)
- resource.getType(project.getPid) shouldBe "Public"
- }
-
- "makePrivate" should "revert a public project so getType reports Private again" in {
- val project = seedProject("p_makeprivate")
- resource.makePublic(project.getPid, sessionUser)
- resource.getType(project.getPid) shouldBe "Public"
-
- resource.makePrivate(project.getPid)
- resource.getType(project.getPid) shouldBe "Private"
- }
-
- "listPublicProjects" should "return an empty list when no project is public" in {
- seedProject("p_still_private")
- resource.listPublicProjects().asScala shouldBe empty
- }
-
- it should "return only the currently-public projects with their name and owner" in {
- val publicProject = seedProject("p_public")
- seedProject("p_hidden")
- resource.makePublic(publicProject.getPid, sessionUser)
-
- val listed = resource.listPublicProjects().asScala
- listed.map(_.pid) shouldBe Seq(publicProject.getPid)
- listed.head.name shouldBe "p_public"
- listed.head.owner shouldBe "public_project_owner"
- }
-
- // NOTE: despite the name, addPublicProjects does NOT set the public flag — it grants the
- // caller READ access to each project (a ProjectUserAccess row) and leaves getType Private.
- "addPublicProjects" should "grant the caller READ access to each listed project" in {
- val p1 = seedProject("p_add1")
- val p2 = seedProject("p_add2")
-
- resource.addPublicProjects(util.Arrays.asList(p1.getPid, p2.getPid), sessionUser)
-
- val grantedPids = getDSLContext
- .select(PROJECT_USER_ACCESS.PID)
- .from(PROJECT_USER_ACCESS)
- .where(
- PROJECT_USER_ACCESS.UID
- .eq(testUid)
- .and(PROJECT_USER_ACCESS.PRIVILEGE.eq(PrivilegeEnum.READ))
- )
- .fetchInto(classOf[Integer])
- .asScala
- grantedPids should contain allOf (p1.getPid, p2.getPid)
- resource.getType(p1.getPid) shouldBe "Private" // access grant is not the public flag
- }
-}
diff --git a/common/config/src/main/resources/default.conf b/common/config/src/main/resources/default.conf
index 6b83f50f6b3..1a17e89aed0 100644
--- a/common/config/src/main/resources/default.conf
+++ b/common/config/src/main/resources/default.conf
@@ -55,9 +55,6 @@ gui {
your_work_enabled = true
your_work_enabled = ${?GUI_TABS_YOUR_WORK_ENABLED}
- projects_enabled = false
- projects_enabled = ${?GUI_TABS_PROJECTS_ENABLED}
-
workflows_enabled = true
workflows_enabled = ${?GUI_TABS_WORKFLOWS_ENABLED}
diff --git a/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala b/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala
index 86449e0a11d..5948a1f28d5 100644
--- a/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala
+++ b/common/dao/src/test/scala/org/apache/texera/dao/MockTexeraDB.scala
@@ -84,7 +84,6 @@ object MockTexeraDB {
val replacementText =
"""CREATE INDEX idx_workflow_name_description_content ON workflow USING GIN (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '') || ' ' || COALESCE(content, '')));
|CREATE INDEX idx_user_name ON "user" USING GIN (to_tsvector('english', COALESCE(name, '')));
- |CREATE INDEX idx_user_project_name_description ON project USING GIN (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '')));
|CREATE INDEX idx_dataset_name_description ON dataset USING GIN (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(description, '')));
|CREATE INDEX idx_dataset_version_name ON dataset_version USING GIN (to_tsvector('english', COALESCE(name, '')));""".stripMargin
diff --git a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala
index f27f9463d8e..eddaa13aa1a 100644
--- a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala
+++ b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala
@@ -416,7 +416,6 @@ class ConfigResourceSpec
"workflow_enabled",
"dataset_enabled",
"your_work_enabled",
- "projects_enabled",
"workflows_enabled",
"datasets_enabled",
"compute_enabled",
diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala
index bef9c38cc21..e439ad35a4f 100644
--- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala
+++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala
@@ -29,7 +29,7 @@ object WorkflowAccessResource {
/**
* Whether the given user holds a direct WRITE grant on the given workflow.
*
- * Only the direct WORKFLOW_USER_ACCESS grant is consulted — no project or
+ * Only the direct WORKFLOW_USER_ACCESS grant is consulted — no
* public-visibility fallback — so the notebook endpoints stay self-contained.
*
* @param wid workflow id
diff --git a/sql/changelog.xml b/sql/changelog.xml
index bdba73339de..9108e1804cf 100644
--- a/sql/changelog.xml
+++ b/sql/changelog.xml
@@ -68,6 +68,11 @@
+
+
+
+
+