Skip to content

Instantly share code, notes, and snippets.

@ctrlplusb
Last active February 18, 2022 17:22
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save ctrlplusb/17b5a1bd1736b5ba547bb15b3dd5be29 to your computer and use it in GitHub Desktop.
Save ctrlplusb/17b5a1bd1736b5ba547bb15b3dd5be29 to your computer and use it in GitHub Desktop.
Utility to provide Relay Cursor Connection Specification support to Prisma Framework
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);
});
/**
* 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 })),
};
}
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();
},
});
datasource sqlite {
url = "file:data.db"
provider = "sqlite"
}
generator photonjs {
provider = "photonjs"
}
model Country {
id Int @id
name String @unique
}
@Sytten
Copy link

Sytten commented Jun 4, 2020

This needs to be update with the latest changes to findMany

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment