From 1bbd3d70057c0df451ad9d01b14a173c2de50e31 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Fri, 1 Sep 2023 08:54:49 -0700 Subject: [PATCH] [prisma] update model loader to cache queries and batch compatible queries to reduce likelyhood pf prisma deoptimization --- .changeset/pink-hairs-cry.md | 6 + packages/plugin-prisma/prisma/schema.prisma | 2 +- packages/plugin-prisma/src/index.ts | 18 +-- packages/plugin-prisma/src/model-loader.ts | 134 +++++++++++++----- packages/plugin-prisma/src/util/loader-map.ts | 2 +- packages/test-utils/src/create-server.ts | 3 + 6 files changed, 117 insertions(+), 48 deletions(-) create mode 100644 .changeset/pink-hairs-cry.md diff --git a/.changeset/pink-hairs-cry.md b/.changeset/pink-hairs-cry.md new file mode 100644 index 000000000..b1f513d5a --- /dev/null +++ b/.changeset/pink-hairs-cry.md @@ -0,0 +1,6 @@ +--- +'@pothos/plugin-prisma': minor +--- + +update model loader to cache query mappings and batch compatible queries to reduce likelyhood of +prisma deoptimization diff --git a/packages/plugin-prisma/prisma/schema.prisma b/packages/plugin-prisma/prisma/schema.prisma index a8846bc15..e904c5b8b 100644 --- a/packages/plugin-prisma/prisma/schema.prisma +++ b/packages/plugin-prisma/prisma/schema.prisma @@ -8,7 +8,7 @@ datasource db { generator client { provider = "prisma-client-js" output = "../tests/client" - previewFeatures = ["fullTextSearch", "filteredRelationCount", "extendedWhereUnique"] + previewFeatures = ["fullTextSearch"] } generator pothos { diff --git a/packages/plugin-prisma/src/index.ts b/packages/plugin-prisma/src/index.ts index c14d90f37..68ca879a2 100644 --- a/packages/plugin-prisma/src/index.ts +++ b/packages/plugin-prisma/src/index.ts @@ -16,7 +16,7 @@ import { PrismaModelTypes } from './types'; import { formatPrismaCursor, parsePrismaCursor } from './util/cursors'; import { getModel, getRefFromModel } from './util/datamodel'; import { getLoaderMapping, setLoaderMappings } from './util/loader-map'; -import { queryFromInfo, selectionStateFromInfo } from './util/map-query'; +import { queryFromInfo } from './util/map-query'; export { prismaConnectionHelpers } from './connection-helpers'; export { PrismaInterfaceRef } from './interface-ref'; @@ -164,19 +164,9 @@ export class PrismaPlugin extends BasePlugin { return fallback(queryFromInfo({ context, info }), parent, args, context, info); } - const selectionState = selectionStateFromInfo(context, info); - - return loaderCache(parent) - .loadSelection(selectionState, context) - .then((result) => { - const mappings = selectionState.mappings[info.path.key]; - - if (mappings) { - setLoaderMappings(context, info, mappings.mappings); - } - - return resolver(result, args, context, info); - }); + return loaderCache(context) + .loadSelection(info, parent as object) + .then((result) => resolver(result, args, context, info)); }; } } diff --git a/packages/plugin-prisma/src/model-loader.ts b/packages/plugin-prisma/src/model-loader.ts index 0822061b1..445b49b45 100644 --- a/packages/plugin-prisma/src/model-loader.ts +++ b/packages/plugin-prisma/src/model-loader.ts @@ -1,3 +1,4 @@ +import { GraphQLResolveInfo } from 'graphql'; import { createContextCache, InterfaceRef, @@ -5,8 +6,11 @@ import { PothosSchemaError, SchemaTypes, } from '@pothos/core'; +import { PrismaDelegate, SelectionMap } from './types'; import { getDelegateFromModel, getModel } from './util/datamodel'; import { getClient } from './util/get-client'; +import { cacheKey, setLoaderMappings } from './util/loader-map'; +import { selectionStateFromInfo } from './util/map-query'; import { mergeSelection, selectionCompatible, @@ -14,27 +18,38 @@ import { selectionToQuery, } from './util/selections'; +interface ResolvablePromise { + promise: Promise; + resolve: (value: T) => void; + reject: (err: unknown) => void; +} export class ModelLoader { - model: object; + context: object; builder: PothosSchemaTypes.SchemaBuilder; findUnique: (model: Record, ctx: {}) => unknown; modelName: string; - + queryCache = new Map(); staged = new Set<{ - promise: Promise>; state: SelectionState; + models: Map | null>>; }>(); + delegate: PrismaDelegate; + tick = Promise.resolve(); constructor( - model: object, + context: object, builder: PothosSchemaTypes.SchemaBuilder, modelName: string, findUnique: (model: Record, ctx: {}) => unknown, ) { - this.model = model; + this.context = context; this.builder = builder; this.findUnique = findUnique; this.modelName = modelName; + this.delegate = getDelegateFromModel( + getClient(this.builder, this.context as never), + this.modelName, + ); } static forRef( @@ -217,48 +232,103 @@ export class ModelLoader { return this.getFindUnique(findBy); } - async loadSelection(selection: SelectionState, context: object) { - const query = selectionToQuery(selection); + getSelection(info: GraphQLResolveInfo) { + const key = cacheKey(info.parentType.name, info.path); + if (!this.queryCache.has(key)) { + const selection = selectionStateFromInfo(this.context, info); + this.queryCache.set(key, { + selection, + query: selectionToQuery(selection), + }); + } + + return this.queryCache.get(key)!; + } + + async loadSelection(info: GraphQLResolveInfo, model: object) { + const { selection, query } = this.getSelection(info); + const result = await this.stageQuery(selection, query, model); + + if (result) { + const mappings = selection.mappings[info.path.key]; + + if (mappings) { + setLoaderMappings(this.context, info, mappings.mappings); + } + } + + return result; + } + + async stageQuery(selection: SelectionState, query: SelectionMap, model: object) { for (const entry of this.staged) { if (selectionCompatible(entry.state, query)) { mergeSelection(entry.state, query); - return entry.promise; + if (!entry.models.has(model)) { + entry.models.set(model, createResolvablePromise | null>()); + } + + return entry.models.get(model)!.promise; } } - return this.initLoad(selection, context); + return this.initLoad(selection, model); } - async initLoad(state: SelectionState, context: {}) { - const entry = { - promise: Promise.resolve().then(() => { - this.staged.delete(entry); - - const delegate = getDelegateFromModel( - getClient(this.builder, context as never), - this.modelName, - ); - - if (delegate.findUniqueOrThrow) { - return delegate.findUniqueOrThrow({ - ...selectionToQuery(state), - where: { ...(this.findUnique(this.model as Record, context) as {}) }, - } as never) as Promise>; - } + async initLoad(state: SelectionState, initialModel: {}) { + const models = new Map | null>>(); + + const promise = createResolvablePromise | null>(); + models.set(initialModel, promise); - return delegate.findUnique({ - rejectOnNotFound: true, - ...selectionToQuery(state), - where: { ...(this.findUnique(this.model as Record, context) as {}) }, - } as never) as Promise>; - }), + const entry = { + models, state, }; this.staged.add(entry); - return entry.promise; + const nextTick = createResolvablePromise(); + void this.tick.then(() => { + this.staged.delete(entry); + + for (const [model, { resolve, reject }] of entry.models) { + if (this.delegate.findUniqueOrThrow) { + void this.delegate + .findUniqueOrThrow({ + ...selectionToQuery(state), + where: { ...(this.findUnique(model as Record, this.context) as {}) }, + } as never) + // eslint-disable-next-line promise/no-nesting + .then(resolve as () => {}, reject); + } else { + void this.delegate + .findUnique({ + rejectOnNotFound: true, + ...selectionToQuery(state), + where: { ...(this.findUnique(model as Record, this.context) as {}) }, + } as never) + // eslint-disable-next-line promise/no-nesting + .then(resolve as () => {}, reject); + } + } + }); + setTimeout(() => void nextTick.resolve(), 0); + this.tick = nextTick.promise; + + return promise.promise; } } + +function createResolvablePromise(): ResolvablePromise { + let resolveFn!: (value: T) => void; + let rejectFn!: (reason?: unknown) => void; + const promise = new Promise((resolve, reject) => { + resolveFn = resolve; + rejectFn = reject; + }); + + return { promise, resolve: resolveFn, reject: rejectFn }; +} diff --git a/packages/plugin-prisma/src/util/loader-map.ts b/packages/plugin-prisma/src/util/loader-map.ts index b2cd7e79e..ab66ea783 100644 --- a/packages/plugin-prisma/src/util/loader-map.ts +++ b/packages/plugin-prisma/src/util/loader-map.ts @@ -4,7 +4,7 @@ import { LoaderMappings } from '../types'; const cache = createContextCache((ctx) => new Map()); -export function cacheKey(type: string, path: GraphQLResolveInfo['path'], subPath: string[]) { +export function cacheKey(type: string, path: GraphQLResolveInfo['path'], subPath: string[] = []) { let key = ''; let current: GraphQLResolveInfo['path'] | undefined = path; diff --git a/packages/test-utils/src/create-server.ts b/packages/test-utils/src/create-server.ts index b8e10a69f..856ec65f3 100644 --- a/packages/test-utils/src/create-server.ts +++ b/packages/test-utils/src/create-server.ts @@ -23,6 +23,9 @@ export function createTestServer(options: TestServerOptions) { schema: options.schema, context: options.contextFactory ?? (() => ({})), plugins: executePlugin ? [executePlugin] : [], + maskedErrors: { + isDev: true, + }, }); // eslint-disable-next-line @typescript-eslint/no-misused-promises