Skip to content

Commit

Permalink
feature: add relay integration for prisma plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
hayes committed Aug 3, 2021
1 parent 7329670 commit e714e54
Show file tree
Hide file tree
Showing 23 changed files with 1,139 additions and 277 deletions.
11 changes: 11 additions & 0 deletions packages/core/src/build-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
InputType,
OutputType,
SchemaTypes,
typeBrandKey,
} from '.';

export default class BuildCache<Types extends SchemaTypes> {
Expand Down Expand Up @@ -563,6 +564,16 @@ export default class BuildCache<Types extends SchemaTypes> {

private buildInterface(config: GiraphQLInterfaceTypeConfig) {
const resolveType: GraphQLTypeResolver<unknown, Types['Context']> = (parent, context, info) => {
if (typeof parent === 'object' && parent !== null && typeBrandKey in parent) {
const typeBrand = (parent as { [typeBrandKey]: OutputType<SchemaTypes> })[typeBrandKey];

if (typeof typeBrand === 'string') {
return typeBrand;
}

return this.getTypeConfig(typeBrand).name;
}

// eslint-disable-next-line @typescript-eslint/no-use-before-define
const implementers = this.getImplementers(type);

Expand Down
16 changes: 13 additions & 3 deletions packages/core/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from './types';
import { normalizeEnumValues, valuesFromEnum, verifyRef } from './utils';
import {
AbstractReturnShape,
BaseEnum,
EnumParam,
EnumTypeOptions,
Expand Down Expand Up @@ -143,6 +144,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
name,
interfaces: (options.interfaces ?? []) as ObjectParam<SchemaTypes>[],
description: options.description,
extensions: options.extensions,
isTypeOf: options.isTypeOf as GraphQLIsTypeOfFn<unknown, Types['Context']>,
giraphqlOptions: options as GiraphQLSchemaTypes.ObjectTypeOptions,
};
Expand Down Expand Up @@ -203,6 +205,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
name: 'Query',
description: options.description,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.QueryTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config);
Expand Down Expand Up @@ -236,6 +239,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
name: 'Mutation',
description: options.description,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.MutationTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config);
Expand Down Expand Up @@ -269,6 +273,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
name: 'Subscription',
description: options.description,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.SubscriptionTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config);
Expand Down Expand Up @@ -313,8 +318,8 @@ export default class SchemaBuilder<Types extends SchemaTypes> {

const ref =
param instanceof InterfaceRef
? (param as InterfaceRef<OutputShape<Types, Param>, ParentShape<Types, Param>>)
: new InterfaceRef<OutputShape<Types, Param>, ParentShape<Types, Param>>(name);
? (param as InterfaceRef<AbstractReturnShape<Types, Param>, ParentShape<Types, Param>>)
: new InterfaceRef<AbstractReturnShape<Types, Param>, ParentShape<Types, Param>>(name);

const typename = ref.name;

Expand All @@ -325,6 +330,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
interfaces: (options.interfaces ?? []) as ObjectParam<SchemaTypes>[],
description: options.description,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.InterfaceTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config, ref);
Expand Down Expand Up @@ -373,7 +379,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
name: string,
options: GiraphQLSchemaTypes.UnionTypeOptions<Types, Member>,
) {
const ref = new UnionRef<OutputShape<Types, Member>, ParentShape<Types, Member>>(name);
const ref = new UnionRef<AbstractReturnShape<Types, Member>, ParentShape<Types, Member>>(name);

options.types.forEach((type) => {
verifyRef(type);
Expand All @@ -387,6 +393,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
description: options.description,
resolveType: options.resolveType as GraphQLTypeResolver<unknown, object>,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.UnionTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config, ref);
Expand Down Expand Up @@ -417,6 +424,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
values,
description: options.description,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.EnumTypeOptions<Types>,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config, ref);
Expand Down Expand Up @@ -448,6 +456,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
parseValue: options.parseValue,
serialize: options.serialize,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.ScalarTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config, ref);
Expand Down Expand Up @@ -499,6 +508,7 @@ export default class SchemaBuilder<Types extends SchemaTypes> {
name,
description: options.description,
giraphqlOptions: options as unknown as GiraphQLSchemaTypes.InputObjectTypeOptions,
extensions: options.extensions,
};

this.configStore.addTypeConfig(config, ref);
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/types/type-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { InterfaceRef, ObjectRef, RootName, SchemaTypes } from '..';

export const outputShapeKey = Symbol.for('GiraphQL.outputShapeKey');
export const parentShapeKey = Symbol.for('GiraphQL.parentShapeKey');
export const abstractReturnShapeKey = Symbol.for('GiraphQL.abstractReturnShapeKey');
export const inputShapeKey = Symbol.for('GiraphQL.inputShapeKey');
export const inputFieldShapeKey = Symbol.for('GiraphQL.inputFieldShapeKey');
export const outputFieldShapeKey = Symbol.for('GiraphQL.outputFieldShapeKey');
export const typeBrandKey = Symbol.for('GiraphQL.typeBrandKey');

export type OutputShape<Types extends SchemaTypes, T> = T extends {
[outputShapeKey]: infer U;
Expand All @@ -28,6 +30,12 @@ export type ParentShape<Types extends SchemaTypes, T> = T extends {
? U
: OutputShape<Types, T>;

export type AbstractReturnShape<Types extends SchemaTypes, T> = T extends {
[abstractReturnShapeKey]: infer U;
}
? U
: OutputShape<Types, T>;

export type InputShape<Types extends SchemaTypes, T> = T extends {
[inputShapeKey]: infer U;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { OutputType, SchemaTypes, typeBrandKey } from '..';

export * from './context-cache';
export * from './enums';
export * from './input';
Expand Down Expand Up @@ -34,3 +36,14 @@ you may be able to resolve this by importing it directly fron the file that defi
`);
}
}

export function brandWithType<Types extends SchemaTypes>(val: unknown, type: OutputType<Types>) {
if (typeof val !== 'object' || val === null) {
return;
}

Object.defineProperty(val, typeBrandKey, {
enumerable: false,
value: type,
});
}
1 change: 1 addition & 0 deletions packages/plugin-prisma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
},
"devDependencies": {
"@giraphql/core": "^2.11.0",
"@giraphql/plugin-relay": "^2.11.0",
"@prisma/client": "^2.27.0",
"apollo-server": "^2.25.2",
"graphql": ">=15.5.1",
Expand Down
134 changes: 134 additions & 0 deletions packages/plugin-prisma/src/cursors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { MaybePromise } from '@giraphql/core';

const DEFAULT_MAX_SIZE = 100;
const DEFAULT_SIZE = 20;

function formatCursor(value: unknown): string {
switch (typeof value) {
case 'number':
return Buffer.from(`GPC:N:${value}`).toString('base64');
case 'string':
return Buffer.from(`GPC:S:${value}`).toString('base64');
default:
throw new TypeError(`Unsupported cursor type ${typeof value}`);
}
}

function parseCursor(cursor: unknown) {
if (typeof cursor !== 'string') {
throw new TypeError('Cursor must be a string');
}

try {
const decoded = Buffer.from(cursor, 'base64').toString();
const [, type, value] = decoded.match(/^GPC:(\w):(.*)/) as [string, string, string];

switch (type) {
case 'S':
return value;
case 'N':
return Number.parseInt(value, 10);
default:
throw new TypeError(`Invalid cursor type ${type}`);
}
} catch {
throw new Error(`Invalid cursor: ${cursor}`);
}
}

interface PrismaCursorConnectionQueryOptions {
args: GiraphQLSchemaTypes.DefaultConnectionArguments;
defaultSize?: number;
maxSize?: number;
column: string;
}

interface ResolvePrismaCursorConnectionOptions extends PrismaCursorConnectionQueryOptions {
query: {};
}

export function prismaCursorConnectionQuery({
args: { before, after, first, last },
maxSize = DEFAULT_MAX_SIZE,
defaultSize = DEFAULT_SIZE,
column,
}: PrismaCursorConnectionQueryOptions) {
if (first != null && first < 0) {
throw new TypeError('Argument "first" must be a non-negative integer');
}

if (last != null && last < 0) {
throw new Error('Argument "last" must be a non-negative integer');
}

if (before && after) {
throw new Error('Arguments "before" and "after" are not supported at the same time');
}

if (before != null && last == null) {
throw new Error('Argument "last" must be provided when using "before"');
}

if (before != null && first != null) {
throw new Error('Arguments "before" and "first" are not supported at the same time');
}

if (after != null && last != null) {
throw new Error('Arguments "after" and "last" are not supported at the same time');
}

const cursor = before ?? after;

let take = Math.min(first ?? last ?? defaultSize, maxSize) + 1;

if (before) {
take = -take;
}

return cursor == null
? { take, skip: 0 }
: {
cursor: {
[column]: parseCursor(cursor),
},
take,
skip: 1,
};
}

export async function resolvePrismaCursorConnection<T extends {}>(
options: ResolvePrismaCursorConnectionOptions,
resolve: (query: { include?: {}; cursor?: {}; take: number; skip: number }) => MaybePromise<T[]>,
) {
const query = prismaCursorConnectionQuery(options);
const results = await resolve({
...options.query,
...query,
});

const gotFullResults = results.length === Math.abs(query.take);
const hasNextPage = options.args.before ? true : gotFullResults;
const hasPreviousPage = options.args.after ? true : gotFullResults;
const nodes = gotFullResults
? results.slice(query.take < 0 ? 1 : 0, query.take < 0 ? results.length : -1)
: results;

const edges = nodes.map((value, index) =>
value == null
? null
: {
cursor: formatCursor((value as Record<string, string>)[options.column]),
node: value,
},
);

return {
edges,
pageInfo: {
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
hasPreviousPage,
hasNextPage,
},
};
}

0 comments on commit e714e54

Please sign in to comment.