Skip to content

Commit

Permalink
refactor(labels): pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
unicornware committed Nov 12, 2023
1 parent 0ce0b1c commit 52e66fd
Show file tree
Hide file tree
Showing 34 changed files with 678 additions and 337 deletions.
228 changes: 63 additions & 165 deletions __fixtures__/api.github.com/graphql.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions __fixtures__/octokit.provider.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* @module fixtures/OctokitProvider
*/

import * as github from '@actions/github'
import type { ValueProvider } from '@nestjs/common'
import { Octokit } from '@octokit/core'
import { paginateGraphql } from '@octokit/plugin-paginate-graphql'
import INPUT_API from './input-api.fixture'
import INPUT_TOKEN from './input-token.fixture'

Expand All @@ -15,8 +17,7 @@ import INPUT_TOKEN from './input-token.fixture'
*/
const OctokitProvider: ValueProvider<Octokit> = {
provide: Octokit,
useValue: new Octokit({
auth: INPUT_TOKEN,
useValue: github.getOctokit(INPUT_TOKEN, {
baseUrl: INPUT_API,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
Expand All @@ -26,7 +27,7 @@ const OctokitProvider: ValueProvider<Octokit> = {
request: {
fetch: async (info: RequestInfo, opts: RequestInit) => fetch(info, opts)
}
})
}, paginateGraphql)
}

export default OctokitProvider
73 changes: 20 additions & 53 deletions __tests__/setup/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ import type {
UpdateLabelCommand
} from '#src/labels/commands'
import type { Label } from '#src/labels/types'
import type { PayloadObject } from '#src/types'
import type { MutationVariables, QueryVariables } from '#tests/types'
import type {
GQLPayload,
MutationVariables,
QueryVariables
} from '#tests/types'
import gqh from '#tests/utils/gqh'
import {
at,
isNull,
merge,
pick,
select,
type Omit
} from '@flex-development/tutils'
import { HttpResponse } from 'msw'
import GQLResponse from '#tests/utils/gql-response'
import { merge, pick, type Omit } from '@flex-development/tutils'
import { setupServer, type SetupServer } from 'msw/node'

/**
Expand All @@ -35,10 +31,10 @@ import { setupServer, type SetupServer } from 'msw/node'
*/
const server: SetupServer = setupServer(
gqh.mutation<
PayloadObject<{ label: Label }>,
GQLPayload<'label', Label>,
MutationVariables<CreateLabelCommand>
>('CreateLabel', ({ variables: { input } }) => {
return HttpResponse.json({
return GQLResponse.json({
data: {
payload: {
label: <Label>{
Expand All @@ -50,20 +46,20 @@ const server: SetupServer = setupServer(
})
}),
gqh.mutation<
PayloadObject<{ clientMutationId: string }>,
GQLPayload<'clientMutationId', string>,
MutationVariables<DeleteLabelCommand>
>('DeleteLabel', () => {
return HttpResponse.json({
return GQLResponse.json({
data: { payload: { clientMutationId: CLIENT_MUTATION_ID } }
})
}),
gqh.mutation<
PayloadObject<{ label: Label }>,
GQLPayload<'label', Label>,
MutationVariables<UpdateLabelCommand>
>('UpdateLabel', ({ variables: { input } }) => {
const { nodes } = data.data.payload.labels
const { nodes } = data.data.repository.labels

return HttpResponse.json({
return GQLResponse.json({
data: {
payload: {
label: <Label>merge(nodes.find(({ id }) => id === input.id)!, input)
Expand All @@ -72,47 +68,18 @@ const server: SetupServer = setupServer(
})
}),
gqh.query<
PayloadObject<{ id: string }>,
GQLPayload<'id', string>,
Omit<QueryVariables, 'cursor'>
>('GetRepository', () => {
return HttpResponse.json({
data: { payload: pick(data.data.payload, ['id']) }
return GQLResponse.json({
data: { payload: pick(data.data.repository, ['id']) }
})
}),
gqh.query<
PayloadObject<{ labels: { nodes: Label[] } }>,
GQLPayload<'labels', Label[]>,
QueryVariables
>('Labels', ({ variables: { cursor } }) => {
const { edges, nodes } = data.data.payload.labels

/**
* Index of current edge.
*
* @var {number} i
*/
let i: number = select(edges, null, e => e.cursor).indexOf(cursor ?? '')

/**
* Index of next edge.
*
* @var {number} j
*/
const j: number = (i < 0 ? ++i : i) + 10

return HttpResponse.json({
data: {
payload: {
labels: {
nodes: isNull(cursor) ? [] : nodes.slice(i, j),
pageInfo: {
endCursor: isNull(cursor)
? cursor
: at(edges, j)?.cursor ?? null
}
}
}
}
})
>('Labels', ({ variables }) => {
return GQLResponse.paginate({ ...variables, key: 'labels' })
})
)

Expand Down
26 changes: 26 additions & 0 deletions __tests__/types/gql-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @file Test Type Definitions - GQLError
* @module tests/types/GQLError
*/

/**
* Mock GraphQL error object.
*/
type GQLError = {
/**
* Error description.
*/
message: string

/**
* Error path segments.
*/
path: string[]

/**
* Error type.
*/
type: string
}

export type { GQLError as default }
21 changes: 21 additions & 0 deletions __tests__/types/gql-payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @file Test Type Definitions - GQLPayload
* @module tests/types/GQLPayload
*/

import type { Nullable } from '@flex-development/tutils'

/**
* GraphQL payload object.
*
* @template K - Payload data name
* @template T - Payload data type
*/
type GQLPayload<K extends string, T = any> = {
/**
* Payload data.
*/
payload: Nullable<Record<K, T extends readonly any[] ? { nodes: T } : T>>
}

export type { GQLPayload as default }
2 changes: 2 additions & 0 deletions __tests__/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
* @module tests/types
*/

export type { default as GQLError } from './gql-error'
export type { default as GQLPayload } from './gql-payload'
export type { default as MutationVariables } from './variables-mutation'
export type { default as QueryVariables } from './variables-query'
2 changes: 1 addition & 1 deletion __tests__/types/variables-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type QueryVariables = {
/**
* Pagination cursor.
*/
cursor: Nullable<string>
cursor?: Nullable<string>

/**
* Repository owner.
Expand Down
132 changes: 132 additions & 0 deletions __tests__/utils/gql-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @file Test Utilities - GQLResponse
* @module tests/utils/GQLResponse
*/

import data from '#fixtures/api.github.com/graphql.json' assert { type: 'json' }
import type { GQLError } from '#tests/types'
import {
at,
isNull,
select,
type Nullable,
type ObjectCurly
} from '@flex-development/tutils'
import type { GQLPaginated } from '@octokit/plugin-paginate-graphql'
import { HttpResponse, type StrictResponse } from 'msw'

/**
* Paginated data names.
*/
type PaginateKey = Exclude<keyof typeof data.data.repository, 'id'>

/**
* Pagination options.
*/
type PaginateOptions = {
/**
* Cursor of last node.
*
* @default null
*/
cursor?: Nullable<string>

/**
* Paginated data key.
*/
key: PaginateKey
}

/**
* Paginated data object.
*
* @template K - Paginated data name
* @template T - Paginated data type
*/
type PaginatedDataObject<K extends string, T extends ObjectCurly> =
| { data: { payload: null }; errors: GQLError[] }
| { data: GQLPaginated<K, T> }

/**
* GraphQL-specific response object mock.
*
* @see {@linkcode HttpResponse}
*
* @class
* @extends {HttpResponse}
*/
class GQLResponse extends HttpResponse {
/**
* Create a paginated {@linkcode Response}.
*
* @public
* @static
*
* @template K - Paginated data name
* @template T - Paginated data type
*
* @param {PaginateOptions} options - Pagination options
* @return {StrictResponse<PaginatedDataObject<K, T>>} Paginated response
*/
public static paginate<
K extends PaginateKey,
T extends ObjectCurly
>(options: PaginateOptions): StrictResponse<PaginatedDataObject<K, T>> {
const { cursor = null, key } = options
const { edges, nodes } = data.data.repository[key]

/**
* Index of current edge.
*
* @const {number} i
*/
const i: number = isNull(cursor)
? 0
: select(edges, null, e => e.cursor).indexOf(cursor) + 1

// return error response if cursor is invalid
if (i === -1) {
return this.json({
data: { payload: null },
errors: [
{
locations: [{ column: -1, line: -1 }],
message: `\`${cursor}\` does not appear to be a valid cursor.`,
path: ['payload', key, 'edges'],
type: 'INVALID_CURSOR_ARGUMENTS'
}
]
})
}

/**
* Index of next edge.
*
* @var {number} j
*/
const j: number = i + 10

/**
* Cursors from current edge to next edge.
*
* @const {{ cursor: string }[]} cursors
*/
const cursors: { cursor: string }[] = edges.slice(i, j)

return this.json({
data: {
payload: <T>{
[key]: {
nodes: nodes.slice(i, j),
pageInfo: {
endCursor: at(cursors, -1)?.cursor ?? cursor,
hasNextPage: !!cursors.length
}
}
}
}
})
}
}

export default GQLResponse
Loading

0 comments on commit 52e66fd

Please sign in to comment.