- Cache related directives
@complexitydirective for list queries
https://www.apollographql.com/docs/apollo-server/data/resolvers
-
Update files in
./types/according to API design then runnpm run gento updateschema.graphqland graphql Types for resolvers- Add directives accordingly for caching / authorization / ratelimit
- Add Type Mapper config in
./codegen.jsonfor new type in schema
-
Create the resolver file
- Create a new file in the appropriate directory (e.g., mutations/ or queries/)
- Export the resolver as default
- Import necessary types from definitions
-
Implement authentication/authorization
- Check viewer context for authentication
- Verify appropriate permissions/roles
- Throw AuthenticationError if unauthorized
-
Validate input
- Check required fields are present
- Validate field formats and constraints
- Throw Errors defined in
src/common/errors.jsfor invalid inputs - Handle special validations (e.g., datetime ranges)
- Validate global IDs:
const { id, type } = fromGlobalId(globalId) if (type !== NODE_TYPES.ExpectedType) { throw new UserInputError('Invalid id type') } return id
- Use appropriate error codes:
BAD_USER_INPUTfor validation errorsENTITY_NOT_FOUNDfor not found errorsFORBIDDENfor permission errorsUNAUTHENTICATEDfor authentication errors
-
Implement business logic
- Use service layer classes for database operations
- Handle translations if needed
- Process data transformations
- Manage relationships between entities
-
Return response
- Format response according to GraphQL schema
- Include all required fields
- Handle errors appropriately
- Transform data if needed (e.g., ID to global ID)
- Important: Avoid returning
nullwhen cache invalidation is needed, asinvalidateFQCrequires ID information to work properly - For mutations, invalidate related cache after database operations using
invalidateFQC(cache of returned object is handled bylogCachedirective):import { invalidateFQC } from '@matters/apollo-response-cache' // Invalidate cache for specific node type await invalidateFQC({ node: { type: NODE_TYPES.Article, id }, redis }) return channel
- If the operation might result in no data but cache invalidation is still needed, consider:
- Returning an empty object with required fields (like
id) - Throwing an appropriate error instead of returning
null - Handling the cache invalidation before the early return
- Returning an empty object with required fields (like
-
Register resolver
- Import resolver in index file
- Add to appropriate export object (Query/Mutation)
-
Add tests
- Create test file in
types/__test__directory - Test authorization
- Test input validation
- Test successful operations
- Test error cases
- Test edge cases
- Create test file in
For implementing GraphQL connections (pagination), use the following utility functions from #common/utils/index.js:
-
connectionFromArray: For simple array dataif (!data || data.length === 0) { return connectionFromArray([], input) }
-
connectionFromPromisedArray: For async data loadingreturn connectionFromPromisedArray( dataLoader.loadMany(ids), input, totalCount // optional )
-
fromConnectionArgs: For parsing pagination argumentsconst { take, skip } = fromConnectionArgs(input, { allowTakeAll?: boolean, defaultTake?: number, maxTake?: number, maxSkip?: number })
-
connectionFromQuery: For database queries with cursor-based paginationreturn connectionFromQuery({ query, orderBy: { column: 'order', order: 'desc' }, cursorColumn: 'id', args: input })
Example:
// Implement resolver
const resolver: GQLDraftResolvers['collections'] = async (
{ collections },
{ input },
{ dataSources: { atomService } }
) => {
if (!collections || collections.length === 0) {
return connectionFromArray([], input)
}
return connectionFromPromisedArray(
atomService.collectionIdLoader.loadMany(collections),
input
)
}Warning: Cache invalidation requires ID information to work properly. Avoid returning
nullwhen cache invalidation is needed.
Common Pitfalls:
- Returning
nullfrom mutations that need cache invalidation - Early returns that bypass cache invalidation logic
- Missing ID information in returned objects
Best Practices:
-
Always ensure ID is available for cache invalidation:
// ❌ Bad: Returns null, cache invalidation fails if (!channel) { return null } // ✅ Good: Handle cache invalidation before early return if (!channel) { // Invalidate cache if needed await invalidateFQC({ node: { type: NODE_TYPES.Channel, id: input.id }, redis }) throw new UserInputError('Channel not found') }
-
Return objects with required fields instead of null:
// ❌ Bad: Returns null return null // ✅ Good: Returns object with ID for cache invalidation return { id: channelId, __type: 'Channel' }
-
Handle cache invalidation before any early returns:
// ✅ Good: Cache invalidation happens regardless of return value await invalidateFQC({ node: { type: NODE_TYPES.Channel, id }, redis }) if (!result) { return { id, __type: 'Channel' } } return result
Note: Always use
__type(not__typename) for type discrimination in resolver return objects. This ensures consistency with our type resolution logic and cache handling.
- return
__typein resolvers:
// Example from comment/node.ts
const resolver = async ({ targetId, targetTypeId, type }, _, { dataSources: { atomService } }) => {
// Determine type and fetch data
if (type === COMMENT_TYPE.article) {
const draft = await atomService.articleIdLoader.load(targetId)
return { ...draft, __type: 'Article' }
} else if (type === COMMENT_TYPE.moment) {
const moment = await atomService.momentIdLoader.load(targetId)
return { ...moment, __type: 'Moment' }
}
// ... handle other types
}- implement
__resolveTypeon Unions/Interfaces
// Interface/Union need to add `__resolveType`
const resolvers = {
Node: {
__resolveType: ({ __type }) => __type
}
}
// Add Interface/Union to typeResolver in `src/schema.ts` for cache operation
const typeResolver = (type: string, result: any) => {
const unionsAndInterfaces = [
...
NODE_TYPES.Channel,
]
if (unionsAndInterfaces.indexOf(type as NODE_TYPES) >= 0 && result?.__type) {
return result.__type
}
return type
}