From 10416b9f18bd99f54dbb426a1295f238e8bb0508 Mon Sep 17 00:00:00 2001 From: Chris Penner Date: Thu, 25 Jul 2024 14:38:28 -0700 Subject: [PATCH 1/2] Fix project/user searches --- package.yaml | 2 +- share-api.cabal | 4 ++-- src/Share/App.hs | 4 ++-- src/Share/Postgres/Queries.hs | 9 ++++---- src/Share/Web/Share/Impl.hs | 22 ++++++++++++++----- .../project-search-with-user.json | 15 +++++++++++++ transcripts/share-apis/projects-flow/run.zsh | 3 +++ 7 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 transcripts/share-apis/projects-flow/project-search-with-user.json diff --git a/package.yaml b/package.yaml index 8bdb42e..158703f 100644 --- a/package.yaml +++ b/package.yaml @@ -20,7 +20,7 @@ description: Please see the README on GitHub at = minSeverity) $ do timestamp <- asks timeCache >>= liftIO - liftIO . log . Logging.logFmtFormatter timestamp $ msg + liftIO . log' . Logging.logFmtFormatter timestamp $ msg instance Cryptonite.MonadRandom (AppM reqCtx) where getRandomBytes = diff --git a/src/Share/Postgres/Queries.hs b/src/Share/Postgres/Queries.hs index 1f822c6..18c6b71 100644 --- a/src/Share/Postgres/Queries.hs +++ b/src/Share/Postgres/Queries.hs @@ -39,7 +39,7 @@ import Share.Web.Share.Releases.Types (ReleaseStatusFilter (..), StatusUpdate (. import Unison.Util.List qualified as Utils import Unison.Util.Monoid (intercalateMap) -userByUserId :: PG.QueryM m => UserId -> m (Maybe User) +userByUserId :: (PG.QueryM m) => UserId -> m (Maybe User) userByUserId uid = do PG.query1Row [PG.sql| @@ -260,8 +260,8 @@ searchUsersByNameOrHandlePrefix (Query prefix) (Limit limit) = do -- -- The PG.queryListRows accepts strings as web search queries, see -- https://www.postgresql.org/docs/current/textsearch-controls.html -searchProjectsByUserQuery :: Maybe UserId -> Query -> Limit -> PG.Transaction e [(Project, UserHandle)] -searchProjectsByUserQuery caller (Query query) limit = do +searchProjects :: Maybe UserId -> Maybe UserId -> Query -> Limit -> PG.Transaction e [(Project, UserHandle)] +searchProjects caller userIdFilter (Query query) limit = do let prefixQuery = query -- Remove any chars with special meaning for tsqueries. @@ -280,6 +280,7 @@ searchProjectsByUserQuery caller (Query query) limit = do JOIN users AS owner ON p.owner_user_id = owner.id WHERE (webquery @@ p.project_text_document OR prefixquery @@ p.project_text_document) AND (NOT p.private OR (#{caller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects WHERE user_id = #{caller} AND project_id = p.id))) + AND (#{userIdFilter} IS NULL OR p.owner_user_id = #{userIdFilter}) ORDER BY (ts_rank_cd(p.project_text_document, webquery), ts_rank_cd(p.project_text_document, prefixquery)) DESC LIMIT #{limit} |] @@ -698,7 +699,7 @@ createBranch !_nlReceipt projectId branchName contributorId causalId mergeTarget |] createRelease :: - PG.QueryM m => + (PG.QueryM m) => NameLookupReceipt -> ProjectId -> ReleaseVersion -> diff --git a/src/Share/Web/Share/Impl.hs b/src/Share/Web/Share/Impl.hs index 0648348..d929e94 100644 --- a/src/Share/Web/Share/Impl.hs +++ b/src/Share/Web/Share/Impl.hs @@ -6,9 +6,11 @@ module Share.Web.Share.Impl where +import Data.Text qualified as Text +import Servant import Share.Codebase qualified as Codebase import Share.Codebase.Types qualified as Codebase -import Share.IDs (TourId, UserHandle) +import Share.IDs (TourId, UserHandle (..)) import Share.IDs qualified as IDs import Share.JWT qualified as JWT import Share.OAuth.Session @@ -21,6 +23,7 @@ import Share.Postgres.Users.Queries qualified as UsersQ import Share.Prelude import Share.Project (Project (..)) import Share.User (User (..)) +import Share.User qualified as User import Share.UserProfile (UserProfile (..)) import Share.Utils.API import Share.Utils.Caching @@ -35,7 +38,6 @@ import Share.Web.Share.CodeBrowsing.API (CodeBrowseAPI) import Share.Web.Share.Contributions.Impl qualified as Contributions import Share.Web.Share.Projects.Impl qualified as Projects import Share.Web.Share.Types -import Servant import Unison.Codebase.Path qualified as Path import Unison.HashQualified qualified as HQ import Unison.Name (Name) @@ -332,13 +334,23 @@ getUserReadmeEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle = do -- all private users in the PG query itself. searchEndpoint :: Maybe Session -> Query -> Maybe Limit -> WebApp [SearchResult] searchEndpoint _caller (Query "") _limit = pure [] -searchEndpoint (MaybeAuthedUserID callerUserId) query (fromMaybe (Limit 20) -> limit) = do +searchEndpoint (MaybeAuthedUserID callerUserId) (Query query) (fromMaybe (Limit 20) -> limit) = do + (userQuery :: Query, (projectUserFilter :: Maybe UserId, projectQuery :: Query)) <- + fromMaybe query (Text.stripPrefix "@" query) + & Text.splitOn "/" + & \case + (userQuery : projectQueryText : _rest) -> do + mayUserId <- PG.runTransaction $ fmap User.user_id <$> Q.userByHandle (UserHandle userQuery) + pure (Query query, (mayUserId, Query projectQueryText)) + [projectOrUserQuery] -> pure (Query projectOrUserQuery, (Nothing, Query projectOrUserQuery)) + -- This is impossible + [] -> pure (Query query, (Nothing, Query query)) -- We don't have a great way to order users and projects together, so we just limit to a max -- of 5 users (who match the query as a prefix), then return the rest of the results from -- projects. (users, projects) <- PG.runTransaction $ do - users <- Q.searchUsersByNameOrHandlePrefix query (Limit 5) - projects <- Q.searchProjectsByUserQuery callerUserId query limit + users <- Q.searchUsersByNameOrHandlePrefix userQuery (Limit 5) + projects <- Q.searchProjects callerUserId projectUserFilter projectQuery limit pure (users, projects) let userResults = users diff --git a/transcripts/share-apis/projects-flow/project-search-with-user.json b/transcripts/share-apis/projects-flow/project-search-with-user.json new file mode 100644 index 0000000..d395ae3 --- /dev/null +++ b/transcripts/share-apis/projects-flow/project-search-with-user.json @@ -0,0 +1,15 @@ +{ + "body": [ + { + "projectRef": "@test/publictestproject", + "summary": "test project summary", + "tag": "Project", + "visibility": "public" + } + ], + "status": [ + { + "status_code": 200 + } + ] +} diff --git a/transcripts/share-apis/projects-flow/run.zsh b/transcripts/share-apis/projects-flow/run.zsh index 08e1ae0..902da5d 100755 --- a/transcripts/share-apis/projects-flow/run.zsh +++ b/transcripts/share-apis/projects-flow/run.zsh @@ -78,6 +78,9 @@ fetch "$transcript_user" GET project-catalog-get '/catalog' # Should find projects we have access to (e.g. Unison's private project), but none that we don't. fetch "$transcript_user" GET project-search '/search?query=test' +# Should filter project search by user if provided a full valid handle: +fetch "$transcript_user" GET project-search-with-user '/search?query=@test/public' + # Transcript user should not find 'test' user's private project fetch "$transcript_user" GET project-search-inaccessible '/search?query=privatetestproject' From 15161e1543bbc06f42db0d578b2f78f48f7f14b0 Mon Sep 17 00:00:00 2001 From: Chris Penner Date: Thu, 25 Jul 2024 15:30:17 -0700 Subject: [PATCH 2/2] If user is provided but project query is empty, just list projects by the user --- src/Share/Postgres/Queries.hs | 16 ++++++++++++++++ ...r.json => project-search-with-only-user.json} | 0 ...oject-search-with-user-and-project-query.json | 15 +++++++++++++++ transcripts/share-apis/projects-flow/run.zsh | 5 ++++- 4 files changed, 35 insertions(+), 1 deletion(-) rename transcripts/share-apis/projects-flow/{project-search-with-user.json => project-search-with-only-user.json} (100%) create mode 100644 transcripts/share-apis/projects-flow/project-search-with-user-and-project-query.json diff --git a/src/Share/Postgres/Queries.hs b/src/Share/Postgres/Queries.hs index 18c6b71..6d8c90b 100644 --- a/src/Share/Postgres/Queries.hs +++ b/src/Share/Postgres/Queries.hs @@ -261,6 +261,22 @@ searchUsersByNameOrHandlePrefix (Query prefix) (Limit limit) = do -- The PG.queryListRows accepts strings as web search queries, see -- https://www.postgresql.org/docs/current/textsearch-controls.html searchProjects :: Maybe UserId -> Maybe UserId -> Query -> Limit -> PG.Transaction e [(Project, UserHandle)] +-- Don't search with an empty query +searchProjects _caller Nothing (Query "") _limit = pure [] +searchProjects caller (Just userId) (Query "") limit = do + -- If we have a userId filter but no query, just return all the projects owned by that user + -- which the caller has access to. + PG.queryListRows @(Project PG.:. PG.Only UserHandle) + [PG.sql| + SELECT p.id, p.owner_user_id, p.slug, p.summary, p.tags, p.private, p.created_at, p.updated_at, owner.handle + FROM projects p + JOIN users owner ON p.owner_user_id = owner.id + WHERE p.owner_user_id = #{userId} + AND (NOT p.private OR (#{caller} IS NOT NULL AND EXISTS (SELECT FROM accessible_private_projects WHERE user_id = #{caller} AND project_id = p.id))) + ORDER BY p.created_at DESC + LIMIT #{limit} + |] + <&> fmap \(project PG.:. PG.Only handle) -> (project, handle) searchProjects caller userIdFilter (Query query) limit = do let prefixQuery = query diff --git a/transcripts/share-apis/projects-flow/project-search-with-user.json b/transcripts/share-apis/projects-flow/project-search-with-only-user.json similarity index 100% rename from transcripts/share-apis/projects-flow/project-search-with-user.json rename to transcripts/share-apis/projects-flow/project-search-with-only-user.json diff --git a/transcripts/share-apis/projects-flow/project-search-with-user-and-project-query.json b/transcripts/share-apis/projects-flow/project-search-with-user-and-project-query.json new file mode 100644 index 0000000..d395ae3 --- /dev/null +++ b/transcripts/share-apis/projects-flow/project-search-with-user-and-project-query.json @@ -0,0 +1,15 @@ +{ + "body": [ + { + "projectRef": "@test/publictestproject", + "summary": "test project summary", + "tag": "Project", + "visibility": "public" + } + ], + "status": [ + { + "status_code": 200 + } + ] +} diff --git a/transcripts/share-apis/projects-flow/run.zsh b/transcripts/share-apis/projects-flow/run.zsh index 902da5d..b58084d 100755 --- a/transcripts/share-apis/projects-flow/run.zsh +++ b/transcripts/share-apis/projects-flow/run.zsh @@ -79,7 +79,10 @@ fetch "$transcript_user" GET project-catalog-get '/catalog' fetch "$transcript_user" GET project-search '/search?query=test' # Should filter project search by user if provided a full valid handle: -fetch "$transcript_user" GET project-search-with-user '/search?query=@test/public' +fetch "$transcript_user" GET project-search-with-user-and-project-query '/search?query=@test/public' + +# Should return all projects in a user if provided a full valid handle, but no project query: +fetch "$transcript_user" GET project-search-with-only-user '/search?query=@test/' # Transcript user should not find 'test' user's private project fetch "$transcript_user" GET project-search-inaccessible '/search?query=privatetestproject'