Skip to content

Commit

Permalink
Add builder.nodeRef, and improve loadableNodeRef implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hayes committed Oct 15, 2024
1 parent c1eda85 commit 6a80a7c
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 134 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-ears-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pothos/plugin-dataloader": minor
---

Use builder.nodeRef in ImplementableLoadableNodeRef to avoid re-implementing node logic
5 changes: 5 additions & 0 deletions .changeset/nice-turtles-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pothos/plugin-relay": minor
---

Add builder.nodeRef method
56 changes: 15 additions & 41 deletions packages/plugin-dataloader/src/refs/node.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
type FieldRef,
ImplementableObjectRef,
type InterfaceRef,
ObjectFieldBuilder,
type SchemaTypes,
completeValue,
} from '@pothos/core';
import type { GraphQLResolveInfo } from 'graphql';
import { ImplementableObjectRef, type ObjectRef, type SchemaTypes } from '@pothos/core';
import type { DataLoaderOptions, LoadableNodeId } from '../types';
import { dataloaderGetter } from '../util';
import { LoadableObjectRef } from './object';
Expand Down Expand Up @@ -78,41 +70,23 @@ export class ImplementableLoadableNodeRef<
this.cacheResolved =
typeof cacheResolved === 'function' ? cacheResolved : cacheResolved && toKey;

this.builder.configStore.onTypeConfig(this, (config) => {
config.extensions = {
(
this.builder as typeof builder & {
nodeRef: (ref: ObjectRef<Types, unknown>, options: Record<string, unknown>) => void;
}
).nodeRef(this, {
id,
loadManyWithoutCache: (ids: Key[], context: SchemaTypes['Context']) =>
this.getDataloader(context).loadMany(ids),
});

this.updateConfig((config) => ({
...config,
extensions: {
...config.extensions,
getDataloader: this.getDataloader,
cacheResolved: this.cacheResolved,
};
(config.pothosOptions as { loadManyWithoutCache: unknown }).loadManyWithoutCache = (
ids: Key[],
context: SchemaTypes['Context'],
) => this.getDataloader(context).loadMany(ids);
});

this.addInterfaces([
(
this.builder as typeof this.builder & { nodeInterfaceRef: () => InterfaceRef<Types, {}> }
).nodeInterfaceRef(),
]);

this.addFields(() => ({
id: (
new ObjectFieldBuilder(this.builder) as PothosSchemaTypes.FieldBuilder<Types, 'Object'> & {
globalID: (options: unknown) => FieldRef<Types>;
}
).globalID({
...(this.builder.options as { relayOptions?: { idFieldOptions?: object } }).relayOptions
?.idFieldOptions,
...id,
nullable: false,
args: {},
resolve: (parent: Shape, args: object, context: object, info: GraphQLResolveInfo) =>
completeValue(id.resolve(parent, args, context, info), (globalId) => ({
type: name,
id: globalId,
})),
}),
},
}));
}
}
9 changes: 8 additions & 1 deletion packages/plugin-relay/src/global-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
type ShapeFromTypeParam,
inputShapeKey,
} from '@pothos/core';
import type { NodeRef } from './node-ref';
import type { ImplementableNodeRef, NodeRef } from './node-ref';
import type {
ConnectionResultShape,
ConnectionShape,
Expand All @@ -39,6 +39,7 @@ import type {
NodeFieldOptions,
NodeListFieldOptions,
NodeObjectOptions,
NodeRefOptions,
PageInfoShape,
RelayMutationFieldOptions,
RelayMutationInputOptions,
Expand Down Expand Up @@ -87,6 +88,12 @@ declare global {
pageInfoRef: () => ObjectRef<Types, PageInfoShape>;
nodeInterfaceRef: () => InterfaceRef<Types, unknown>;

nodeRef: <Shape, IDShape = string, Param extends string | ObjectRef<Types, Shape> = string>(
param: Param,
options: NodeRefOptions<Types, Shape, Shape, IDShape>,
fields?: ObjectFieldsShape<Types, Shape>,
) => Param extends string ? ImplementableNodeRef<Types, Shape, Shape, IDShape> : Param;

node: <
Interfaces extends InterfaceParam<Types>[],
Param extends ObjectParam<Types>,
Expand Down
40 changes: 35 additions & 5 deletions packages/plugin-relay/src/node-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ObjectRef, type SchemaTypes } from '@pothos/core';
import {
ImplementableObjectRef,
type ObjectParam,
ObjectRef,
type SchemaTypes,
} from '@pothos/core';
import type { NodeRefOptions } from './types';
import { addNodeProperties } from './utils/add-node-props';

export const relayIDShapeKey = Symbol.for('Pothos.relayIDShapeKey');

Expand All @@ -12,12 +19,35 @@ export class NodeRef<Types extends SchemaTypes, T, P = T, IDShape = string> exte
parseId: ((id: string, ctx: object) => IDShape) | undefined;

constructor(
builder: PothosSchemaTypes.SchemaBuilder<Types>,
name: string,
options: {
parseId?: (id: string, ctx: object) => IDShape;
},
param: ObjectParam<Types>,
options: NodeRefOptions<Types, T, P, IDShape>,
) {
super(name);
this.parseId = options.parseId;
this.parseId = options.id.parse;

addNodeProperties(name, builder, this, param, options);
}
}

export class ImplementableNodeRef<
Types extends SchemaTypes,
T,
P = T,
IDShape = string,
> extends ImplementableObjectRef<Types, T, P> {
parseId: ((id: string, ctx: object) => IDShape) | undefined;

constructor(
builder: PothosSchemaTypes.SchemaBuilder<Types>,
name: string,
options: NodeRefOptions<Types, T, P, IDShape>,
) {
super(builder, name);

this.parseId = options.id.parse;

addNodeProperties(name, builder, this, undefined, options);
}
}
121 changes: 49 additions & 72 deletions packages/plugin-relay/src/schema-builder.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import SchemaBuilder, {
BaseTypeRef,
completeValue,
createContextCache,
type FieldMap,
type FieldRef,
getTypeBrand,
InputObjectRef,
type InterfaceParam,
type InterfaceRef,
type MaybePromise,
type ObjectFieldsShape,
type ObjectParam,
ObjectRef,
type OutputRef,
PothosValidationError,
type SchemaTypes,
verifyRef,
} from '@pothos/core';
import { type GraphQLResolveInfo, defaultTypeResolver } from 'graphql';
import { NodeRef } from './node-ref';
import type { ConnectionShape, GlobalIDShape, PageInfoShape } from './types';
import { defaultTypeResolver } from 'graphql';
import { ImplementableNodeRef, NodeRef } from './node-ref';
import type { ConnectionShape, PageInfoShape } from './types';
import { capitalize, resolveNodes } from './utils';
import { addNodeProperties } from './utils/add-node-props';

const schemaBuilderProto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder<SchemaTypes>;

Expand Down Expand Up @@ -226,80 +225,58 @@ schemaBuilderProto.nodeInterfaceRef = function nodeInterfaceRef() {
return ref;
};

schemaBuilderProto.node = function node(param, { interfaces, extensions, id, ...options }, fields) {
verifyRef(param);
const interfacesWithNode: () => InterfaceParam<SchemaTypes>[] = () => [
this.nodeInterfaceRef(),
...(typeof interfaces === 'function' ? interfaces() : interfaces ?? []),
];
schemaBuilderProto.nodeRef = function nodeRef(param, options) {
if (typeof param === 'string') {
return new ImplementableNodeRef(this, param, options) as never;
}

let nodeName!: string;
addNodeProperties(param.name, this, param, undefined, options);

const ref = this.objectType<[], ObjectParam<SchemaTypes>>(
param,
{
...(options as {}),
extensions: {
...extensions,
pothosParseGlobalID: id.parse,
},
isTypeOf:
options.isTypeOf ??
(typeof param === 'function'
? (maybeNode: unknown, _context: object, _info: GraphQLResolveInfo) => {
if (!maybeNode) {
return false;
}

if (maybeNode instanceof (param as Function)) {
return true;
}

const proto = Object.getPrototypeOf(maybeNode) as { constructor: unknown };

try {
if (proto?.constructor) {
const config = this.configStore.getTypeConfig(proto.constructor as OutputRef);

return config.name === nodeName;
}
} catch {
// ignore
}

return false;
}
: undefined),
interfaces: interfacesWithNode as () => [],
},
fields,
);
return param as never;
};

schemaBuilderProto.node = function node(
param,
{
id,
name,
loadMany,
loadOne,
loadWithoutCache,
loadManyWithoutCache,
brandLoadedObjects,
...options
},
fields,
) {
verifyRef(param);

this.configStore.onTypeConfig(ref, (nodeConfig) => {
nodeName = nodeConfig.name;
const nodeName =
typeof param === 'string' ? param : param instanceof BaseTypeRef ? param.name : name!;

this.objectField(ref, this.options.relay?.idFieldName ?? 'id', (t) =>
t.globalID<{}, false, MaybePromise<GlobalIDShape<SchemaTypes>>>({
nullable: false,
...this.options.relay?.idFieldOptions,
...id,
args: {},
resolve: (parent, args, context, info): MaybePromise<GlobalIDShape<SchemaTypes>> =>
completeValue(id.resolve(parent, args, context, info), (globalId) => ({
type: nodeConfig.name,
id: globalId,
})),
}),
);
const ref = new NodeRef(this, nodeName, param, {
id,
loadMany,
loadOne,
loadWithoutCache,
loadManyWithoutCache,
brandLoadedObjects,
});

const nodeRef = new NodeRef(ref.name, {
parseId: id.parse,
});
if (typeof param !== 'string') {
this.configStore.associateParamWithRef(param, ref);
}

this.configStore.associateParamWithRef(nodeRef, ref);
this.objectType(
ref,
{
name: nodeName,
...options,
},
fields,
);

return nodeRef as never;
return ref as never;
};

schemaBuilderProto.globalConnectionField = function globalConnectionField(name, field) {
Expand Down
28 changes: 14 additions & 14 deletions packages/plugin-relay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,16 +433,11 @@ export type NodeBaseObjectOptionsForParam<
Interfaces extends InterfaceParam<Types>[],
> = ObjectTypeOptions<Types, Param, ParentShape<Types, Param>, Interfaces>;

export type NodeObjectOptions<
Types extends SchemaTypes,
Param extends ObjectParam<Types>,
Interfaces extends InterfaceParam<Types>[],
IDShape = string,
> = NodeBaseObjectOptionsForParam<Types, Param, Interfaces> & {
export type NodeRefOptions<Types extends SchemaTypes, T, P = T, IDShape = string> = {
id: Omit<
FieldOptionsFromKind<
Types,
ParentShape<Types, Param>,
P,
'ID',
false,
{},
Expand All @@ -455,25 +450,30 @@ export type NodeObjectOptions<
parse?: (id: string, ctx: Types['Context']) => IDShape;
};
brandLoadedObjects?: boolean;
loadOne?: (
id: IDShape,
context: Types['Context'],
) => MaybePromise<OutputShape<Types, Param> | null | undefined>;
loadOne?: (id: IDShape, context: Types['Context']) => MaybePromise<T | null | undefined>;
loadMany?: (
ids: IDShape[],
context: Types['Context'],
) => MaybePromise<readonly MaybePromise<OutputShape<Types, Param> | null | undefined>[]>;
) => MaybePromise<readonly MaybePromise<T | null | undefined>[]>;
loadWithoutCache?: (
id: IDShape,
context: Types['Context'],
info: GraphQLResolveInfo,
) => MaybePromise<OutputShape<Types, Param> | null | undefined>;
) => MaybePromise<T | null | undefined>;
loadManyWithoutCache?: (
ids: IDShape[],
context: Types['Context'],
) => MaybePromise<readonly MaybePromise<OutputShape<Types, Param> | null | undefined>[]>;
) => MaybePromise<readonly MaybePromise<T | null | undefined>[]>;
};

export type NodeObjectOptions<
Types extends SchemaTypes,
Param extends ObjectParam<Types>,
Interfaces extends InterfaceParam<Types>[],
IDShape = string,
> = NodeBaseObjectOptionsForParam<Types, Param, Interfaces> &
NodeRefOptions<Types, OutputShape<Types, Param>, ParentShape<Types, Param>, IDShape>;

export type GlobalIDFieldOptions<
Types extends SchemaTypes,
ParentShape,
Expand Down
Loading

0 comments on commit 6a80a7c

Please sign in to comment.