From e1f1b0f581838330acba0e643555a67c7458e55a Mon Sep 17 00:00:00 2001 From: tantaka tomokazu Date: Thu, 10 Oct 2024 15:18:05 +0900 Subject: [PATCH] feat(server): Projects Recycle bin (#1169) Co-authored-by: tomokazu tantaka --- server/e2e/gql_project_test.go | 82 ++++++- server/gql/project.graphql | 6 +- server/internal/adapter/gql/generated.go | 229 ++++++++++++++++-- .../adapter/gql/gqlmodel/models_gen.go | 2 + server/internal/adapter/gql/loader_project.go | 22 ++ .../adapter/gql/resolver_mutation_project.go | 1 + server/internal/adapter/gql/resolver_query.go | 6 +- .../internal/infrastructure/memory/project.go | 22 ++ .../infrastructure/mongo/mongodoc/project.go | 2 + .../internal/infrastructure/mongo/project.go | 13 + server/internal/usecase/interactor/project.go | 8 + .../usecase/interactor/project_test.go | 1 + server/internal/usecase/interfaces/project.go | 2 + server/internal/usecase/repo/project.go | 1 + server/pkg/project/project.go | 9 + web/src/services/gql/__gen__/graphql.ts | 8 +- 16 files changed, 384 insertions(+), 30 deletions(-) diff --git a/server/e2e/gql_project_test.go b/server/e2e/gql_project_test.go index aad2e57ad1..4cf817a2c1 100644 --- a/server/e2e/gql_project_test.go +++ b/server/e2e/gql_project_test.go @@ -449,6 +449,84 @@ func TestSortByUpdatedAt(t *testing.T) { edges.Length().Equal(3) edges.Element(0).Object().Value("node").Object().Value("name").Equal("project2-test") // 'project2' is first - edges.Element(1).Object().Value("node").Object().Value("name").Equal("project3-test") - edges.Element(2).Object().Value("node").Object().Value("name").Equal("project1-test") +} + +// go test -v -run TestDeleteProjects ./e2e/... + +func TestDeleteProjects(t *testing.T) { + + e := StartServer(t, &config.Config{ + Origins: []string{"https://example.com"}, + AuthSrv: config.AuthSrvConfig{ + Disabled: true, + }, + }, true, baseSeeder) + + createProject(e, "project1-test") + project2ID := createProject(e, "project2-test") + createProject(e, "project3-test") + + // Deleted 'project2' + requestBody := GraphQLRequest{ + OperationName: "UpdateProject", + Query: `mutation UpdateProject($input: UpdateProjectInput!) { + updateProject(input: $input) { + project { + id + name + isDeleted + updatedAt + __typename + } + __typename + } + }`, + Variables: map[string]any{ + "input": map[string]any{ + "projectId": project2ID, + "deleted": true, + }, + }, + } + + e.POST("/api/graphql"). + WithHeader("Origin", "https://example.com"). + WithHeader("X-Reearth-Debug-User", uID.String()). + WithHeader("Content-Type", "application/json"). + WithJSON(requestBody). + Expect(). + Status(http.StatusOK). + JSON() + + // check + requestBody = GraphQLRequest{ + OperationName: "GetDeletedProjects", + Query: ` + query GetDeletedProjects($teamId: ID!) { + deletedProjects(teamId: $teamId) { + nodes { + id + name + isDeleted + } + totalCount + } + }`, + Variables: map[string]any{ + "teamId": wID, + }, + } + deletedProjects := e.POST("/api/graphql"). + WithHeader("Origin", "https://example.com"). + WithHeader("X-Reearth-Debug-User", uID.String()). + WithHeader("Content-Type", "application/json"). + WithJSON(requestBody). + Expect(). + Status(http.StatusOK). + JSON(). + Object().Value("data").Object().Value("deletedProjects").Object() + + deletedProjects.Value("totalCount").Equal(1) + deletedProjects.Value("nodes").Array().Length().Equal(1) + deletedProjects.Value("nodes").Array().First().Object().Value("name").Equal("project2-test") } diff --git a/server/gql/project.graphql b/server/gql/project.graphql index 9ce328c2fd..27a1cb723f 100644 --- a/server/gql/project.graphql +++ b/server/gql/project.graphql @@ -24,6 +24,7 @@ type Project implements Node { enableGa: Boolean! trackingId: String! starred: Boolean! + isDeleted: Boolean! } type ProjectAliasAvailability { @@ -80,6 +81,7 @@ input UpdateProjectInput { trackingId: String sceneId: ID starred: Boolean + deleted: Boolean } input PublishProjectInput { @@ -140,13 +142,13 @@ type ProjectEdge { extend type Query { projects( teamId: ID! - includeArchived: Boolean pagination: Pagination keyword: String sort: ProjectSort - ): ProjectConnection! + ): ProjectConnection! # not included deleted projects checkProjectAlias(alias: String!): ProjectAliasAvailability! starredProjects(teamId: ID!): ProjectConnection! + deletedProjects(teamId: ID!): ProjectConnection! } extend type Mutation { diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go index 6b88fddd69..75320de377 100644 --- a/server/internal/adapter/gql/generated.go +++ b/server/internal/adapter/gql/generated.go @@ -843,6 +843,7 @@ type ComplexityRoot struct { ImageURL func(childComplexity int) int IsArchived func(childComplexity int) int IsBasicAuthActive func(childComplexity int) int + IsDeleted func(childComplexity int) int Name func(childComplexity int) int PublicDescription func(childComplexity int) int PublicImage func(childComplexity int) int @@ -1009,13 +1010,14 @@ type ComplexityRoot struct { CheckProjectAlias func(childComplexity int, alias string) int DatasetSchemas func(childComplexity int, sceneID gqlmodel.ID, first *int, last *int, after *usecasex.Cursor, before *usecasex.Cursor) int Datasets func(childComplexity int, datasetSchemaID gqlmodel.ID, first *int, last *int, after *usecasex.Cursor, before *usecasex.Cursor) int + DeletedProjects func(childComplexity int, teamID gqlmodel.ID) int Layer func(childComplexity int, id gqlmodel.ID) int Me func(childComplexity int) int Node func(childComplexity int, id gqlmodel.ID, typeArg gqlmodel.NodeType) int Nodes func(childComplexity int, id []gqlmodel.ID, typeArg gqlmodel.NodeType) int Plugin func(childComplexity int, id gqlmodel.ID) int Plugins func(childComplexity int, id []gqlmodel.ID) int - Projects func(childComplexity int, teamID gqlmodel.ID, includeArchived *bool, pagination *gqlmodel.Pagination, keyword *string, sort *gqlmodel.ProjectSort) int + Projects func(childComplexity int, teamID gqlmodel.ID, pagination *gqlmodel.Pagination, keyword *string, sort *gqlmodel.ProjectSort) int PropertySchema func(childComplexity int, id gqlmodel.ID) int PropertySchemas func(childComplexity int, id []gqlmodel.ID) int Scene func(childComplexity int, projectID gqlmodel.ID) int @@ -1700,9 +1702,10 @@ type QueryResolver interface { Layer(ctx context.Context, id gqlmodel.ID) (gqlmodel.Layer, error) Plugin(ctx context.Context, id gqlmodel.ID) (*gqlmodel.Plugin, error) Plugins(ctx context.Context, id []gqlmodel.ID) ([]*gqlmodel.Plugin, error) - Projects(ctx context.Context, teamID gqlmodel.ID, includeArchived *bool, pagination *gqlmodel.Pagination, keyword *string, sort *gqlmodel.ProjectSort) (*gqlmodel.ProjectConnection, error) + Projects(ctx context.Context, teamID gqlmodel.ID, pagination *gqlmodel.Pagination, keyword *string, sort *gqlmodel.ProjectSort) (*gqlmodel.ProjectConnection, error) CheckProjectAlias(ctx context.Context, alias string) (*gqlmodel.ProjectAliasAvailability, error) StarredProjects(ctx context.Context, teamID gqlmodel.ID) (*gqlmodel.ProjectConnection, error) + DeletedProjects(ctx context.Context, teamID gqlmodel.ID) (*gqlmodel.ProjectConnection, error) PropertySchema(ctx context.Context, id gqlmodel.ID) (*gqlmodel.PropertySchema, error) PropertySchemas(ctx context.Context, id []gqlmodel.ID) ([]*gqlmodel.PropertySchema, error) Scene(ctx context.Context, projectID gqlmodel.ID) (*gqlmodel.Scene, error) @@ -5749,6 +5752,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Project.IsBasicAuthActive(childComplexity), true + case "Project.isDeleted": + if e.complexity.Project.IsDeleted == nil { + break + } + + return e.complexity.Project.IsDeleted(childComplexity), true + case "Project.name": if e.complexity.Project.Name == nil { break @@ -6573,6 +6583,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Datasets(childComplexity, args["datasetSchemaId"].(gqlmodel.ID), args["first"].(*int), args["last"].(*int), args["after"].(*usecasex.Cursor), args["before"].(*usecasex.Cursor)), true + case "Query.deletedProjects": + if e.complexity.Query.DeletedProjects == nil { + break + } + + args, err := ec.field_Query_deletedProjects_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.DeletedProjects(childComplexity, args["teamId"].(gqlmodel.ID)), true + case "Query.layer": if e.complexity.Query.Layer == nil { break @@ -6650,7 +6672,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.Projects(childComplexity, args["teamId"].(gqlmodel.ID), args["includeArchived"].(*bool), args["pagination"].(*gqlmodel.Pagination), args["keyword"].(*string), args["sort"].(*gqlmodel.ProjectSort)), true + return e.complexity.Query.Projects(childComplexity, args["teamId"].(gqlmodel.ID), args["pagination"].(*gqlmodel.Pagination), args["keyword"].(*string), args["sort"].(*gqlmodel.ProjectSort)), true case "Query.propertySchema": if e.complexity.Query.PropertySchema == nil { @@ -9519,6 +9541,7 @@ extend type Mutation { enableGa: Boolean! trackingId: String! starred: Boolean! + isDeleted: Boolean! } type ProjectAliasAvailability { @@ -9575,6 +9598,7 @@ input UpdateProjectInput { trackingId: String sceneId: ID starred: Boolean + deleted: Boolean } input PublishProjectInput { @@ -9635,13 +9659,13 @@ type ProjectEdge { extend type Query { projects( teamId: ID! - includeArchived: Boolean pagination: Pagination keyword: String sort: ProjectSort ): ProjectConnection! checkProjectAlias(alias: String!): ProjectAliasAvailability! starredProjects(teamId: ID!): ProjectConnection! + deletedProjects(teamId: ID!): ProjectConnection! } extend type Mutation { @@ -12615,6 +12639,21 @@ func (ec *executionContext) field_Query_datasets_args(ctx context.Context, rawAr return args, nil } +func (ec *executionContext) field_Query_deletedProjects_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 gqlmodel.ID + if tmp, ok := rawArgs["teamId"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("teamId")) + arg0, err = ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["teamId"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_layer_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -12720,42 +12759,33 @@ func (ec *executionContext) field_Query_projects_args(ctx context.Context, rawAr } } args["teamId"] = arg0 - var arg1 *bool - if tmp, ok := rawArgs["includeArchived"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includeArchived")) - arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) - if err != nil { - return nil, err - } - } - args["includeArchived"] = arg1 - var arg2 *gqlmodel.Pagination + var arg1 *gqlmodel.Pagination if tmp, ok := rawArgs["pagination"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination")) - arg2, err = ec.unmarshalOPagination2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐPagination(ctx, tmp) + arg1, err = ec.unmarshalOPagination2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐPagination(ctx, tmp) if err != nil { return nil, err } } - args["pagination"] = arg2 - var arg3 *string + args["pagination"] = arg1 + var arg2 *string if tmp, ok := rawArgs["keyword"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("keyword")) - arg3, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + arg2, err = ec.unmarshalOString2ᚖstring(ctx, tmp) if err != nil { return nil, err } } - args["keyword"] = arg3 - var arg4 *gqlmodel.ProjectSort + args["keyword"] = arg2 + var arg3 *gqlmodel.ProjectSort if tmp, ok := rawArgs["sort"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("sort")) - arg4, err = ec.unmarshalOProjectSort2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐProjectSort(ctx, tmp) + arg3, err = ec.unmarshalOProjectSort2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐProjectSort(ctx, tmp) if err != nil { return nil, err } } - args["sort"] = arg4 + args["sort"] = arg3 return args, nil } @@ -39845,6 +39875,50 @@ func (ec *executionContext) fieldContext_Project_starred(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _Project_isDeleted(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Project) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Project_isDeleted(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsDeleted, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Project_isDeleted(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Project", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _ProjectAliasAvailability_alias(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.ProjectAliasAvailability) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ProjectAliasAvailability_alias(ctx, field) if err != nil { @@ -40072,6 +40146,8 @@ func (ec *executionContext) fieldContext_ProjectConnection_nodes(ctx context.Con return ec.fieldContext_Project_trackingId(ctx, field) case "starred": return ec.fieldContext_Project_starred(ctx, field) + case "isDeleted": + return ec.fieldContext_Project_isDeleted(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -40307,6 +40383,8 @@ func (ec *executionContext) fieldContext_ProjectEdge_node(ctx context.Context, f return ec.fieldContext_Project_trackingId(ctx, field) case "starred": return ec.fieldContext_Project_starred(ctx, field) + case "isDeleted": + return ec.fieldContext_Project_isDeleted(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -40403,6 +40481,8 @@ func (ec *executionContext) fieldContext_ProjectPayload_project(ctx context.Cont return ec.fieldContext_Project_trackingId(ctx, field) case "starred": return ec.fieldContext_Project_starred(ctx, field) + case "isDeleted": + return ec.fieldContext_Project_isDeleted(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -45154,7 +45234,7 @@ func (ec *executionContext) _Query_projects(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().Projects(rctx, fc.Args["teamId"].(gqlmodel.ID), fc.Args["includeArchived"].(*bool), fc.Args["pagination"].(*gqlmodel.Pagination), fc.Args["keyword"].(*string), fc.Args["sort"].(*gqlmodel.ProjectSort)) + return ec.resolvers.Query().Projects(rctx, fc.Args["teamId"].(gqlmodel.ID), fc.Args["pagination"].(*gqlmodel.Pagination), fc.Args["keyword"].(*string), fc.Args["sort"].(*gqlmodel.ProjectSort)) }) if err != nil { ec.Error(ctx, err) @@ -45331,6 +45411,71 @@ func (ec *executionContext) fieldContext_Query_starredProjects(ctx context.Conte return fc, nil } +func (ec *executionContext) _Query_deletedProjects(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_deletedProjects(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().DeletedProjects(rctx, fc.Args["teamId"].(gqlmodel.ID)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*gqlmodel.ProjectConnection) + fc.Result = res + return ec.marshalNProjectConnection2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐProjectConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_deletedProjects(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_ProjectConnection_edges(ctx, field) + case "nodes": + return ec.fieldContext_ProjectConnection_nodes(ctx, field) + case "pageInfo": + return ec.fieldContext_ProjectConnection_pageInfo(ctx, field) + case "totalCount": + return ec.fieldContext_ProjectConnection_totalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ProjectConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_deletedProjects_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_propertySchema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_propertySchema(ctx, field) if err != nil { @@ -47727,6 +47872,8 @@ func (ec *executionContext) fieldContext_Scene_project(ctx context.Context, fiel return ec.fieldContext_Project_trackingId(ctx, field) case "starred": return ec.fieldContext_Project_starred(ctx, field) + case "isDeleted": + return ec.fieldContext_Project_isDeleted(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Project", field.Name) }, @@ -62133,7 +62280,7 @@ func (ec *executionContext) unmarshalInputUpdateProjectInput(ctx context.Context asMap[k] = v } - fieldsInOrder := [...]string{"projectId", "name", "description", "archived", "isBasicAuthActive", "basicAuthUsername", "basicAuthPassword", "alias", "imageUrl", "publicTitle", "publicDescription", "publicImage", "publicNoIndex", "deleteImageUrl", "deletePublicImage", "enableGa", "trackingId", "sceneId", "starred"} + fieldsInOrder := [...]string{"projectId", "name", "description", "archived", "isBasicAuthActive", "basicAuthUsername", "basicAuthPassword", "alias", "imageUrl", "publicTitle", "publicDescription", "publicImage", "publicNoIndex", "deleteImageUrl", "deletePublicImage", "enableGa", "trackingId", "sceneId", "starred", "deleted"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -62273,6 +62420,13 @@ func (ec *executionContext) unmarshalInputUpdateProjectInput(ctx context.Context return it, err } it.Starred = data + case "deleted": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("deleted")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.Deleted = data } } @@ -71030,6 +71184,11 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "isDeleted": + out.Values[i] = ec._Project_isDeleted(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -72927,6 +73086,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "deletedProjects": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_deletedProjects(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "propertySchema": field := field diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go index 44d7f54624..dcdc8ba0fc 100644 --- a/server/internal/adapter/gql/gqlmodel/models_gen.go +++ b/server/internal/adapter/gql/gqlmodel/models_gen.go @@ -1174,6 +1174,7 @@ type Project struct { EnableGa bool `json:"enableGa"` TrackingID string `json:"trackingId"` Starred bool `json:"starred"` + IsDeleted bool `json:"isDeleted"` } func (Project) IsNode() {} @@ -1869,6 +1870,7 @@ type UpdateProjectInput struct { TrackingID *string `json:"trackingId,omitempty"` SceneID *ID `json:"sceneId,omitempty"` Starred *bool `json:"starred,omitempty"` + Deleted *bool `json:"deleted,omitempty"` } type UpdatePropertyItemInput struct { diff --git a/server/internal/adapter/gql/loader_project.go b/server/internal/adapter/gql/loader_project.go index 8b0c340802..f2921540fb 100644 --- a/server/internal/adapter/gql/loader_project.go +++ b/server/internal/adapter/gql/loader_project.go @@ -101,6 +101,28 @@ func (c *ProjectLoader) FindStarredByWorkspace(ctx context.Context, wsID gqlmode }, nil } +func (c *ProjectLoader) FindDeletedByWorkspace(ctx context.Context, wsID gqlmodel.ID) (*gqlmodel.ProjectConnection, error) { + tid, err := gqlmodel.ToID[accountdomain.Workspace](wsID) + if err != nil { + return nil, err + } + + res, err := c.usecase.FindDeletedByWorkspace(ctx, tid, getOperator(ctx)) + if err != nil { + return nil, err + } + + nodes := make([]*gqlmodel.Project, 0, len(res)) + for _, p := range res { + nodes = append(nodes, gqlmodel.ToProject(p)) + } + + return &gqlmodel.ProjectConnection{ + Nodes: nodes, + TotalCount: len(nodes), + }, nil +} + // data loaders type ProjectDataLoader interface { diff --git a/server/internal/adapter/gql/resolver_mutation_project.go b/server/internal/adapter/gql/resolver_mutation_project.go index cbba930a63..508f5623cf 100644 --- a/server/internal/adapter/gql/resolver_mutation_project.go +++ b/server/internal/adapter/gql/resolver_mutation_project.go @@ -79,6 +79,7 @@ func (r *mutationResolver) UpdateProject(ctx context.Context, input gqlmodel.Upd EnableGa: input.EnableGa, TrackingID: input.TrackingID, Starred: input.Starred, + Deleted: input.Deleted, SceneID: gqlmodel.ToIDRef[id.Scene](input.SceneID), }, getOperator(ctx)) if err != nil { diff --git a/server/internal/adapter/gql/resolver_query.go b/server/internal/adapter/gql/resolver_query.go index 2ed6d91967..463d57e3ac 100644 --- a/server/internal/adapter/gql/resolver_query.go +++ b/server/internal/adapter/gql/resolver_query.go @@ -236,7 +236,7 @@ func (r *queryResolver) Scene(ctx context.Context, projectID gqlmodel.ID) (*gqlm return loaders(ctx).Scene.FindByProject(ctx, projectID) } -func (r *queryResolver) Projects(ctx context.Context, teamID gqlmodel.ID, includeArchived *bool, pagination *gqlmodel.Pagination, keyword *string, sortType *gqlmodel.ProjectSort) (*gqlmodel.ProjectConnection, error) { +func (r *queryResolver) Projects(ctx context.Context, teamID gqlmodel.ID, pagination *gqlmodel.Pagination, keyword *string, sortType *gqlmodel.ProjectSort) (*gqlmodel.ProjectConnection, error) { return loaders(ctx).Project.FindByWorkspace(ctx, teamID, keyword, gqlmodel.ProjectSortTypeFrom(sortType), pagination) } @@ -259,3 +259,7 @@ func (r *queryResolver) CheckProjectAlias(ctx context.Context, alias string) (*g func (r *queryResolver) StarredProjects(ctx context.Context, teamId gqlmodel.ID) (*gqlmodel.ProjectConnection, error) { return loaders(ctx).Project.FindStarredByWorkspace(ctx, teamId) } + +func (r *queryResolver) DeletedProjects(ctx context.Context, teamId gqlmodel.ID) (*gqlmodel.ProjectConnection, error) { + return loaders(ctx).Project.FindDeletedByWorkspace(ctx, teamId) +} diff --git a/server/internal/infrastructure/memory/project.go b/server/internal/infrastructure/memory/project.go index 13688864f3..bb073cc743 100644 --- a/server/internal/infrastructure/memory/project.go +++ b/server/internal/infrastructure/memory/project.go @@ -105,6 +105,28 @@ func (r *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.W return result, nil } +func (r *Project) FindDeletedByWorkspace(ctx context.Context, id accountdomain.WorkspaceID) ([]*project.Project, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.f.CanRead(id) { + return nil, nil + } + + var result []*project.Project + for _, p := range r.data { + if p.Workspace() == id && p.IsDeleted() { + result = append(result, p) + } + } + + sort.Slice(result, func(i, j int) bool { + return result[i].UpdatedAt().After(result[j].UpdatedAt()) + }) + + return result, nil +} + func (r *Project) FindIDsByWorkspace(ctx context.Context, id accountdomain.WorkspaceID) (res []project.ID, _ error) { r.lock.Lock() defer r.lock.Unlock() diff --git a/server/internal/infrastructure/mongo/mongodoc/project.go b/server/internal/infrastructure/mongo/mongodoc/project.go index 4d6a01cec8..37a2f7c7de 100644 --- a/server/internal/infrastructure/mongo/mongodoc/project.go +++ b/server/internal/infrastructure/mongo/mongodoc/project.go @@ -35,6 +35,7 @@ type ProjectDocument struct { TrackingID string // Scene string Starred bool + Deleted bool } type ProjectConsumer = Consumer[*ProjectDocument, *project.Project] @@ -77,6 +78,7 @@ func NewProject(project *project.Project) (*ProjectDocument, string) { TrackingID: project.TrackingID(), // Scene: project.Scene().String(), Starred: project.Starred(), + Deleted: project.IsDeleted(), }, pid } diff --git a/server/internal/infrastructure/mongo/project.go b/server/internal/infrastructure/mongo/project.go index cda562ed01..41b92b7aa4 100644 --- a/server/internal/infrastructure/mongo/project.go +++ b/server/internal/infrastructure/mongo/project.go @@ -109,6 +109,19 @@ func (r *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.W return r.find(ctx, filter) } +func (r *Project) FindDeletedByWorkspace(ctx context.Context, id accountdomain.WorkspaceID) ([]*project.Project, error) { + if !r.f.CanRead(id) { + return nil, repo.ErrOperationDenied + } + + filter := bson.M{ + "team": id.String(), + "deleted": true, + } + + return r.find(ctx, filter) +} + func (r *Project) FindByPublicName(ctx context.Context, name string) (*project.Project, error) { if name == "" { return nil, rerror.ErrNotFound diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go index 23743bcc0a..4363382a7c 100644 --- a/server/internal/usecase/interactor/project.go +++ b/server/internal/usecase/interactor/project.go @@ -89,6 +89,10 @@ func (i *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.W return i.projectRepo.FindStarredByWorkspace(ctx, id) } +func (i *Project) FindDeletedByWorkspace(ctx context.Context, id accountdomain.WorkspaceID, operator *usecase.Operator) ([]*project.Project, error) { + return i.projectRepo.FindDeletedByWorkspace(ctx, id) +} + func (i *Project) Create(ctx context.Context, p interfaces.CreateProjectParam, operator *usecase.Operator) (_ *project.Project, err error) { if err := i.CanWriteWorkspace(p.WorkspaceID, operator); err != nil { return nil, err @@ -228,6 +232,10 @@ func (i *Project) Update(ctx context.Context, p interfaces.UpdateProjectParam, o prj.SetStarred(*p.Starred) } + if p.Deleted != nil { + prj.SetDeleted(*p.Deleted) + } + if p.PublicTitle != nil { prj.UpdatePublicTitle(*p.PublicTitle) } diff --git a/server/internal/usecase/interactor/project_test.go b/server/internal/usecase/interactor/project_test.go index 29a5f3bf4b..b0ba3be499 100644 --- a/server/internal/usecase/interactor/project_test.go +++ b/server/internal/usecase/interactor/project_test.go @@ -218,6 +218,7 @@ func TestImportProject(t *testing.T) { "id": "01j7g9ddttkpnt3esk8h4w7xhv", "isArchived": false, "isBasicAuthActive": false, + "isDeleted": false, "basicAuthUsername": "", "basicAuthPassword": "", "name": "ProjectName1", diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go index 069e132846..6fb282c7e1 100644 --- a/server/internal/usecase/interfaces/project.go +++ b/server/internal/usecase/interfaces/project.go @@ -46,6 +46,7 @@ type UpdateProjectParam struct { TrackingID *string SceneID *id.SceneID Starred *bool + Deleted *bool } type PublishProjectParam struct { @@ -63,6 +64,7 @@ type Project interface { Fetch(context.Context, []id.ProjectID, *usecase.Operator) ([]*project.Project, error) FindByWorkspace(context.Context, accountdomain.WorkspaceID, *string, *project.SortType, *usecasex.Pagination, *usecase.Operator) ([]*project.Project, *usecasex.PageInfo, error) FindStarredByWorkspace(context.Context, accountdomain.WorkspaceID, *usecase.Operator) ([]*project.Project, error) + FindDeletedByWorkspace(context.Context, accountdomain.WorkspaceID, *usecase.Operator) ([]*project.Project, error) Create(context.Context, CreateProjectParam, *usecase.Operator) (*project.Project, error) Update(context.Context, UpdateProjectParam, *usecase.Operator) (*project.Project, error) Publish(context.Context, PublishProjectParam, *usecase.Operator) (*project.Project, error) diff --git a/server/internal/usecase/repo/project.go b/server/internal/usecase/repo/project.go index 7ba88d180d..3e3195cc4b 100644 --- a/server/internal/usecase/repo/project.go +++ b/server/internal/usecase/repo/project.go @@ -23,6 +23,7 @@ type Project interface { FindByScene(context.Context, id.SceneID) (*project.Project, error) FindByWorkspace(context.Context, accountdomain.WorkspaceID, ProjectFilter) ([]*project.Project, *usecasex.PageInfo, error) FindStarredByWorkspace(context.Context, accountdomain.WorkspaceID) ([]*project.Project, error) + FindDeletedByWorkspace(context.Context, accountdomain.WorkspaceID) ([]*project.Project, error) FindByPublicName(context.Context, string) (*project.Project, error) CountByWorkspace(context.Context, accountdomain.WorkspaceID) (int, error) CountPublicByWorkspace(context.Context, accountdomain.WorkspaceID) (int, error) diff --git a/server/pkg/project/project.go b/server/pkg/project/project.go index 1fa07a09c4..40ad047f47 100644 --- a/server/pkg/project/project.go +++ b/server/pkg/project/project.go @@ -38,6 +38,7 @@ type Project struct { trackingId string sceneId SceneID starred bool + isDeleted bool } func (p *Project) ID() ID { @@ -141,6 +142,10 @@ func (p *Project) Starred() bool { return p.starred } +func (p *Project) IsDeleted() bool { + return p.isDeleted +} + func (p *Project) SetArchived(isArchived bool) { p.isArchived = isArchived } @@ -179,6 +184,10 @@ func (p *Project) SetStarred(starred bool) { p.starred = starred } +func (p *Project) SetDeleted(isDeleted bool) { + p.isDeleted = isDeleted +} + func (p *Project) UpdateName(name string) { p.name = name } diff --git a/web/src/services/gql/__gen__/graphql.ts b/web/src/services/gql/__gen__/graphql.ts index ebff62f284..2ef96b7069 100644 --- a/web/src/services/gql/__gen__/graphql.ts +++ b/web/src/services/gql/__gen__/graphql.ts @@ -1809,6 +1809,7 @@ export type Project = Node & { imageUrl?: Maybe; isArchived: Scalars['Boolean']['output']; isBasicAuthActive: Scalars['Boolean']['output']; + isDeleted: Scalars['Boolean']['output']; name: Scalars['String']['output']; publicDescription: Scalars['String']['output']; publicImage: Scalars['String']['output']; @@ -2060,6 +2061,7 @@ export type Query = { checkProjectAlias: ProjectAliasAvailability; datasetSchemas: DatasetSchemaConnection; datasets: DatasetConnection; + deletedProjects: ProjectConnection; layer?: Maybe; me?: Maybe; node?: Maybe; @@ -2106,6 +2108,11 @@ export type QueryDatasetsArgs = { }; +export type QueryDeletedProjectsArgs = { + teamId: Scalars['ID']['input']; +}; + + export type QueryLayerArgs = { id: Scalars['ID']['input']; }; @@ -2134,7 +2141,6 @@ export type QueryPluginsArgs = { export type QueryProjectsArgs = { - includeArchived?: InputMaybe; keyword?: InputMaybe; pagination?: InputMaybe; sort?: InputMaybe;