Closed16

Pothos内部実装調査

touyutouyu

builder.queryField を実行したタイミングで configStoreQueryFieldBuilder が登録される

touyutouyu
builder.queryField('articles', (t) =>
  t.prismaConnection({
    type: 'Article',
    cursor: 'databaseId',
    args: getArticlesArgs,
    resolve: (query, parent, args) => 
  })
)

t.prismaConnectionの実態は plugin-prisma/src/field-builder.ts にある fieldBuilderProto.prismaConnection

実際に動いているコードは lib 配下にあるトランスパイルされた .js

touyutouyu

prismaConnection は内部で、 connectionを呼んでいる
fieldBuilderProto は本来、connectionを持たないが this(fieldBuilderProto)

fieldBuilderProto & { connection: (...args: unknown[]) => FieldRef<unknown> }

にすることでアクセスできるようにしている

plugin-relay で connection が実際に実装されている

relay-pluginを読み込んでないと、大元のprismaConnectionは呼べないような実装になっている

plugin-prisma/src/global-types.ts
prismaConnection: 'relay' extends PluginName
touyutouyu

prismaConnection の中の connection では、resolveで resolvePrismaCursorConnection が呼ばれる

plugin-prisma/lib/util/cursors.js
async function resolvePrismaCursorConnection(options, cursor, resolve) {
    const query = prismaCursorConnectionQuery(options);
    console.log("resolvePrismaCursorConnection: ", options.query, query)
    const results = await resolve({
        ...options.query,
        ...query
    });
    if (!results) {
        return results;
    }
    return wrapConnectionResult(results, options.args, query.take, cursor, options.totalCount);
}

resolvePrismaCursorConnection

edges の時は引数のoptions.queryに include: { creator: true } が含まれるが
nodes の時には含まれない
→ これが原因で nodes では N+1 が発生している
optionsはどこからくる?

touyutouyu

これがoptions、queryにどのタイミングでincludeが付与されるか確認したい

plugin-prisma/src/field-builders.ts
resolvePrismaCursorConnection(
  {
    query: queryFromInfo({
      context,
      info,
      select: cursorSelection as {},
      path: ['edges', 'node'],
      typeName,
    }),
    ctx: context,
    parseCursor,
    maxSize,
    defaultSize,
    args,
    totalCount: totalCount && (() => totalCount(parent, args as never, context, info)),
  },
  formatCursor,
plugin-prisma/lib/util/map-query
function queryFromInfo({ context , info , typeName , select , path =[]  }) {
    const returnType = (0, _graphql.getNamedType)(info.returnType);
    const type = typeName ? info.schema.getTypeMap()[typeName] : returnType;
    const state = createStateForType(type, info);
    if (select) {
        (0, _selections.mergeSelection)(state, {
            select
        });
    }
    console.log('path', path);
    if (path.length > 0) {
        var _returnType_extensions;
        const { pothosPrismaIndirectInclude  } = (_returnType_extensions = returnType.extensions) !== null && _returnType_extensions !== void 0 ? _returnType_extensions : {};
        var _pothosPrismaIndirectInclude_path;
        resolveIndirectInclude(returnType, info, info.fieldNodes[0], (_pothosPrismaIndirectInclude_path = pothosPrismaIndirectInclude === null || pothosPrismaIndirectInclude === void 0 ? void 0 : pothosPrismaIndirectInclude.path) !== null && _pothosPrismaIndirectInclude_path !== void 0 ? _pothosPrismaIndirectInclude_path : [], [], (indirectType, indirectField, subPath)=>{
            resolveIndirectInclude(indirectType, info, indirectField, path.map((n)=>typeof n === 'string' ? {
                    name: n
                } : n), subPath, (resolvedType, resolvedField, nested)=>{
                addTypeSelectionsForField(typeName ? type : resolvedType, context, info, state, resolvedField, nested);
            });
        });
    } else {
        addTypeSelectionsForField(type, context, info, state, info.fieldNodes[0], []);
    }
    (0, _loaderMap.setLoaderMappings)(context, info, state.mappings);
    return (0, _selections.selectionToQuery)(state);
}

queryFromInfo(0, _selections.selectionToQuery)(state); の結果が戻り値のよう
{ include: { creator: true } }

edges でも nodes でも path には ['edges', 'node'] が含まれてる

edges では addTypeSelectionsForField が実行されている

touyutouyu
resolveIndirectInclude(indirectType, info, indirectField, path.map((n)=>typeof n === 'string' ? {
                    name: n
                } : n), subPath, (resolvedType, resolvedField, nested)=>{
                console.log("nested: ", nested)
                addTypeSelectionsForField(typeName ? type : resolvedType, context, info, state, resolvedField, nested);
            });

これは、 edges nodes 両方で走ることを確認済み

touyutouyu
  • addTypeSelectionsForField
  • (0, _loaderMap.setLoaderMappings)(context, info, state.mappings);
  • (0, _selections.selectionToQuery)(state);

これらは何をしているのか探る

touyutouyu

(0, _loaderMap.setLoaderMappings)(context, info, state.mappings);

state.mappings は edges の時のみ、下記情報をもつ

mappings {
  creator: {
    field: 'creator',
    type: 'Article',
    mappings: {},
    indirectPath: [ 'edges', 'node' ]
  }
}

const state = createStateForType(type, info); この時点ではまだ mappings は空

stateに触れているのは、 addTypeSelectionsForField だけ → addTypeSelectionsForField で state の書き換えが発生している可能性が高い

touyutouyu

addTypeSelectionsForField

addNestedSelections 後に state.mapping に値が入ることを確認

addFieldSelection で state 変更

touyutouyu

state.mappings がないと include は生成されない
内部でキャッシュに保存していることがわかる

key mapping.mappings.
Article@articles.edges.node.creator {}
touyutouyu

selectionToQuery

この関数で state から include を作成している
state.relations から再起的に { creator: true } が作成されている

touyutouyu

(0, _loaderMap.setLoaderMappings)(context, info, state.mappings); 呼ばない場合、{ include: { creator: true} は発行されるものの、N+1が発生する
state.relationsも存在する

touyutouyu

addTypeSelectionsForField ここでやはり state の加工が行われているようなので戻って深ぼる

touyutouyu

addTypeSelectionsForField v2

addFieldSelection での state.relations の変更を調べる

(0, _selections.mergeSelection)(state, fieldSelectionMap);

ここで値が変化

touyutouyu

mergeSelection

relationsを変更している箇所を発見

(pathがどのようにrelationsに影響を与えるのかを確認したい)

plugin-prisma/lib/util/selections.js
state.relations.set(key, relatedState);
このスクラップは2023/11/25にクローズされました