Skip to content

Instantly share code, notes, and snippets.

@mjurincic
Forked from ctrlplusb/findManyCursor.test.ts
Last active April 21, 2020 12:34
Show Gist options
  • Save mjurincic/04081345c7884f44329e57193a26370c to your computer and use it in GitHub Desktop.
Save mjurincic/04081345c7884f44329e57193a26370c 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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment