-
-
Save mjurincic/04081345c7884f44329e57193a26370c to your computer and use it in GitHub Desktop.
Utility to provide Relay Cursor Connection Specification support to Prisma Framework
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Country, Photon } from '@prisma/photon'; | |
import { findManyCursor } from './findManyCursor'; | |
const photon = new Photon(); | |
let data: Country[]; | |
const createCountry = async (id: string) => photon.countries.create({ | |
data: { | |
id, | |
name: id, | |
} | |
}); | |
beforeAll(async () => { | |
// Insert a bunch of records we can test against | |
data = await Promise.all([ | |
createCountry('country_01'), | |
createCountry('country_02'), | |
createCountry('country_03'), | |
createCountry('country_04'), | |
createCountry('country_05'), | |
createCountry('country_06'), | |
createCountry('country_07'), | |
createCountry('country_08'), | |
createCountry('country_09'), | |
createCountry('country_10'), | |
createCountry('country_11'), | |
createCountry('country_12'), | |
createCountry('country_13'), | |
createCountry('country_14'), | |
createCountry('country_15'), | |
createCountry('country_16'), | |
createCountry('country_17'), | |
createCountry('country_18'), | |
createCountry('country_19'), | |
createCountry('country_20'), | |
]); | |
}); | |
afterAll(async () => { | |
await photon.disconnect(); | |
}); | |
test('it should return all the records when no cursor arguments provided', async () => { | |
// ACT | |
const actual = await findManyCursor(args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: 'country_20', | |
hasNextPage: false, | |
hasPreviousPage: false, | |
startCursor: 'country_01', | |
}); | |
expect(actual.edges.length).toBe(data.length); | |
}); | |
test('first 5 records', async () => { | |
// ACT | |
const actual = await findManyCursor( | |
args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
{ | |
first: 5, | |
}, | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: 'country_05', | |
hasNextPage: true, | |
hasPreviousPage: false, | |
startCursor: 'country_01', | |
}); | |
expect(actual.edges.length).toBe(5); | |
}); | |
test('last 5 records', async () => { | |
// ACT | |
const actual = await findManyCursor( | |
args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
{ | |
last: 5, | |
}, | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: 'country_20', | |
hasNextPage: false, | |
hasPreviousPage: true, | |
startCursor: 'country_16', | |
}); | |
expect(actual.edges.length).toBe(5); | |
}); | |
test('first 5 records after country_05', async () => { | |
// ACT | |
const actual = await findManyCursor( | |
args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
{ | |
first: 5, | |
after: 'country_05', | |
}, | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: 'country_10', | |
hasNextPage: true, | |
hasPreviousPage: true, | |
startCursor: 'country_06', | |
}); | |
expect(actual.edges.length).toBe(5); | |
}); | |
test('first 5 records after country_16', async () => { | |
// ACT | |
const actual = await findManyCursor( | |
args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
{ | |
first: 5, | |
after: 'country_16', | |
}, | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: 'country_20', | |
hasNextPage: false, | |
hasPreviousPage: true, | |
startCursor: 'country_17', | |
}); | |
expect(actual.edges.length).toBe(4); | |
}); | |
test('last 5 records before country_05', async () => { | |
// ACT | |
const actual = await findManyCursor( | |
args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
{ | |
last: 5, | |
before: 'country_05', | |
}, | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: 'country_04', | |
hasNextPage: true, | |
hasPreviousPage: false, | |
startCursor: 'country_01', | |
}); | |
expect(actual.edges.length).toBe(4); | |
}); | |
test('last 5 records before country_01', async () => { | |
// ACT | |
const actual = await findManyCursor( | |
args => | |
photon.countries.findMany({ | |
...args, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
{ | |
last: 5, | |
before: 'country_01', | |
}, | |
); | |
// ASSERT | |
expect(actual.pageInfo).toEqual({ | |
endCursor: undefined, | |
hasNextPage: true, | |
hasPreviousPage: false, | |
startCursor: undefined, | |
}); | |
expect(actual.edges.length).toBe(0); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Credits go to @queicherius who provided the original example: | |
* https://github.com/prisma/photonjs/issues/321#issuecomment-568290134 | |
* The code below is a direct copy/paste and then adapation of it. | |
*/ | |
export type ConnectionCursor = string; | |
export interface ConnectionArguments { | |
before?: ConnectionCursor | null; | |
after?: ConnectionCursor | null; | |
first?: number | null; | |
last?: number | null; | |
} | |
export interface PageInfo { | |
startCursor?: ConnectionCursor; | |
endCursor?: ConnectionCursor; | |
hasPreviousPage: boolean; | |
hasNextPage: boolean; | |
} | |
export interface Edge<T> { | |
node: T; | |
cursor: ConnectionCursor; | |
} | |
export interface Connection<T> { | |
edges: Array<Edge<T>>; | |
pageInfo: PageInfo; | |
} | |
/** | |
* Supports the Relay Cursor Connection Specification | |
* | |
* @see https://facebook.github.io/relay/graphql/connections.htm | |
*/ | |
export async function findManyCursor<Model extends { id: string }>( | |
findMany: (args: ConnectionArguments) => Promise<Model[]>, | |
args: ConnectionArguments = {} as ConnectionArguments, | |
): Promise<Connection<Model>> { | |
if (args.first != null && args.first < 0) { | |
throw new Error('first is less than 0'); | |
} | |
if (args.last != null && args.last < 0) { | |
throw new Error('last is less than 0'); | |
} | |
const originalLength = | |
args.first != null ? args.first : args.last != null ? args.last : undefined; | |
// We will fetch an additional node so that we can determine if there is a | |
// prev/next page | |
const first = args.first != null ? args.first + 1 : undefined; | |
const last = args.last != null ? args.last + 1 : undefined; | |
// Execute the underlying findMany operation | |
const nodes = await findMany({ ...args, first, last }); | |
// Check if we actually got an additional node. This would indicate we have | |
// a prev/next page | |
const hasExtraNode = originalLength != null && nodes.length > originalLength; | |
// Remove the extra node from the results | |
if (hasExtraNode) { | |
if (first != null) { | |
nodes.pop(); | |
} else if (last != null) { | |
nodes.shift(); | |
} | |
} | |
// Get the start and end cursors | |
const startCursor = nodes.length > 0 ? nodes[0].id : undefined; | |
const endCursor = nodes.length > 0 ? nodes[nodes.length - 1].id : undefined; | |
// If paginating forward: | |
// - For the next page, see if we had an extra node in the result set | |
// - For the previous page, see if we are "after" another node (so there has | |
// to be more before this) | |
// If paginating backwards: | |
// - For the next page, see if we are "before" another node (so there has to be | |
// more after this) | |
// - For the previous page, see if we had an extra node in the result set | |
const hasNextPage = first != null ? hasExtraNode : args.before != null; | |
const hasPreviousPage = first != null ? args.after != null : hasExtraNode; | |
return { | |
pageInfo: { | |
startCursor, | |
endCursor, | |
hasNextPage, | |
hasPreviousPage, | |
}, | |
edges: nodes.map(node => ({ cursor: node.id, node })), | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { objectType } from 'nexus'; | |
import { queryType } from 'nexus'; | |
export const Query = queryType({ | |
definition(t) { | |
t.field('countries', { | |
type: 'Countries', | |
args: { | |
first: intArg({ | |
required: false, | |
}), | |
last: intArg({ | |
required: false, | |
}), | |
after: stringArg({ | |
required: false, | |
}), | |
before: stringArg({ | |
required: false, | |
}), | |
}, | |
nullable: false, | |
resolve: async (_parent, args) => { | |
return findManyCursor( | |
_args => | |
photon().countries.findMany({ | |
..._args, | |
select: { | |
id, | |
name | |
}, | |
orderBy: { | |
name: 'asc', | |
}, | |
}), | |
args, | |
); | |
}, | |
}); | |
}, | |
}); | |
export const Countries = objectType({ | |
name: 'Countries', | |
definition(t) { | |
t.field('pageInfo', { | |
type: 'PageInfo', | |
}); | |
t.list.field('edges', { | |
type: 'CountryEdge', | |
}); | |
}, | |
}); | |
export const PageInfo = objectType({ | |
name: 'PageInfo', | |
definition(t) { | |
t.string('startCursor', { | |
nullable: true, | |
}); | |
t.string('endCursor', { | |
nullable: true, | |
}); | |
t.boolean('hasPreviousPage'); | |
t.boolean('hasNextPage'); | |
}, | |
}); | |
export const CountryEdge = objectType({ | |
name: 'CountryEdge', | |
definition(t) { | |
t.string('cursor'); | |
t.field('node', { type: 'Country' }); | |
}, | |
}); | |
export const Country = objectType({ | |
name: 'Country', | |
definition(t) { | |
t.model.id(); | |
t.model.name(); | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
datasource sqlite { | |
url = "file:data.db" | |
provider = "sqlite" | |
} | |
generator photonjs { | |
provider = "photonjs" | |
} | |
model Country { | |
id Int @id | |
name String @unique | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment