Skip to content

Add GraphQL sort/filter/page crm contacts

Lee Tickett requested to merge 2256-improve-crm-contacts-list-backend into master

What does this MR do and why?

In preparation for upgrading the CRM contacts UI to support pagination, sorting and filtering, this MR adds those functionalities to the backend and exposes them via GraphQL.

The UI component also displays counts in tabs for active, inactive and all contacts (hence we had to add the ContactStateCounts too.

SQL Queries

Example main query:

SELECT "customer_relations_contacts".* FROM "customer_relations_contacts" WHERE "customer_relations_contacts"."group_id" = 8250287 AND "customer_relations_contacts"."state" = 1 ORDER BY "customer_relations_contacts"."first_name" DESC NULLS LAST, "customer_relations_contacts"."id" DESC LIMIT 100

Plan (take with a pinch of salt because the lack of test data (I ran for group )- I have asked if we can seed some data internal link

 Limit  (cost=4.63..4.64 rows=2 width=108) (actual time=3.588..3.591 rows=2 loops=1)
   Buffers: shared hit=9 read=3
   I/O Timings: read=3.418 write=0.000
   ->  Sort  (cost=4.63..4.64 rows=2 width=108) (actual time=3.586..3.587 rows=2 loops=1)
         Sort Key: customer_relations_contacts.first_name DESC NULLS LAST, customer_relations_contacts.id DESC
         Sort Method: quicksort  Memory: 25kB
         Buffers: shared hit=9 read=3
         I/O Timings: read=3.418 write=0.000
         ->  Index Scan using index_customer_relations_contacts_on_group_id on public.customer_relations_contacts  (cost=0.14..4.62 rows=2 width=108) (actual time=2.815..3.499 rows=2 loops=1)
               Index Cond: (customer_relations_contacts.group_id = 8250287)
               Filter: (customer_relations_contacts.state = 1)
               Rows Removed by Filter: 0
               Buffers: shared hit=3 read=3
               I/O Timings: read=3.418 write=0.000

Time: 4.747 ms
  - planning: 1.094 ms
  - execution: 3.653 ms
    - I/O read: 3.418 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 9 (~72.00 KiB) from the buffer pool
  - reads: 3 (~24.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

Details and visualization: https://postgres.ai/console/gitlab/gitlab-production-tunnel-pg12/sessions/11399/commands/40753

Example counts query:

SELECT COUNT(*) AS count_all, "customer_relations_contacts"."state" AS customer_relations_contacts_state FROM "customer_relations_contacts" WHERE "customer_relations_contacts"."group_id" = 8250287 AND ((("customer_relations_contacts"."first_name" ILIKE '%wa%' OR "customer_relations_contacts"."last_name" ILIKE '%wa%') OR "customer_relations_contacts"."email" ILIKE '%wa%') OR "customer_relations_contacts"."description" ILIKE '%wa%') GROUP BY "customer_relations_contacts"."state"

Plan

 Aggregate  (cost=4.65..4.67 rows=1 width=10) (actual time=0.055..0.056 rows=0 loops=1)
   Group Key: customer_relations_contacts.state
   Buffers: shared hit=9
   I/O Timings: read=0.000 write=0.000
   ->  Sort  (cost=4.65..4.65 rows=1 width=2) (actual time=0.054..0.054 rows=0 loops=1)
         Sort Key: customer_relations_contacts.state
         Sort Method: quicksort  Memory: 25kB
         Buffers: shared hit=9
         I/O Timings: read=0.000 write=0.000
         ->  Index Scan using index_customer_relations_contacts_on_group_id on public.customer_relations_contacts  (cost=0.14..4.64 rows=1 width=2) (actual time=0.034..0.034 rows=0 loops=1)
               Index Cond: (customer_relations_contacts.group_id = 8250287)
               Filter: ((customer_relations_contacts.first_name ~~* '%wa%'::text) OR (customer_relations_contacts.last_name ~~* '%wa%'::text) OR (customer_relations_contacts.email ~~* '%wa%'::text) OR (customer_relations_contacts.description ~~* '%wa%'::text))
               Rows Removed by Filter: 2
               Buffers: shared hit=6
               I/O Timings: read=0.000 write=0.000

Time: 1.192 ms
  - planning: 1.043 ms
  - execution: 0.149 ms
    - I/O read: 0.000 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 9 (~72.00 KiB) from the buffer pool
  - reads: 0 from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

How to set up and validate locally

You'll need a group with some contacts (ideally active and inactive). If you've recently reseeded your GDK you should already have them.

Otherwise, you should be able to do something like this from rails:

FactoryBot.create_list(:contact, 10, group: Group.find(31), state: 'inactive')

Then use graphql-explorer and play with a query something like:

query {
  group(fullPath: "flightjs") {
    __typename
    id
    contacts(
      state: inactive
      search: null
      sort: LAST_NAME_ASC
    ) {
      nodes {
        id
        firstName
        lastName
        email
        phone
        description
        organization {
          __typename
          id
          name
        }
        __typename
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
        __typename
      }
      __typename
    }
  }
}

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #2256

Edited by Lee Tickett

Merge request reports

Loading