Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions packages/api-gateway/src/graphql-common-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Connection, ConnectionCursor } from 'graphql-relay';
import type { QueryInput } from '@aloxide/demux';

export interface PageInfoInput {
entityName: string;
edges: any[];
limit: number;
offset?: string;
}

const NUMER_OF_ITEMS_PER_PAGE = 20;

export type Base64String = string;

export function base64(i: string): Base64String {
return Buffer.from(i, 'utf8').toString('base64');
}

export function unbase64(i: Base64String): string {
return Buffer.from(i, 'base64').toString('utf8');
}

/**
* Creates the cursor string from an offset.
*/
export function offsetToCursor(prefix: string, offset: number): ConnectionCursor {
return base64(prefix + offset);
}
/**
* Rederives the offset from the cursor string.
*/
export function cursorToOffset(prefix: string, cursor: ConnectionCursor): number {
if (cursor) return parseInt(unbase64(cursor).substring(prefix.length), 10);
return NaN;
}

export function convertCursorToOffet(entityName: string, args: any): QueryInput {
const updatedArgs: QueryInput = { ...args };

const beforOffset = cursorToOffset(entityName, updatedArgs.before);
const afterOffset = cursorToOffset(entityName, updatedArgs.after);

if (isNaN(beforOffset)) delete updatedArgs.before;
else updatedArgs.before = beforOffset.toString();

if (isNaN(afterOffset)) delete updatedArgs.after;
else updatedArgs.after = afterOffset.toString();

if (updatedArgs.first) updatedArgs.first += 1;
if (updatedArgs.last) updatedArgs.last += 1;

return updatedArgs;
}

export function paginationInfo<T>(
entityName: string,
items: any[],
args: QueryInput,
): Connection<T> {
if (args.first) {
return forwardPaginationInfo({
entityName,
edges: items,
limit: args.first,
offset: args.after,
});
}

if (args.last) {
return backwardPaginationInfo({
entityName,
edges: items,
limit: args.last,
offset: args.before,
});
}

return forwardPaginationInfo({ entityName, edges: items, limit: NUMER_OF_ITEMS_PER_PAGE });
}

export function forwardPaginationInfo<T>(pageInput: PageInfoInput): Connection<T> {
let hasNextPage = false;
if (pageInput.edges.length > pageInput.limit) {
pageInput.edges = pageInput.edges.slice(0, pageInput.limit);
hasNextPage = true;
}
const edges = pageInput.edges.map(value => ({
cursor: offsetToCursor(pageInput.entityName, value.id),
node: value,
}));
return {
edges,
pageInfo: {
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
hasNextPage,
hasPreviousPage: pageInput.offset ? true : false,
},
};
}

export function backwardPaginationInfo<T>(pageInput: PageInfoInput): Connection<T> {
let hasPreviousPage = false;
if (pageInput.edges.length > pageInput.limit) {
pageInput.edges = pageInput.edges.slice(0, pageInput.limit);
hasPreviousPage = true;
}
const edges = pageInput.edges.reverse().map(value => ({
cursor: offsetToCursor(pageInput.entityName, value.id),
node: value,
}));
return {
edges,
pageInfo: {
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
hasNextPage: pageInput.offset ? true : false,
hasPreviousPage,
},
};
}
13 changes: 4 additions & 9 deletions packages/api-gateway/src/graphql-relay.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { FieldTypeEnum, Interpreter } from '@aloxide/bridge';
import { AloxideDataManager } from '@aloxide/demux';
import { GraphQLObjectType } from 'graphql';
import {
connectionArgs,
ConnectionConfigNodeType,
connectionDefinitions,
connectionFromArray,
} from 'graphql-relay';
import { connectionArgs, ConnectionConfigNodeType, connectionDefinitions } from 'graphql-relay';

import { GraphqlTypeInterpreter } from './GraphqlTypeInterpreter';
import { convertCursorToOffet, paginationInfo } from './graphql-common-utils';

import type { GraphQLFieldConfig, GraphQLScalarType, GraphQLNamedType } from 'graphql';
import type { GraphQLConnectionDefinitions } from 'graphql-relay';
Expand Down Expand Up @@ -79,11 +75,10 @@ export function createGraphQl(config: CreateGraphQlConfig): CreateGraphQlOutput[
type: connectionType,
args: connectionArgs,
resolve: (_, args) => {
const queryInput: QueryInput = args;
// TODO fix find all
const queryInput: QueryInput = convertCursorToOffet(connectionType.name, args);
return dataAdapter
.findAll(name, queryInput, metaData)
.then(items => connectionFromArray(items, args));
.then(items => paginationInfo(connectionType.name, items, args));
},
};

Expand Down
167 changes: 167 additions & 0 deletions packages/api-gateway/test/graphql-common-ultils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { QueryInput } from '@aloxide/demux/src';
import {
base64,
unbase64,
offsetToCursor,
cursorToOffset,
convertCursorToOffet,
forwardPaginationInfo,
backwardPaginationInfo,
paginationInfo,
} from '../src/graphql-common-utils';

describe('Graphql Common Ultils', () => {
const entityName = 'test';

it('Should convert to base64', () => {
expect(base64('hello')).toBe('aGVsbG8=');
});

it('Should revert to string', () => {
expect(unbase64('aGVsbG8=')).toBe('hello');
});

it('Should parse offset to cursor', () => {
expect(offsetToCursor(entityName, 1)).toBe('dGVzdDE=');
});

it('Should revert cursor to offset', () => {
expect(cursorToOffset(entityName, 'dGVzdDE=')).toBe(1);
});

it('Revert offset Should return NaN', () => {
expect(cursorToOffset(entityName, 'hello')).toBeNaN();
});

it('Should convert cursor to offset', () => {
const input: QueryInput = {
first: 2,
after: 'test',
last: 2,
before: 'test',
};

const resp = convertCursorToOffet(entityName, input);
expect(resp.first).toBe(3);
expect(resp.last).toBe(3);
expect(resp.after).toBeUndefined();
expect(resp.before).toBeUndefined();
});

it('Should convert cursor to offset', () => {
const input: QueryInput = {
first: 2,
after: 'dGVzdDE=',
last: 2,
before: 'dGVzdDE=',
};

const resp = convertCursorToOffet(entityName, input);
expect(resp.first).toBe(3);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand why this is 3 but not 2.

expect(resp.last).toBe(3);
expect(resp.after).toBe('1');
expect(resp.before).toBe('1');
});

it('Should forward pagination', () => {
const items = [
{
id: 1,
},
{
id: 2,
},
{
id: 3,
},
];

const resp = paginationInfo(entityName, items, { first: 2 });

expect(resp.edges).toBeDefined();
expect(resp.edges[0].cursor).toBe('dGVzdDE=');
expect(resp.pageInfo.startCursor).toBe('dGVzdDE=');
expect(resp.pageInfo.endCursor).toBe('dGVzdDI=');
expect(resp.pageInfo.hasNextPage).toBe(true);
expect(resp.pageInfo.hasPreviousPage).toBe(false);
});

it('Should forward pagination with after', () => {
const items = [
{
id: 1,
},
{
id: 2,
},
{
id: 3,
},
];

const resp = paginationInfo(entityName, items, { first: 2, after: 'test' });

expect(resp.edges).toBeDefined();
expect(resp.edges[0].cursor).toBe('dGVzdDE=');
expect(resp.pageInfo.startCursor).toBe('dGVzdDE=');
expect(resp.pageInfo.endCursor).toBe('dGVzdDI=');
expect(resp.pageInfo.hasNextPage).toBe(true);
expect(resp.pageInfo.hasPreviousPage).toBe(true);
});

it('Should backward pagination', () => {
const items = [
{
id: 37,
},
{
id: 36,
},
{
id: 35,
},
];

const resp = paginationInfo(entityName, items, { last: 2 });

expect(resp.edges).toBeDefined();
expect(resp.edges[0].cursor).toBe('dGVzdDM2');
expect(resp.pageInfo.startCursor).toBe('dGVzdDM2');
expect(resp.pageInfo.endCursor).toBe('dGVzdDM3');
expect(resp.pageInfo.hasNextPage).toBe(false);
expect(resp.pageInfo.hasPreviousPage).toBe(true);
});

it('Should backward pagination with before', () => {
const items = [
{
id: 37,
},
{
id: 36,
},
{
id: 35,
},
];

const resp = paginationInfo(entityName, items, { last: 2, before: 'test' });

expect(resp.edges).toBeDefined();
expect(resp.edges[0].cursor).toBe('dGVzdDM2');
expect(resp.pageInfo.startCursor).toBe('dGVzdDM2');
expect(resp.pageInfo.endCursor).toBe('dGVzdDM3');
expect(resp.pageInfo.hasNextPage).toBe(true);
expect(resp.pageInfo.hasPreviousPage).toBe(true);
});

it('Should backward pagination by emtpy items', () => {
const resp = paginationInfo(entityName, [], { last: 2, before: 'test' });

expect(resp.edges).toBeDefined();
expect(resp.pageInfo.startCursor).toBeUndefined();
expect(resp.pageInfo.endCursor).toBeUndefined();
expect(resp.pageInfo.hasNextPage).toBe(true);
expect(resp.pageInfo.hasPreviousPage).toBe(false);
});
});
29 changes: 21 additions & 8 deletions packages/example-api-gateway/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,28 @@ export function createDataProvider(
return m.count();
},

findAll({ limit, after }, { entity: { key } }): Promise<any[]> {
return m.findAll({
limit,
where: after && {
[key]: {
[Op.gt]: after,
findAll({ first, after, last, before }, { entity: { key } }): Promise<any[]> {
if (first)
return m.findAll({
limit: first,
where: after && {
[key]: {
[Op.gt]: after,
},
},
},
});
order: [[key, 'asc']],
});

if (last)
return m.findAll({
limit: last,
where: before && {
[key]: {
[Op.lt]: before,
},
},
order: [[key, 'desc']],
});
},

find(id: any, meta?: any): Promise<any> {
Expand Down