Generating CRUD routes
On top of the previous building blocks, routes and entities, Compas can automatically generate CRUD API's for you.
WARNING
This feature is currently only supported with the Koa compatible router and Postgres database client.
Getting started
First specify an entity
import { TypeCreator } from "@compas/code-gen";
const T = new TypeCreator("database");
T.object("todoItem")
.keys({
title: T.string(),
// .searchable() instructs the CRUD generator to use the generated database filter and
// ordering options.
finishedAt: T.string().searchable().optional(),
})
.enableQueries({
withDates: true,
});
Next build up the CRUD you want to support
const T = new TypeCreator("crudTodo");
T.crud("/todo")
// Specify the above entity
.entity(T.reference("database", "todoItem"))
.routes({
// Enable each route individually
listRoute: true,
singleRoute: false,
createRoute: true,
updateRoute: true,
deleteRoute: false,
});
The following routes are created:
apiCrudTodoList(): TodoItem[]
orGET /todo/list
apiCrudTodoCreate(body: TodoItem);
orPOST /todo
apiCrudTodoUpdate(todoId: string, body: TodoItem);
orPUT /todo/:todoId/update
With included support for pagination, filtering and more.
Modifiers and fields
Creating a selection of fields which can be read or written to is possible as well.
T.object("user").keys({
email: T.string(),
password: T.string(),
receiveChangelogEmails: T.bool(),
});
T.crud("/user")
.entity(T.reference("database", "user"))
.routes({
singleRoute: true,
updateRoute: true,
})
.fields({
readable: {
// Don't return the encrypted password
$omit: ["password"],
},
writable: {
// Only allow updating the preference
$pick: ["receiveChangelogEmails"],
},
});
// Results in
declare function apiUserSingle(params: { userId: string }): {
// Doesn't return the password
email: string;
receiveChangelogEmails: boolean;
};
declare function apiUserUpdate(
params: { userId: string },
body: {
// Only allows updating the preference
receiveChangelogEmails: boolean;
},
);
The CRUD implementations will generate modifiers which will be called with the validated input. This allows you to add extra checks, like authentication and authorization, and add or overwrite fields.
// The generated initializer function
userRegisterCrud({
// Pass in the Postgres connection
sql,
// Set a modifier for the single route
userSinglePreModifier(event, ctx) {
// Some way to fetch the user based on their session
const user = resolveUserFromRequest(ctx);
// Authentication check
if (ctx.validatedParams.id !== user.id) {
throw AppError.validationError("user.list.own", {
message: "User can only fetch their own preferences",
});
}
},
});
A custom readable type is supported as well on top level CRUD definitions. Allowing you to flatten many-to-many relations or adding computed fields.
T.crud("/todo")
.entity(T.reference("database", "todo"))
.routes({
listRoute: true,
})
.fields({
readable: T.object("readable").keys({
id: T.uuid(),
title: T.string(),
isCompleted: T.bool(),
createdAt: T.date(),
}),
writable: {},
});
// This also adds a required parameter when registering the CRUD handlers
todoRegisterCrud({
sql,
todoTransform: (entity) => ({
id: entity.id,
title: entity.title,
createdAt: entity.createdAt,
// Our custom field
isCompleted: !isNil(entity.completedAt) && entity.completedAt < new Date(),
}),
});
Relations
Relations are supported in two ways, inline or nested. Inline CRUD allows sub entities to be created, updated or deleted in the same call as the main entity. Nested CRUD generates specific routes for sub entities.
import { TypeCreator } from "@compas/code-gen";
const T = new TypeCreator();
T.crud("/user")
.entity(T.reference("database", "user"))
.routes({
// apiUserSingle({ userId }): { email: string, settings: DatabaseUserSettings };
singleRoute: true,
// apiUserUpdate({ userId }, { settings: { some: "new setting" } })
updateRoute: true,
})
.fields({
readable: {
$pick: ["email"],
},
writable: {
// Don't allow writes on the user
$pick: [],
},
})
.inlineRelations(
// The single
T.crud().fromParent("settings"),
)
.nestedRelations(
T.crud().fromParent("posts", { name: "post" }).routes({
// apiUserPostList({ userId });
listRoute: true,
// apiUserPostSingle({ userId, postId });
singleRoute: true,
}),
);