-
Notifications
You must be signed in to change notification settings - Fork 122
Console 1382 member pagination and search #7299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5825c22
475e7fe
68af504
546b550
abbd0e4
e1a5546
64578a0
8e820f2
5a12547
a2e9037
dea8fc1
a2d73a4
69f6939
c28e2a5
cf07f1a
c7a399f
2628b1b
555e5f1
91975f6
7d69bc4
041045e
650eeef
f9c17da
7df7e4b
b6b8419
c6c914e
5e91797
e45c665
5efef97
e1f56a6
34a0dc9
3f201d6
e1d5da1
a2c0ae1
1e83941
7a6af7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { type MigrationExecutor } from '../pg-migrator'; | ||
|
|
||
| export default { | ||
| name: '2025.11.25T00-00-00.members-search.ts', | ||
| run: ({ sql }) => sql` | ||
| CREATE EXTENSION IF NOT EXISTS pg_trgm; | ||
| CREATE INDEX CONCURRENTLY IF NOT EXISTS "users_search_by_email_and_display_name" on users using gin(LOWER(email|| ' ' || display_name) gin_trgm_ops); | ||
| `, | ||
| } satisfies MigrationExecutor; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -868,6 +868,14 @@ export default gql` | |
| appDeployments: [String!] | ||
| } | ||
| input MembersFilter { | ||
| """ | ||
| Part of a user's email or username that is used to filter the list of | ||
| members. | ||
| """ | ||
| searchTerm: String | ||
| } | ||
| type Organization { | ||
| """ | ||
| Unique UUID of the organization | ||
|
|
@@ -881,8 +889,11 @@ export default gql` | |
| name: String! @deprecated(reason: "Use the 'slug' field instead.") | ||
| owner: Member! @tag(name: "public") | ||
| me: Member! | ||
| members(first: Int @tag(name: "public"), after: String @tag(name: "public")): MemberConnection! | ||
| @tag(name: "public") | ||
| members( | ||
| first: Int @tag(name: "public") | ||
| after: String @tag(name: "public") | ||
| filters: MembersFilter | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. filters Does singular make more sense here?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's named after
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For inputs we also kind of have the convention to append If in doubt on how to name things, I would always recommend to look for similar fields that are tagged with Having that said I am also guilty of my own feedback 😆
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is part of the private GraphQL API for now, I am okay with handling it later though. Just make sure to create a follow up task |
||
| ): MemberConnection! @tag(name: "public") | ||
| invitations( | ||
| first: Int @tag(name: "public") | ||
| after: String @tag(name: "public") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -148,21 +148,31 @@ export class OrganizationMembers { | |
|
|
||
| async getPaginatedOrganizationMembersForOrganization( | ||
| organization: Organization, | ||
| args: { first: number | null; after: string | null }, | ||
| args: { first: number | null; after: string | null; searchTerm: string | null }, | ||
| ) { | ||
| this.logger.debug( | ||
| 'Find paginated organization members for organization. (organizationId=%s)', | ||
| organization.id, | ||
| ); | ||
|
|
||
| const first = args.first; | ||
| const first = args.first ? Math.min(args.first, 50) : 50; | ||
| const cursor = args.after ? decodeCreatedAtAndUUIDIdBasedCursor(args.after) : null; | ||
| const searchTerm = args.searchTerm?.trim().toLowerCase() ?? ''; | ||
| const searching = searchTerm.length > 0; | ||
|
|
||
| const query = sql` | ||
| SELECT | ||
| ${organizationMemberFields(sql`"om"`)} | ||
| FROM | ||
| "organization_member" AS "om" | ||
| ${ | ||
| searching | ||
| ? sql` | ||
| JOIN "users" as "u" | ||
| ON "om"."user_id" = "u"."id" | ||
| ` | ||
| : sql`` | ||
| } | ||
| WHERE | ||
| "om"."organization_id" = ${organization.id} | ||
| ${ | ||
|
|
@@ -178,11 +188,12 @@ export class OrganizationMembers { | |
| ` | ||
| : sql`` | ||
| } | ||
| ${searching ? sql`AND (LOWER("u"."display_name" || ' ' || "u"."email") LIKE ${'%' + searchTerm + '%'})` : sql``} | ||
| ORDER BY | ||
| "om"."organization_id" DESC | ||
| , "om"."created_at" DESC | ||
| , "om"."user_id" DESC | ||
|
Comment on lines
193
to
195
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this affect usage of existing indices?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have an index already that handles this: But this is important for the pagination -- so that the last element in the list has a cursor with the oldest timestamp
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now I get it, so in case of, we would need to update the index as well. 😢 Can be a follow-up PR, though! |
||
| , "om"."user_id" DESC | ||
| ${first ? sql`LIMIT ${first + 1}` : sql``} | ||
| LIMIT ${first + 1} | ||
| `; | ||
|
|
||
| const result = await this.pool.any<unknown>(query); | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this require additional configuration to our azure database? Please make sure to test this migration on the
devenvironment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😢
@jdolle we need to make sure we can introduce this extension without downtime
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to allow list per https://learn.microsoft.com/en-us/azure/postgresql/extensions/how-to-allow-extensions?tabs=allow-extensions-portal
There was no downtime when enabling this and there are no special considerations for the
pg_trgmextension that would cause downtime, since it's not instantly changing behavior at install -- it's enabling more ways to index for searching.For adding the index -- I'm not concerned due to the size of the table. The migration may take a few seconds in the background. And this will slightly increase create/update times for these records but again due to the size of the table, and also because of how infrequently these records are created/updated, this should not be an issue.