Best Practices
Contents
Recommendations from our experience of extending Guillotine
Naming conventions
When extending existing schema types with custom fields or adding new schema types, make sure to use unique names in order to avoid schema conflicts.
We recommend using prefixes:
`<prefix>_fieldName` - for fields related to your application. `<Prefix>_TypeName` - for other schema types
For fields within your custom schema types, you may safely drop the prefix.
| Apps developed by Enonic never use prefixes. This is done in order to keep commonly used fields and the schema as easy to read as possible. |
Remember, GraphQL fields cannot start with a number, and may not contain - or .. Use _ as a separator instead.
Field placement
When adding new fields, there are several locations where you may be placing them in the schema, here we present the different options, and what to use when:
Query Type (query field)
The Query type only has a single default field guillotine, meaning you may extend it with other fields of your preference.
Generally, fields placed at this level should not be directly related to "content", specifically it means it should not be returning any types generated by Guillotine.
For instance, you may add a field returning the current server time like this:
-
myapp_serverTime: DateTime- this field returns the current server time.
For example:
import type {Extensions, GraphQL} from '@enonic-types/guillotine';
export const extensions = (graphQL: GraphQL): Extensions => {
return {
types: {},
creationCallbacks: {
Query: function (params) {
params.addFields({
myapp_serverTime: {
type: graphQL.DateTime
}
});
}
},
resolvers: {
Query: {
myapp_serverTime: (): string => new Date().toISOString()
}
}
}
};
HeadlessCms Type (guillotine field)
The guillotine field is the only standard root field in the Query type. Utilizing this field enables you to access a variety of functionalities, including retrieving individual Content instances by key.
We recommend to use this rule: if field is related to Content, then add it to the HeadlessCms type.
-
myapp_menu(contentKey: String!): MyApp_Menu- Returns a menu for specifiedContentinstance, the response might include existing Guillotine types likeContent.
For example:
import {get} from '/lib/xp/content';
import type {DataFetchingEnvironment, Extensions, GraphQL} from '@enonic-types/guillotine';
export const extensions = (graphQL: GraphQL): Extensions => {
return {
types: {
MyApp_Menu: {
description: 'A menu entry for a content item',
fields: {
title: {type: graphQL.GraphQLString},
content: {type: graphQL.reference('Content')},
}
}
},
creationCallbacks: {
HeadlessCms: function (params) {
params.addFields({
myapp_menu: {
type: graphQL.reference('MyApp_Menu'),
args: {
contentKey: graphQL.nonNull(graphQL.GraphQLString)
}
}
});
}
},
resolvers: {
HeadlessCms: {
myapp_menu: (env: DataFetchingEnvironment<{contentKey: string}>) => {
const content = get({key: env.args.contentKey});
return content ? {title: content.displayName, content} : null;
}
}
}
}
};
Other types
You may also extend any other standard or generated Guillotine type, for instance a custom content type. The recommendations here are essentially the same as for the HeadlessCms type.
Coding guidelines
Split larger extensions into typed factory functions. Keep guillotine.ts as the entry point and import the factories it needs. For example, these three files define the server-time field from above:
import type {Extensions, GraphQL} from '@enonic-types/guillotine';
import {create as createCallbacks} from './creation-callbacks-factory';
import {create as createResolvers} from './resolvers-factory';
export const extensions = (graphQL: GraphQL): Extensions => ({
creationCallbacks: createCallbacks(graphQL),
resolvers: createResolvers(),
});
import type {Extensions, GraphQL} from '@enonic-types/guillotine';
export const create = (graphQL: GraphQL): NonNullable<Extensions['creationCallbacks']> => ({
Query: (params) => {
params.addFields({
myapp_serverTime: {
type: graphQL.DateTime,
},
});
},
});
import type {Extensions} from '@enonic-types/guillotine';
export const create = (): NonNullable<Extensions['resolvers']> => ({
Query: {
myapp_serverTime: (): string => new Date().toISOString(),
},
});
Use the same pattern for types, inputTypes and other extension properties. You can also split resolver factories by GraphQL type as the extension grows.