API Generator
The API Generator can be used to generate the usual CRUD operations in GraphQL for an entity.
Annotate entity
The API Generator uses the entity and the fields defined within it to generate resolvers, services, inputs, and other
DTOs for the feature. For this, the entity must be annotated with the CrudGenerator
decorator:
@CrudGenerator({ targetDirectory: `${__dirname}/../generated/` })
export class Product extends BaseEntity<Product, "id"> {
// ...
}
The option targetDirectory
specifies the path where the generated files should be written.
For features that should exist only once per scope (e.g., a footer), there is the special CrudSingleGenerator
decorator. The usage of both decorators is the same.
@CrudGenerator()
options
Parameter | Type | Default | Description |
---|---|---|---|
targetDirectory | string | Required | The directory where the CRUD operations are generated. |
requiredPermission | string[] \| string | undefined | Permission(s) required to access the CRUD operations. |
create | boolean | true | If true , includes the "create" operation. |
update | boolean | true | If true , includes the "update" operation. |
delete | boolean | true | If true , includes the "delete" operation. |
list | boolean | true | If true , includes the "list" operation. |
position | object | undefined | Only relevant if the entity has the magic position field. This option allows to split the position by specific fields. E.g. { groupByFields: ["country"] } means the position starts over at 1 for each distinct country value. |
Annotate field
By default, all entities' fields are used for search, filtering, sorting, and input. If you want to change this for a
specific field (e.g., making description
not filterable), you can adjust it with the @CrudField
decorator.
@CrudField({
search: true,
filter: false,
sort: true,
input: true,
})
description: string;
@CrudField()
options
Parameter | Type | Default | Description |
---|---|---|---|
search | boolean | true | Specifies if the field should be searchable. |
filter | boolean | true | Specifies if the field should be filterable. |
sort | boolean | true | Specifies if the field should be sortable. |
input | boolean | true | Specifies if the field should be included in input types (e.g., for create/update). |
resolveField | boolean | true | Relevant for relations. Indicates if a field resolver for the relation should be added to the resolver. |
dedicatedResolverArg | boolean | false | Relevant for relations. Adds a dedicated resolver argument for the relation to the create mutation. Otherwise it's included in the input object. |
Generating code
After the entity has been successfully annotated, you can run the API Generator. Newer projects should already have an
api-generator
npm script.
If it's still missing, you can add it to api/package.json
:
{
...
"scripts": {
"api-generator": "rimraf 'src/*/generated' && comet-api-generator generate",
...
}
}
Now you can run the generator with npm run api-generator
. The generated files are located in the specified
targetDirectory
.
Although this is generated code, it should still be checked into the repository. This enables a quick start of the API.
Register generated resolvers and services
The resolvers and services created by the API Generator must be registered in the corresponding module:
import { ProductsService } from "./generated/products.service";
import { ProductResolver } from "./generated/product.resolver";
@Module({
// ...
providers: [ProductResolver, ProductsService],
})
export class ProductsModule {}
Depending on the magic fields of the entity (e.g., position
), the service might not be generated.
Done! The CRUD operations now appear in the GraphQL schema and can be used.
The generated code must be viewed as a self-contained unit and can change incompatibly even between minor versions.
You should not reference the generated code externally (except, of course, to provide the resolver in the module).
Changing the entity
When making changes (e.g., adding a new field) to an entity annotated with the CrudGenerator, the API Generator must be
run again: npm run api-generator
. The resulting changes must be checked into the repository.
The CI/CD pipeline checks whether the checked-in code matches the generated code. See the
lint:generated-files-not-modified
script in api/package.json
.
Magic fields
The API generator supports the following magic fields:
position
Adding a position
field enables item ordering. The generated code ensures unique positions and updates them during
create, update, or delete actions.
status
A status
field lets you filter items by status in the list query.
scope
The API generator treats a scope
as a COMET content scope. A scope
arg is added to the list
and create operations, ensuring the scope check can be made.
If no scope
field is present, the scope check is skipped for all operations.
slug
Adding a slug
field generates a entityBySlug
operation in the API.
Customizing
Don't make any changes in the generated files! They will be overwritten during the next run of the API Generator.
If the generated code does not meet your requirements, there are two ways to add custom functionality:
- Extending the generated code without changing it (recommended)
- Scaffolding (if the generated code is not suitable at all)
Always try to extend the generated code instead of scaffolding if possible.
Extending
Resolver
You can't add custom code to the generated resolver directly. Instead, the recommended way is to create a second, non-generated resolver for specific functionality:
// products/generated/product.resolver.ts
// Generated; don't touch this!
@Resolver(() => Product)
export class ProductResolver {
// ...
}
// products/custom-product.resolver.ts
// custom resolver
@Resolver(() => Product)
export class CustomProductResolver {
// ...
}
GraphQL will automatically "merge" the resolvers if the returned entities in @Resolver(() => Entity)
is identical.
Service
You can't add custom code to the generated service directly. Instead, the recommended way is to create a second, non-generated service for specific functionality:
// products/generated/products.service.ts
// Generated; don't touch this!
@Injectable()
export class ProductsService {
constructor(
private readonly entityManager: EntityManager,
@InjectRepository(Product) private readonly repository: EntityRepository<Product>,
) {}
// ...
}
// products/custom-products.service.ts
// custom service
@Injectable()
export class CustomProductsService {
constructor(private readonly productsService: ProductsService) {}
calculateVAT(product: Product): number {
return Number(((Number(product.price) / 1.2) * 0.2).toFixed(2));
}
}
Scaffolding
If the generated code doesn't fit your needs at all, you can "scaffold" the code. To do this, you must
- Remove the
@Crud\*
decorators from the entity
@ObjectType()
@Entity()
@RootBlockEntity<Product>({ isVisible: (product) => product.status === ProductStatus.Published })
- @CrudGenerator({ targetDirectory: `${__dirname}/../generated/` })
export class Product extends BaseEntity<Product, "id"> {
// ...
}
- Move the generated files outside the /generated folder
renamed: demo/api/src/products/generated/dto/paginated-products.ts -> demo/api/src/products/paginated-products.ts
renamed: demo/api/src/products/generated/dto/product.filter.ts -> demo/api/src/products/product.filter.ts
renamed: demo/api/src/products/generated/dto/product.input.ts -> demo/api/src/products/product.input.ts
renamed: demo/api/src/products/generated/product.resolver.ts -> demo/api/src/products/product.resolver.ts
renamed: demo/api/src/products/generated/dto/product.sort.ts -> demo/api/src/products/product.sort.ts
- Remove the comments at the start of each generated file
- // This file has been generated by comet api-generator.
- // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
// ...
Now the code is completely under your control and can be adjusted as needed.
Scaffolding is all or nothing!
If you decide to scaffold, you must remove all code from the /generated folder. It's not possible to "split" the code, e.g. by scaffolding only the resolver and keeping the DTOs generated. This will likely lead to major issues during the next major update (maybe even after minor updates).
FAQ: Extend or scaffold?
Always try to extend the generated code instead of scaffolding if possible.
I want to add an additional field resolver for the entity
Don't scaffold.
Instead, create a second, custom resolver as described above.
I need custom logic only in my create mutation
Don't scaffold.
Instead, deactivate create
in the @CrudGenerator
decorator:
@CrudGenerator({
targetDirectory: `${__dirname}/../generated/`,
create: false,
})
export class Product extends BaseEntity<Product, "id"> {
// ...
}
Then create a second, custom resolver and implement the create mutation there. This way, the other CRUD operations are still managed by the generator.