Skip to content

Commit

Permalink
refactor(experimental): graphql: enable schema extending
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec committed May 4, 2024
1 parent ec860f6 commit 1f83c37
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 18 deletions.
127 changes: 127 additions & 0 deletions packages/rpc-graphql/src/__tests__/customization-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
GetAccountInfoApi,
GetBlockApi,
GetMultipleAccountsApi,
GetProgramAccountsApi,
GetTransactionApi,
Rpc,
} from '@solana/rpc';

import { createRpcGraphQL, resolveAccount } from '../index';
import { createLocalhostSolanaRpc } from './__setup__';

type GraphQLCompliantRpc = Rpc<
GetAccountInfoApi & GetBlockApi & GetMultipleAccountsApi & GetProgramAccountsApi & GetTransactionApi
>;

describe('schema customization', () => {
let rpc: GraphQLCompliantRpc;
beforeEach(() => {
rpc = createLocalhostSolanaRpc();
});

it('query with types', async () => {
expect.assertions(1);

const masterEditionAddress = 'B2Srva38aD8bWpjghkU7jKFUqT1Y4KB2ejAnsJbP2ibA';

// Define custom type definitions for the GraphQL schema.
const customTypeDefs = /* GraphQL */ `
# A Solana Master Edition NFT.
type NftMasterEdition {
address: Address
metadata: Account
mint: Account
}
# Query to retrieve a Solana Master Edition NFT.
type Query {
masterEdition(address: Address!): NftMasterEdition
}
`;

// Define custom resolvers for the GraphQL schema.
const customTypeResolvers = {
// Resolver for the custom `NftMasterEdition` type.
NftMasterEdition: {
metadata: resolveAccount('metadata'),
mint: resolveAccount('mint'),
},
};

// Define custom queries for the GraphQL schema.
const customQueryResolvers = {
// Query to retrieve a Solana Master Edition NFT.
masterEdition: () => {
return {
// Arbitrary address.
address: masterEditionAddress,
// See scripts/fixtures/gpa1.json.
metadata: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
// See scripts/fixtures/spl-token-mint-account.json.
mint: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
};
},
};

// Create the RPC-GraphQL client with the custom type definitions and
// resolvers.
const rpcGraphQL = createRpcGraphQL(rpc, {
queryResolvers: customQueryResolvers,
typeDefs: customTypeDefs,
typeResolvers: customTypeResolvers,
});

// Create a test query for the custom `masterEdition` query.
const source = /* GraphQL */ `
query ($masterEditionAddress: Address!) {
masterEdition(address: $masterEditionAddress) {
address
metadata {
address
lamports
}
mint {
address
lamports
ownerProgram {
address
}
... on MintAccount {
decimals
supply
}
}
}
}
`;

// Execute the test query.
const result = await rpcGraphQL.query(source, {
masterEditionAddress,
});

// Assert the custom type definitions and resolvers were accepted and
// the query was successful.
expect(result).toMatchObject({
data: {
masterEdition: {
address: masterEditionAddress,
metadata: {
address: 'CcYNb7WqpjaMrNr7B1mapaNfWctZRH7LyAjWRLBGt1Fk',
lamports: expect.any(BigInt),
},
mint: {
address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr',
decimals: expect.any(Number),
lamports: expect.any(BigInt),
ownerProgram: {
address: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
},
supply: expect.any(String),
},
},
},
});
});
});
55 changes: 47 additions & 8 deletions packages/rpc-graphql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,51 @@ export interface RpcGraphQL {
): ReturnType<typeof graphql>;
}

export function createRpcGraphQL(
rpc: Parameters<typeof createSolanaGraphQLContext>[0],
config?: Partial<Parameters<typeof createSolanaGraphQLContext>[1]>,
): RpcGraphQL {
type Config = {
/**
* Maximum number of acceptable bytes to waste before splitting two
* `dataSlice` requests into two requests.
*/
maxDataSliceByteRange?: number;
/**
* Maximum number of accounts to fetch in a single batch.
* See https://docs.solana.com/api/http#getmultipleaccounts.
*/
maxMultipleAccountsBatchSize?: number;
/**
* Resolvers for custom queries to extend the default queries.
*/
queryResolvers?: Parameters<typeof makeExecutableSchema>[0]['resolvers'];
/**
* Custom type definitions to extend the default type definitions in
* the GraphQL schema.
*
* Note: custom type definitions may not override existing type
* definitions, but may implement existing interfaces.
*
* These type definitions should correspond to any custom resolvers
* provided in `resolvers`.
*/
typeDefs?: Parameters<typeof makeExecutableSchema>[0]['typeDefs'];
/**
* Custom type resolvers to extend the default resolvers.
*
* These resolvers should correspond to any custom type definitions
* provided in `typeDefs`.
*/
typeResolvers?: Parameters<typeof makeExecutableSchema>[0]['resolvers'];
};

export function createRpcGraphQL(rpc: Parameters<typeof createSolanaGraphQLContext>[0], config?: Config): RpcGraphQL {
const { maxDataSliceByteRange, maxMultipleAccountsBatchSize, queryResolvers, typeResolvers, typeDefs } =
config ?? {};
const rpcGraphQLConfig = {
maxDataSliceByteRange: config?.maxDataSliceByteRange ?? 200,
maxMultipleAccountsBatchSize: config?.maxMultipleAccountsBatchSize ?? 100,
maxDataSliceByteRange: maxDataSliceByteRange ?? 200,
maxMultipleAccountsBatchSize: maxMultipleAccountsBatchSize ?? 100,
};
const schema = makeExecutableSchema({
resolvers: createSolanaGraphQLResolvers(),
typeDefs: createSolanaGraphQLTypeDefs(),
resolvers: createSolanaGraphQLResolvers({ queryResolvers, typeResolvers }),
typeDefs: createSolanaGraphQLTypeDefs({ typeDefs }),
});
return {
async query(source, variableValues?) {
Expand All @@ -36,3 +70,8 @@ export function createRpcGraphQL(
},
};
}

// Export resolvers to be used to build custom resolvers.
export { resolveAccount } from './resolvers/account';
export { resolveBlock } from './resolvers/block';
export { resolveTransaction } from './resolvers/transaction';
16 changes: 14 additions & 2 deletions packages/rpc-graphql/src/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@ import { rootResolvers } from './root';
import { transactionResolvers } from './transaction';
import { typeTypeResolvers } from './types';

export function createSolanaGraphQLResolvers(): Parameters<typeof makeExecutableSchema>[0]['resolvers'] {
type Resolvers = Parameters<typeof makeExecutableSchema>[0]['resolvers'];

export function createSolanaGraphQLResolvers({
queryResolvers,
typeResolvers,
}: {
queryResolvers?: Resolvers;
typeResolvers?: Resolvers;
}): Resolvers {
return {
Query: {
...queryResolvers,
...rootResolvers,
},
...accountResolvers,
...blockResolvers,
...instructionResolvers,
...rootResolvers,
...transactionResolvers,
...typeResolvers,
...typeTypeResolvers,
};
}
10 changes: 4 additions & 6 deletions packages/rpc-graphql/src/resolvers/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import { resolveProgramAccounts } from './program-accounts';
import { resolveTransaction } from './transaction';

export const rootResolvers: Parameters<typeof makeExecutableSchema>[0]['resolvers'] = {
Query: {
account: resolveAccount(),
block: resolveBlock(),
programAccounts: resolveProgramAccounts(),
transaction: resolveTransaction(),
},
account: resolveAccount(),
block: resolveBlock(),
programAccounts: resolveProgramAccounts(),
transaction: resolveTransaction(),
};
19 changes: 17 additions & 2 deletions packages/rpc-graphql/src/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import type { makeExecutableSchema } from '@graphql-tools/schema';

import { accountTypeDefs } from './account';
import { blockTypeDefs } from './block';
import { instructionTypeDefs } from './instruction';
import { rootTypeDefs } from './root';
import { transactionTypeDefs } from './transaction';
import { typeTypeDefs } from './types';

export function createSolanaGraphQLTypeDefs() {
return [accountTypeDefs, blockTypeDefs, instructionTypeDefs, rootTypeDefs, typeTypeDefs, transactionTypeDefs];
type TypeDefs = Parameters<typeof makeExecutableSchema>[0]['typeDefs'];

export function createSolanaGraphQLTypeDefs({ typeDefs }: { typeDefs?: TypeDefs }): TypeDefs {
const schemaTypeDefs = [
accountTypeDefs,
blockTypeDefs,
instructionTypeDefs,
rootTypeDefs,
typeTypeDefs,
transactionTypeDefs,
] as TypeDefs[];
if (typeDefs) {
schemaTypeDefs.push(typeDefs);
}
return schemaTypeDefs;
}

0 comments on commit 1f83c37

Please sign in to comment.