Code generator CRUD
Compas code-gen also supports generating CRUD routes. It combines the features of the api generator, sql generator and a generated implementation of the necessary events.
TIP
Requires @compas/cli
, @compas/stdlib
, @compas/store
and @compas/code-gen
to be installed.
The features
CRUD generation supports quite a variety of features and combinations there of. Let's break them all down;
Route selection:
Any CRUD declaration fully controls which routes are generated, by explicitly enabling them.
const T = new TypeCreator("tag");
const Tdatabase = new TypeCreator("database");
Tdatabase.object("tag")
.keys({
key: T.string().searchable().trim().min(3),
value: T.string(),
})
.enableQueries({
withDates: true,
});
T.crud("/tag").entity(T.reference("database", "tag")).routes({
listRoute: true,
singleRoute: true,
createRoute: true,
updateRoute: true,
deleteRoute: true,
});
Combining all options, it generates the following routes;
apiTagList
//tag/list
apiTagSingle
//tag/:tagId/single
apiTagCreate
//tag/create
apiTagUpdate
//tag/:tagId/update
apiTagDelete
//tag/:tagId/delete
Filters, sorting and pagination:
The generated list
route comes fully equipped with filters, sorting and pagination. The filters are a subset of the filters as supported by the query builder, ie ignoring $raw
and $or
to prevent SQL injection and too complex filters respectively. Sorting is also based on the current query builder behaviour where you specify the columns to be sorted on in orderBy
and a seperated orderBySpec
to determine the sort order for that column. And finally pagination is supported via the offset
and limit
params.
Inline relations
The CRUD generator can also include inline relations in the response, but also allow creating and updating them via their respective routes. This works for both oneToMany
relations as for the referenced side of an oneToOne
relation. These inline relations can be added via .inlineRelations()
like the following;
const T = new TypeCreator("post");
const Tdatabase = new TypeCreator("database");
Tdatabase.object("post")
.keys({
title: T.string().searchable(),
})
.enableQueries({
withDates: true,
})
.relations(T.oneToMany("tags", T.reference("database", "tag")));
Tdatabase.object("tag")
.keys({
key: T.string().searchable().trim().min(3),
value: T.string(),
})
.enableQueries({
withDates: true,
})
.relations(T.manyToOne("post", T.reference("database", "post"), "tags"));
T.crud("/post")
.entity(T.reference("database", "post"))
.routes({
listRoute: true,
createRoute: true,
updateRoute: true,
})
.inlineRelations(T.crud().fromParent("tags", { name: "tag" }));
The above generates approximately the following type for both the read routes, like apiPostList
, as well as for the write routes like apiPostCrete
and apiPostUpdate
.
type PostItem = {
id: string;
title: string,
tags: { id: string, key: string, value: string }[]
}
When updating an inline relation, all existing values are first removed, before the new values are added. oneToOne
relations are mandatory by default, but can be made optional via T.crud().fromParent(...).optional()
. Inline relations can be nested as many times as required.
Nested relations
The same thing as above can be done but now with .nestedRelations
. This creates a nested route structure.
// Using the same Post -> tags[] relation like above
T.crud("/post")
.entity(T.reference("database", "post"))
.routes({
listRoute: true,
singleRoute: true,
createRoute: true,
updateRoute: true,
deleteRoute: true,
})
.nestedRelations(
T.crud("/tag").fromParent("tags", { name: "tag" }).routes({
// Routes need to be enabled
listRoute: true,
singleRoute: true,
createRoute: true,
updateRoute: true,
deleteRoute: true,
}),
);
This generates the following routes:
apiPostList
//post/list
apiPostSingle
//post/:postId/single
apiPostCreate
//post/create
apiPostUpdate
//post/:postId/update
apiPostDelete
//post/:postId/delete
apiPostTagList
//post/:postId/taglist
apiPostTagSingle
//post/:postId/tag/:tagId/single
apiPostTagCreate
//post/:postId/tag/create
apiPostTagUpdate
//post/:postId/tag/:tagId/update
apiPostTagDelete
//post/:postId/tag/:tagId/delete
Appropriate route invalidations for react-query generator are automatically added in all cases. In case a nested relation is used with a oneToOne
relation, the list
route is automatically disabled, and the extra route params are removed. So /post/:postId/author/:authorId/single
is shortened to /post/:postId/author/single
.
Modifiers
While calling groupRegisterCrud
from the generated crud.js
you can pass in various 'modifiers'. These modifiers are all optional and can mutate the passed in context, resolve a user, determine access control and edit the provided 'builders'. They are called after the static validation of params, query and body, but before executing any other logic.
All modifier functions can be async, getting an event
as the first argument and the request ctx
as the second. The single
, update
and delete
routes also provide the used builder
as the third argument. This way you can mutate the executed where clause to prevent unauthorized access.
The list
route modifier passes in a countBuilder
and a listBuilder
as the third and fourth argument respectively. By mutating both, the total stays in sync with the returned values. Note that the result of the count
event is used to mutate the listBuilder
afterwards to only select the results of the current pagination result.
Another edge case is the create
event for a nested oneToOne
relation. It's third argument is the same builder as used in the single
routes, for checking if the relation already the oneToOne
field.
TODO
- fields readable, writable