@lbu/code-gen
Version:
Generate various boring parts of your server
417 lines (348 loc) • 8.6 kB
JavaScript
import { isNil } from "@lbu/stdlib";
import { lowerCaseFirst } from "../utils.js";
import { TypeBuilder } from "./TypeBuilder.js";
import { buildOrInfer } from "./utils.js";
export class RouteBuilder extends TypeBuilder {
constructor(method, group, name, path) {
super("route", group, name);
this.data.method = method;
this.data.path = path;
this.data.tags = [];
this.queryBuilder = undefined;
this.paramsBuilder = undefined;
this.bodyBuilder = undefined;
this.filesBuilder = undefined;
this.responseBuilder = undefined;
}
/**
* @param {string} values
* @returns {RouteBuilder}
*/
tags(...values) {
for (const v of values) {
this.data.tags.push(lowerCaseFirst(v));
}
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteBuilder}
*/
query(builder) {
this.queryBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteBuilder}
*/
params(builder) {
this.paramsBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteBuilder}
*/
body(builder) {
if (["POST", "PUT", "PATCH", "DELETE"].indexOf(this.data.method) === -1) {
throw new Error("Can only use body on POST, PUT, PATCH or DELETE routes");
}
this.bodyBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteBuilder}
*/
files(builder) {
if (["POST", "PUT", "PATCH"].indexOf(this.data.method) === -1) {
throw new Error("Can only use files on POST, PUT or PATCH routes");
}
this.filesBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteBuilder}
*/
response(builder) {
this.responseBuilder = builder;
return this;
}
build() {
const result = super.build();
if (this.bodyBuilder && this.filesBuilder) {
throw new Error(
`Route ${result.group} - ${result.name} can't have both body and files.`,
);
}
if (this.queryBuilder) {
result.query = buildOrInfer(this.queryBuilder);
if (isNil(result.query.name)) {
result.query.group = result.group;
result.query.name = `${result.name}Query`;
}
}
if (this.bodyBuilder) {
result.body = buildOrInfer(this.bodyBuilder);
if (isNil(result.body.name)) {
result.body.group = result.group;
result.body.name = `${result.name}Body`;
}
}
if (this.filesBuilder) {
result.files = buildOrInfer(this.filesBuilder);
if (isNil(result.files.name)) {
result.files.group = result.group;
result.files.name = `${result.name}Files`;
}
}
if (this.responseBuilder) {
result.response = buildOrInfer(this.responseBuilder);
if (isNil(result.response.name)) {
result.response.group = result.group;
result.response.name = `${result.name}Response`;
}
}
const pathParamKeys = collectPathParams(result.path);
if (this.paramsBuilder || pathParamKeys.length > 0) {
const paramsResult = this.paramsBuilder
? buildOrInfer(this.paramsBuilder)
: buildOrInfer({});
paramsResult.group = result.group;
paramsResult.name = `${result.name}Params`;
for (const param of pathParamKeys) {
if (isNil(paramsResult.keys?.[param])) {
throw new Error(
`Route ${result.group}->${result.name} is missing a type definition for '${param}' parameter.`,
);
}
}
for (const key of Object.keys(paramsResult.keys ?? {})) {
if (pathParamKeys.indexOf(key) === -1) {
throw new Error(
`Route ${result.group}->${result.name} has type definition for '${key}' but is not found in the path: ${result.path}`,
);
}
}
result.params = paramsResult;
}
return result;
}
}
/**
* Collect all path params
*
* @param {string} path
* @returns {string[]}
*/
function collectPathParams(path) {
const keys = [];
for (const part of path.split("/")) {
if (part.startsWith(":")) {
keys.push(part.substring(1));
}
}
return keys;
}
export class RouteCreator {
constructor(group, path) {
this.data = {
group,
path,
};
if (this.data.path.startsWith("/")) {
this.data.path = this.data.path.slice(1);
}
this.defaultTags = [];
this.queryBuilder = undefined;
this.paramsBuilder = undefined;
this.bodyBuilder = undefined;
this.filesBuilder = undefined;
this.responseBuilder = undefined;
}
/**
* @param {string} values
* @returns {RouteCreator}
*/
tags(...values) {
this.defaultTags.push(...values);
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteCreator}
*/
query(builder) {
this.queryBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteCreator}
*/
params(builder) {
this.paramsBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteCreator}
*/
body(builder) {
this.bodyBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteCreator}
*/
files(builder) {
this.filesBuilder = builder;
return this;
}
/**
* @param {TypeBuilderLike} builder
* @returns {RouteCreator}
*/
response(builder) {
this.responseBuilder = builder;
return this;
}
/**
* @param {string} name
* @param {string} path
* @returns {RouteCreator}
*/
group(name, path) {
return new RouteCreator(name, concatenateRoutePaths(this.data.path, path));
}
/**
* @param {string} [path]
* @param {string} [name]
* @returns {RouteBuilder}
*/
get(path, name) {
return this.create(
"GET",
this.data.group,
name || "get",
concatenateRoutePaths(this.data.path, path || "/"),
);
}
/**
* @param {string} [path]
* @param {string} [name]
* @returns {RouteBuilder}
*/
post(path, name) {
return this.create(
"POST",
this.data.group,
name || "post",
concatenateRoutePaths(this.data.path, path || "/"),
);
}
/**
* @param {string} [path]
* @param {string} [name]
* @returns {RouteBuilder}
*/
put(path, name) {
return this.create(
"PUT",
this.data.group,
name || "put",
concatenateRoutePaths(this.data.path, path || "/"),
);
}
/**
* @param {string} [path]
* @param {string} [name]
* @returns {RouteBuilder}
*/
patch(path, name) {
return this.create(
"PATCH",
this.data.group,
name || "patch",
concatenateRoutePaths(this.data.path, path || "/"),
);
}
/**
* @param {string} [path]
* @param {string} [name]
* @returns {RouteBuilder}
*/
delete(path, name) {
return this.create(
"DELETE",
this.data.group,
name || "delete",
concatenateRoutePaths(this.data.path, path || "/"),
);
}
/**
* @param {string} [path]
* @param {string} [name]
* @returns {RouteBuilder}
*/
head(path, name) {
return this.create(
"HEAD",
this.data.group,
name || "get",
concatenateRoutePaths(this.data.path, path || "/"),
);
}
/**
* Create a new RouteBuilder and add the defaults if exists.
*
* @param {string} method
* @param {string} group
* @param {string} name
* @param {string} path
* @returns {RouteBuilder}
*/
create(method, group, name, path) {
const b = new RouteBuilder(method, group, name, path);
b.tags(...this.defaultTags);
if (!isNil(this.paramsBuilder)) {
b.params(this.paramsBuilder);
}
if (!isNil(this.queryBuilder)) {
b.query(this.queryBuilder);
}
if (
!isNil(this.bodyBuilder) &&
["POST", "PUT", "PATCH", "DELETE"].indexOf(method) !== -1
) {
b.body(this.bodyBuilder);
}
if (
!isNil(this.filesBuilder) &&
["POST", "PUT", "PATCH"].indexOf(method) !== -1
) {
b.files(this.filesBuilder);
}
if (!isNil(this.responseBuilder)) {
b.response(this.responseBuilder);
}
return b;
}
}
/**
* @param {string} path1
* @param {string} path2
* @returns {string}
*/
function concatenateRoutePaths(path1, path2) {
if (!path1.endsWith("/")) {
path1 += "/";
}
if (path2.startsWith("/")) {
path2 = path2.substring(1);
}
return path1 + path2;
}