Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/required-typename/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @pothos-examples/required-typename

## 3.1.19

### Patch Changes

- Initial release: Example demonstrating a custom plugin that adds requiredTypename() method to interface and union refs
102 changes: 102 additions & 0 deletions examples/required-typename/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Required Typename Example

This example demonstrates a custom Pothos plugin that adds a `requiredTypename()` method to interface and union refs.

## What it does

The `requiredTypename()` method updates the TypeScript types of an interface or union ref to require that `__typename` is present as a required string property on any fields returning that interface or union. This helps ensure type safety when working with GraphQL's `__typename` field.

**Important:** When `__typename` is provided in resolved values, no `resolveType` or `isTypeOf` is required for unions and interfaces, as GraphQL can use the `__typename` field directly to determine the concrete type.

## Plugin Implementation

The plugin is implemented in `src/required-typename-plugin.ts` and includes:

1. **Type Declarations**: Global TypeScript declarations that add the `requiredTypename()` method to `InterfaceRef`, `ImplementableInterfaceRef`, and `UnionRef`
2. **Method Implementations**: Prototype methods attached to `InterfaceRef` and `UnionRef` classes that return the same ref with updated types

Note: This plugin doesn't need to be registered with the builder since it doesn't use any plugin hooks - it only adds methods and types.

## Usage

```typescript
import SchemaBuilder from '@pothos/core';
import './required-typename-plugin';

const builder = new SchemaBuilder({});

// Create a union with required typename
// When __typename is provided, no resolveType is required
const PetUnion = builder.unionType('Pet', {
types: [DogType, CatType],
}).requiredTypename();

// Create an interface with required typename - chained immediately
const NodeInterface = builder.interfaceRef<{ id: string }>('Node').implement({
fields: (t) => ({
id: t.exposeID('id'),
}),
}).requiredTypename();

// Now when you use these types, TypeScript enforces __typename
builder.queryType({
fields: (t) => ({
pets: t.field({
type: [PetUnion],
resolve: () => {
// TypeScript requires __typename to be present in the returned objects
return [
{ __typename: 'Dog', name: 'Fido', breed: 'Golden Retriever' },
{ __typename: 'Cat', name: 'Whiskers', livesRemaining: 9 },
];
},
}),
}),
});
```

## Running the Example

```bash
# Install dependencies (from repo root)
pnpm install

# Start the server
pnpm --filter @pothos-examples/required-typename start

# Run tests
pnpm --filter @pothos-examples/required-typename test

# Type check
pnpm --filter @pothos-examples/required-typename type
```

The server will start at http://localhost:4000/graphql with a GraphiQL interface.

## Example Query

```graphql
query ExampleQuery {
pets {
__typename
... on Dog {
name
breed
}
... on Cat {
name
livesRemaining
}
}
nodes {
__typename
id
... on Person {
name
}
... on Company {
companyName
}
}
}
```
21 changes: 21 additions & 0 deletions examples/required-typename/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"private": true,
"$schema": "https://json.schemastore.org/package.json",
"version": "3.1.19",
"name": "@pothos-examples/required-typename",
"main": "src/server.ts",
"types": "src/server.ts",
"scripts": {
"start": "node -r @swc-node/register src/server.ts",
"test": "vitest",
"type": "tsc --noEmit"
},
"dependencies": {
"@pothos/core": "workspace:*",
"graphql": "^16.10.0",
"graphql-yoga": "5.15.2"
},
"publishConfig": {
"access": "public"
}
}
8 changes: 8 additions & 0 deletions examples/required-typename/src/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import SchemaBuilder from '@pothos/core';
import './required-typename-plugin';

export const builder = new SchemaBuilder<{
Scalars: {
ID: { Input: string; Output: string };
};
}>({});
62 changes: 62 additions & 0 deletions examples/required-typename/src/required-typename-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ImplementableInterfaceRef, InterfaceRef, type SchemaTypes, UnionRef } from '@pothos/core';

// Add requiredTypename method to InterfaceRef
InterfaceRef.prototype.requiredTypename = function requiredTypename<
Types extends SchemaTypes,
T,
P,
>(this: InterfaceRef<Types, T, P>) {
// Return the same ref but with updated types to require __typename
return this as unknown as InterfaceRef<Types, T & { __typename: string }, P>;
};

// Add requiredTypename method to ImplementableInterfaceRef
// This extends InterfaceRef but needs its own prototype method implementation
ImplementableInterfaceRef.prototype.requiredTypename = function requiredTypename<
Types extends SchemaTypes,
T,
P,
>(this: ImplementableInterfaceRef<Types, T, P>) {
// Return the same ref but with updated types to require __typename
return this as unknown as ImplementableInterfaceRef<Types, T & { __typename: string }, P>;
};

// Add requiredTypename method to UnionRef
UnionRef.prototype.requiredTypename = function requiredTypename<Types extends SchemaTypes, T, P>(
this: UnionRef<Types, T, P>,
) {
// Return the same ref but with updated types to require __typename
return this as unknown as UnionRef<Types, T & { __typename: string }, P>;
};

// Global type declarations to make the methods available in TypeScript
declare global {
export namespace PothosSchemaTypes {
export interface InterfaceRef<Types extends SchemaTypes, T, P = T> {
/**
* Returns the same interface ref but with __typename as a required field.
* This ensures that any fields returning this interface will have __typename: string
* as a required property in their resolved type.
*/
requiredTypename(): InterfaceRef<Types, T & { __typename: string }, P>;
}

export interface ImplementableInterfaceRef<Types extends SchemaTypes, T, P = T> {
/**
* Returns the same interface ref but with __typename as a required field.
* This ensures that any fields returning this interface will have __typename: string
* as a required property in their resolved type.
*/
requiredTypename(): ImplementableInterfaceRef<Types, T & { __typename: string }, P>;
}

export interface UnionRef<Types extends SchemaTypes, T, P = T> {
/**
* Returns the same union ref but with __typename as a required field.
* This ensures that any fields returning this union will have __typename: string
* as a required property in their resolved type.
*/
requiredTypename(): UnionRef<Types, T & { __typename: string }, P>;
}
}
}
94 changes: 94 additions & 0 deletions examples/required-typename/src/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { execute, parse } from 'graphql';
import { describe, expect, it } from 'vitest';
import { schema } from './schema';

describe('requiredTypename plugin', () => {
it('should resolve pets with __typename', async () => {
const query = parse(`
query {
pets {
__typename
... on Dog {
name
breed
}
... on Cat {
name
livesRemaining
}
}
}
`);

const result = await execute({ schema, document: query });

expect(result.errors).toBeUndefined();
expect(result.data?.pets).toBeDefined();
expect(Array.isArray(result.data?.pets)).toBe(true);

const pets = result.data?.pets as Array<{ __typename: string; name: string }>;
expect(pets.length).toBeGreaterThan(0);

for (const pet of pets) {
expect(pet.__typename).toBeDefined();
expect(['Dog', 'Cat']).toContain(pet.__typename);
expect(pet.name).toBeDefined();
}
});

it('should resolve nodes with __typename', async () => {
const query = parse(`
query {
nodes {
__typename
id
... on Person {
name
}
... on Company {
companyName
}
}
}
`);

const result = await execute({ schema, document: query });

expect(result.errors).toBeUndefined();
expect(result.data?.nodes).toBeDefined();
expect(Array.isArray(result.data?.nodes)).toBe(true);

const nodes = result.data?.nodes as Array<{ __typename: string; id: string }>;
expect(nodes.length).toBeGreaterThan(0);

for (const node of nodes) {
expect(node.__typename).toBeDefined();
expect(['Person', 'Company']).toContain(node.__typename);
expect(node.id).toBeDefined();
}
});

it('should resolve single pet with __typename', async () => {
const query = parse(`
query {
pet(name: "Fido") {
__typename
... on Dog {
name
breed
}
}
}
`);

const result = await execute({ schema, document: query });

expect(result.errors).toBeUndefined();
expect(result.data?.pet).toBeDefined();

const pet = result.data?.pet as { __typename: string; name: string; breed: string };
expect(pet.__typename).toBe('Dog');
expect(pet.name).toBe('Fido');
expect(pet.breed).toBe('Golden Retriever');
});
});
Loading