@compas/code-gen
Version:
Generate various boring parts of your server
251 lines (215 loc) • 6.16 kB
JavaScript
import { AppError, isNil } from "@compas/stdlib";
import { buildOrInfer } from "../builders/index.js";
import { TypeBuilder } from "../builders/TypeBuilder.js";
/**
* Type builder for generating CRUD routes
*/
export class CrudType extends TypeBuilder {
/**
* Create a new Crud route;
*
* Not supported yet;
* - soft deletable entities
* - Relations to StoreFile
*
* @example
* T.crud()
* .entity(T.reference("database", "post"))
* .nestedRelations(
* T.crud()
* .fromParent("comments", { name: "comment" })
* .routes({
* createRoute: false,
* updateRoute: false,
* deleteRoute: false,
* })
* );
* @param {string} group
* @param {string} [basePath]
*/
constructor(group, basePath) {
super("crud", group, `internalCrud`);
/**
* @private
*/
this.inlineRelationsCache = [];
/**
* @private
*/
this.nestedRelationsCache = [];
/**
* @private
*/
this.readableType = undefined;
/** @type {any} */
this.data = {
...this.data,
basePath,
routeOptions: {},
fieldOptions: {},
inlineRelations: [],
nestedRelations: [],
};
}
/**
* Entity for which this crud route is created
*
* @param {import("../../types/advanced-types.js").TypeBuilderLike} reference
* @returns {CrudType}
*/
entity(reference) {
this.data.entity = buildOrInfer(reference);
return this;
}
/**
* Create a nested or inline CRUD configuration. The field should correspond to one of
* the relations of the parent entity. The relation defined on 'field' should be the
* owning side of the relation, resolving to the parent entity. Path part is mandatory
* for nested relations.
*
* Note that options.name is mandatory if the 'field' is a `oneToMany` relation.
* If no `path` is passed to `T.crud()` it defaults to `/$options.name`.
*
* @param {string} field
* @param {{
* name?: string
* }} options
* @returns {CrudType}
*/
fromParent(field, options = {}) {
this.data.name = undefined;
this.data.fromParent = { field, options };
return this;
}
/**
* Enable routes that should be generated. Can not be used on inline relations
*
* @param {{
* listRoute?: boolean,
* singleRoute?: boolean,
* createRoute?: boolean,
* updateRoute?: boolean,
* deleteRoute?: boolean,
* }} routeOptions
* @returns {CrudType}
*/
routes(routeOptions) {
this.data.routeOptions = routeOptions;
return this;
}
/**
* Omit or pick fields that can be set or are returned from the routes.
* It is still possible to provide these fields via the generated controller hooks
*
* @param {{
* readable: {
* $omit?: string[],
* $pick?: string[],
* }|TypeBuilderLike,
* writable: {
* $omit?: string[],
* $pick?: string[],
* }
* }} fieldOptions
* @returns {CrudType}
*/
fields(fieldOptions) {
const { readable, writable } = fieldOptions;
if (readable instanceof TypeBuilder) {
if (!readable.data.name) {
throw AppError.serverError({
message:
"A custom readable type should have a name, e.g 'T.object('item').keys(...)'.",
});
}
this.data.fieldOptions = {
readable: {},
writable,
};
this.readableType = readable;
} else {
this.data.fieldOptions = fieldOptions;
}
return this;
}
/**
*
* @param {...import("../../types/advanced-types.js").TypeBuilderLike} builders
* @returns {CrudType}
*/
inlineRelations(...builders) {
this.inlineRelationsCache = builders;
return this;
}
/**
*
* @param {...import("../../types/advanced-types.js").TypeBuilderLike} builders
* @returns {CrudType}
*/
nestedRelations(...builders) {
this.nestedRelationsCache = builders;
return this;
}
build() {
if (isNil(this.data.entity) && isNil(this.data.fromParent)) {
throw AppError.serverError({
message: `T.crud() should either call '.entity()' when toplevel, or '.fromParent()' when nested.`,
});
}
if (!isNil(this.data.entity) && !this.data.basePath) {
throw AppError.serverError({
message: `T.crud(path) should be called a top-level path.`,
});
}
const result = super.build();
if (this.readableType) {
result.fieldOptions.readableType = buildOrInfer(this.readableType);
}
result.inlineRelations = this.inlineRelationsCache.map((it) =>
this.processRelation("inline", result, it),
);
result.nestedRelations = this.nestedRelationsCache.map((it) =>
this.processRelation("nested", result, it),
);
return result;
}
/**
* @private
* @param {string} type
* @param {any} result
* @param {CrudType} it
*/
processRelation(type, result, it) {
it.data.group = result.group;
it.data.name = undefined;
const build = buildOrInfer(it);
if (build.type !== "crud") {
throw AppError.serverError({
message: `Values passed to 'T.crud().${type}Relations' should be created via 'T.crud().fromParent()`,
found: build.type,
});
}
if (typeof build.fromParent?.field !== "string") {
throw AppError.serverError({
message: `Values passed to 'T.crud().${type}Relations' should pass the key of the relation that is ${type} to 'T.crud().fromParent().`,
});
}
if (type === "nested" && !it.data.basePath) {
throw AppError.serverError({
message:
"T.crud()'s provided in 'nestedRelations' should have a path specified.",
});
}
if (type === "nested" && it.data.isOptional) {
throw AppError.serverError({
message: `T.crud()'s provided in 'nestedRelations' can't be '.optional()'`,
});
}
if (type === "inline" && Object.keys(it.data.routeOptions).length > 0) {
throw AppError.serverError({
message: `Inline CRUD can't specify 'routeOptions'.`,
});
}
return build;
}
}