Skip to content

Commit

Permalink
Add a to the prisma plugin options
Browse files Browse the repository at this point in the history
  • Loading branch information
hayes committed Aug 3, 2023
1 parent 26b4dd8 commit 21e4014
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 20 deletions.
105 changes: 90 additions & 15 deletions packages/plugin-prisma/src/field-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
FieldKind,
FieldRef,
InputFieldMap,
isThenable,
MaybePromise,
ObjectRef,
PothosError,
RootFieldBuilder,
SchemaTypes,
} from '@pothos/core';
Expand All @@ -13,6 +15,7 @@ import { PrismaConnectionFieldOptions, PrismaModelTypes } from './types';
import { getCursorFormatter, getCursorParser, resolvePrismaCursorConnection } from './util/cursors';
import { getRefFromModel } from './util/datamodel';
import { queryFromInfo } from './util/map-query';
import { isUsed } from './util/usage';

const fieldBuilderProto = RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder<
SchemaTypes,
Expand All @@ -32,9 +35,18 @@ fieldBuilderProto.prismaField = function prismaField({ type, resolve, ...options
...(options as {}),
type: typeParam,
resolve: (parent: never, args: unknown, context: {}, info: GraphQLResolveInfo) => {
const query = queryFromInfo({ context, info });
const query = queryFromInfo({
context,
info,
withUsageCheck: !!this.builder.options.prisma?.onUnusedQuery,
});

return resolve(query, parent, args as never, context, info) as never;
return checkIfQueryIsUsed(
this.builder,
query,
info,
resolve(query, parent, args as never, context, info) as never,
);
},
}) as never;
};
Expand All @@ -57,9 +69,18 @@ fieldBuilderProto.prismaFieldWithInput = function prismaFieldWithInput({
...(options as {}),
type: typeParam,
resolve: (parent: never, args: unknown, context: {}, info: GraphQLResolveInfo) => {
const query = queryFromInfo({ context, info });
const query = queryFromInfo({
context,
info,
withUsageCheck: !!this.builder.options.prisma?.onUnusedQuery,
});

return resolve(query, parent, args as never, context, info) as never;
return checkIfQueryIsUsed(
this.builder,
query,
info,
resolve(query, parent, args as never, context, info) as never,
);
},
}) as never;
};
Expand Down Expand Up @@ -114,17 +135,20 @@ fieldBuilderProto.prismaConnection = function prismaConnection<
args: PothosSchemaTypes.DefaultConnectionArguments,
context: {},
info: GraphQLResolveInfo,
) =>
resolvePrismaCursorConnection(
) => {
const query = queryFromInfo({
context,
info,
select: cursorSelection as {},
paths: [['nodes'], ['edges', 'node']],
typeName,
withUsageCheck: !!this.builder.options.prisma?.onUnusedQuery,
});

return resolvePrismaCursorConnection(
{
parent,
query: queryFromInfo({
context,
info,
select: cursorSelection as {},
paths: [['nodes'], ['edges', 'node']],
typeName,
}),
query,
ctx: context,
parseCursor,
maxSize,
Expand All @@ -133,8 +157,15 @@ fieldBuilderProto.prismaConnection = function prismaConnection<
totalCount: totalCount && (() => totalCount(parent, args as never, context, info)),
},
formatCursor,
(query) => resolve(query as never, parent, args as never, context, info) as never,
),
(q) =>
checkIfQueryIsUsed(
this.builder,
query,
info,
resolve(q as never, parent, args as never, context, info) as never,
),
);
},
},
connectionOptions instanceof ObjectRef
? connectionOptions
Expand Down Expand Up @@ -163,3 +194,47 @@ fieldBuilderProto.prismaConnection = function prismaConnection<

return fieldRef;
} as never;

function checkIfQueryIsUsed<Types extends SchemaTypes, T>(
builder: PothosSchemaTypes.SchemaBuilder<Types>,
query: object,
info: GraphQLResolveInfo,
result: T,
): T {
const { onUnusedQuery } = builder.options.prisma || {};
if (!onUnusedQuery) {
return result;
}

if (isThenable(result)) {
return result.then((resolved) => {
if (!isUsed(query)) {
onUnused();
}

return resolved;
}) as T;
}

if (!isUsed(query)) {
onUnused();
}

return result;

function onUnused() {
if (typeof onUnusedQuery === 'function') {
onUnusedQuery(info);
return;
}

const message = `Prisma query was unused in resolver for ${info.parentType.name}.${info.fieldName}`;

if (onUnusedQuery === 'error') {
throw new PothosError(message);
} else if (onUnusedQuery === 'warn') {
// eslint-disable-next-line no-console
console.warn(message);
}
}
}
3 changes: 3 additions & 0 deletions packages/plugin-prisma/src/global-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-empty-interface */
import { GraphQLResolveInfo } from 'graphql';
import {
FieldKind,
FieldMap,
Expand Down Expand Up @@ -52,6 +53,7 @@ declare global {
models?: boolean;
fields?: boolean;
};
onUnusedQuery?: null | 'warn' | 'error' | ((info: GraphQLResolveInfo) => void);
}
| {
filterConnectionTotalCount?: boolean;
Expand All @@ -63,6 +65,7 @@ declare global {
models?: boolean;
fields?: boolean;
};
onUnusedQuery?: null | 'warn' | 'error' | ((info: GraphQLResolveInfo) => void);
};
}

Expand Down
7 changes: 6 additions & 1 deletion packages/plugin-prisma/src/util/map-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
SelectionState,
selectionToQuery,
} from './selections';
import { wrapWithUsageCheck } from './usage';

function addTypeSelectionsForField(
type: GraphQLNamedType,
Expand Down Expand Up @@ -370,13 +371,15 @@ export function queryFromInfo<T extends SelectionMap['select'] | undefined = und
select,
path = [],
paths = [],
withUsageCheck = false,
}: {
context: object;
info: GraphQLResolveInfo;
typeName?: string;
select?: T;
path?: string[];
paths?: string[][];
withUsageCheck?: boolean;
}): { select: T } | { include?: {} } {
const returnType = getNamedType(info.returnType);
const type = typeName ? info.schema.getTypeMap()[typeName] : returnType;
Expand Down Expand Up @@ -437,7 +440,9 @@ export function queryFromInfo<T extends SelectionMap['select'] | undefined = und

setLoaderMappings(context, info, state.mappings);

return selectionToQuery(state) as { select: T };
const query = selectionToQuery(state) as { select: T };

return withUsageCheck ? wrapWithUsageCheck(query) : query;
}

export function selectionStateFromInfo(
Expand Down
33 changes: 33 additions & 0 deletions packages/plugin-prisma/src/util/usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const usageSymbol = Symbol.for('Pothos.isUsed');

export function wrapWithUsageCheck<T extends Object>(obj: T): T {
const result = {};
let used = true;

Object.defineProperty(result, usageSymbol, {
get() {
return used;
},
enumerable: false,
});

for (const key of Object.keys(obj)) {
// only set to false if the object has keys
used = false;
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
// eslint-disable-next-line @typescript-eslint/no-loop-func
get() {
used = true;
return obj[key as keyof T];
},
});
}

return result as T;
}

export function isUsed(obj: object): boolean {
return !(usageSymbol in obj) || (obj as { [usageSymbol]: boolean })[usageSymbol];
}
1 change: 1 addition & 0 deletions packages/plugin-prisma/tests/example/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const builder = new SchemaBuilder<{
client: () => prisma,
dmmf: Prisma.dmmf,
exposeDescriptions: true,
onUnusedQuery: 'error',
},
errorOptions: {
defaultTypes: [Error],
Expand Down
15 changes: 11 additions & 4 deletions packages/plugin-prisma/tests/example/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,8 +984,10 @@ const WithCompositeUniqueNodeCustom = builder.prismaNode('WithCompositeUnique',
builder.queryFields((t) => ({
findUniqueRelations: t.prismaField({
type: 'FindUniqueRelations',
resolve: () =>
prisma.findUniqueRelations.findUniqueOrThrow({
resolve: (query) => {
void query.include;

return prisma.findUniqueRelations.findUniqueOrThrow({
where: {
id: '1',
},
Expand All @@ -995,7 +997,8 @@ builder.queryFields((t) => ({
withCompositeID: true,
withCompositeUnique: true,
},
}),
});
},
}),
findUniqueRelationsSelect: t.prismaField({
type: 'FindUniqueRelations',
Expand Down Expand Up @@ -1087,7 +1090,11 @@ const Blog = builder.objectRef<{ posts: Post[]; pages: number[] }>('Blog').imple
fields: (t) => ({
posts: t.prismaField({
type: ['Post'],
resolve: (_, blog) => blog.posts,
resolve: (query, blog) => {
void query.include;

return blog.posts;
},
}),
pages: t.exposeIntList('pages'),
}),
Expand Down
21 changes: 21 additions & 0 deletions website/pages/docs/plugins/prisma.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ const builder = new SchemaBuilder<{
exposeDescriptions: boolean | { models: boolean, fields: boolean },
// use where clause from prismaRelatedConnection for totalCount (will true by default in next major version)
filterConnectionTotalCount: true,
// warn when not using a query parameter correctly
onUnusedQuery: process.env.NODE_ENV === 'production' ? null : 'warn',
},
});
```
Expand Down Expand Up @@ -626,6 +628,7 @@ const Viewer = builder.prismaObject('User', {
variant: 'Viewer',
fields: (t) => ({
id: t.exposeID('id'),
});
});
```

Expand Down Expand Up @@ -833,6 +836,24 @@ const Media = builder.prismaObject('Media', {
});
```

## Detecting unused query arguments

Forgetting to spread the `query` argument from `t.prismaField` or `t.prismaConnection` into your
prisma query can result in inefficient queries, or even missing data. To help catch these issues,
the plugin can warn you when you are not using the query argument correctly.

the `onUnusedQuery` option can be set to `warn` or `error` to enable this feature. When set to
`warn` it will log a warning to the console if Pothos detects that you have not properly used the
query in your resolver. Similarly if you set the option to `error` it will throw an error instead.
You can also pass a function which will receive the `info` object which can be used to log or throw
your own error.

This check is fairly naive and works by wrapping the properties on the query with a getter that sets
a flag if the property is accessed. If no properties are accessed on the query object before the
resolver returns, it will trigger the `onUnusedQuery` condition.

It's recommended to enable this check in development to more quickly find potential issues.

## Optimized queries without `t.prismaField`

In some cases, it may be useful to get an optimized query for fields where you can't use
Expand Down

1 comment on commit 21e4014

@vercel
Copy link

@vercel vercel bot commented on 21e4014 Aug 3, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.