Pothos内部実装調査

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

QueryFieldBuilder
は RootFieldBuilder
を継承したもの

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

prismaConnection
は内部で、 connectionを呼んでいる
fieldBuilderProto
は本来、connectionを持たないが this(fieldBuilderProto)
を
fieldBuilderProto & { connection: (...args: unknown[]) => FieldRef<unknown> }
にすることでアクセスできるようにしている
plugin-relay
で connection が実際に実装されている
relay-pluginを読み込んでないと、大元のprismaConnectionは呼べないような実装になっている
prismaConnection: 'relay' extends PluginName

prismaConnection
の中の connection
では、resolveで resolvePrismaCursorConnection
が呼ばれる
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はどこからくる?

これがoptions、queryにどのタイミングでincludeが付与されるか確認したい
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,
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
が実行されている

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
両方で走ることを確認済み

- addTypeSelectionsForField
- (0, _loaderMap.setLoaderMappings)(context, info, state.mappings);
- (0, _selections.selectionToQuery)(state);
これらは何をしているのか探る

(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 の書き換えが発生している可能性が高い

addTypeSelectionsForField
addNestedSelections
後に state.mapping に値が入ることを確認
addFieldSelection
で state 変更

state.mappings
がないと include は生成されない
内部でキャッシュに保存していることがわかる
key | mapping.mappings. |
---|---|
Article@articles.edges.node.creator | {} |

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

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

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

addTypeSelectionsForField v2
addFieldSelection
での state.relations の変更を調べる
(0, _selections.mergeSelection)(state, fieldSelectionMap);
ここで値が変化

mergeSelection
relationsを変更している箇所を発見
(pathがどのようにrelationsに影響を与えるのかを確認したい)
state.relations.set(key, relatedState);