Add GraphQL sort/filter/page crm organizations

What does this MR do and why?

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

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

We have had all approvals and MWPS set on the same change for CRM contacts: !94217 (merged)

SQL Queries

Organizations

SELECT "customer_relations_organizations".* FROM "customer_relations_organizations" WHERE "customer_relations_organizations"."group_id" = 31 AND "customer_relations_organizations"."state" = 0 ORDER BY "customer_relations_organizations"."name" DESC NULLS LAST, "customer_relations_organizations"."id" DESC LIMIT 100

 Limit  (cost=3.30..3.31 rows=1 width=59) (actual time=3.196..3.199 rows=0 loops=1)
   Buffers: shared hit=9 read=2
   I/O Timings: read=3.059 write=0.000
   ->  Sort  (cost=3.30..3.31 rows=1 width=59) (actual time=3.193..3.195 rows=0 loops=1)
         Sort Key: customer_relations_organizations.name DESC NULLS LAST, customer_relations_organizations.id DESC
         Sort Method: quicksort  Memory: 25kB
         Buffers: shared hit=9 read=2
         I/O Timings: read=3.059 write=0.000
         ->  Index Scan using index_organizations_on_unique_name_per_group on public.customer_relations_organizations  (cost=0.27..3.29 rows=1 width=59) (actual time=3.109..3.110 rows=0 loops=1)
               Index Cond: (customer_relations_organizations.group_id = 31)
               Filter: (customer_relations_organizations.state = 0)
               Rows Removed by Filter: 0
               Buffers: shared hit=3 read=2
               I/O Timings: read=3.059 write=0.000

Time: 5.081 ms
  - planning: 1.691 ms
  - execution: 3.390 ms
    - I/O read: 3.059 ms
    - I/O write: 0.000 ms

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

Counts:

SELECT COUNT(*) AS count_all, "customer_relations_organizations"."state" AS customer_relations_organizations_state FROM "customer_relations_organizations" WHERE "customer_relations_organizations"."group_id" = 31 GROUP BY "customer_relations_organizations"."state"

 Aggregate  (cost=3.30..3.32 rows=1 width=10) (actual time=0.073..0.075 rows=0 loops=1)
   Group Key: customer_relations_organizations.state
   Buffers: shared hit=8
   I/O Timings: read=0.000 write=0.000
   ->  Sort  (cost=3.30..3.30 rows=1 width=2) (actual time=0.071..0.072 rows=0 loops=1)
         Sort Key: customer_relations_organizations.state
         Sort Method: quicksort  Memory: 25kB
         Buffers: shared hit=8
         I/O Timings: read=0.000 write=0.000
         ->  Index Scan using index_organizations_on_unique_name_per_group on public.customer_relations_organizations  (cost=0.27..3.29 rows=1 width=2) (actual time=0.026..0.026 rows=0 loops=1)
               Index Cond: (customer_relations_organizations.group_id = 31)
               Buffers: shared hit=5
               I/O Timings: read=0.000 write=0.000

Time: 0.435 ms
  - planning: 0.264 ms
  - execution: 0.171 ms
    - I/O read: 0.000 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 8 (~64.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 CRM enabled and some organizations (ideally active and inactive)- see docs. 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(:organization, 10, group: Group.find(31), state: 'inactive')

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

query {
  group(fullPath: "flightjs") {
    __typename
    id
    organizations (
      state: inactive
      search: null
      sort: NAME_DESC
    ) {
      nodes {
        id
        name
        description
        defaultRate
        __typename
      }
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
        __typename
      }
      __typename
    }
  }
}

And:

query {
  group(fullPath: "flightjs") {
    __typename
    id
    organizationStateCounts {
      all
      inactive
      active
    }
  }
}

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 (Personal Account)

Merge request reports

Loading