@compas/code-gen
Version:
Generate various boring parts of your server
455 lines (412 loc) • 13.5 kB
JavaScript
import { importCreator } from "../generator/utils.js";
import { partialAsString } from "../partials/helpers.js";
import { structureIteratorNamedTypes } from "../structure/structureIterators.js";
import { upperCaseFirst } from "../utils.js";
import {
crudPartialRouteCreate,
crudPartialRouteDelete,
crudPartialRouteList,
crudPartialRouteSingle,
crudPartialRouteUpdate,
} from "./partials/routes.js";
import { crudCreateName, crudResolveGroup } from "./resolvers.js";
import {
crudCallFunctionsForRoutes,
crudCreateRouteParam,
} from "./route-functions.js";
/**
* Create the implementation of the controllers, including hooks
*
* @param {import("../generated/common/types.js").CodeGenContext} context
*/
export function crudGenerateRouteImplementations(context) {
for (const type of structureIteratorNamedTypes(context.structure)) {
if (!("type" in type) || type.type !== "crud") {
continue;
}
const importer = importCreator();
const sources = [];
crudGenerateRouteImplementationForType(context, importer, sources, type);
const { modifierJSDoc, modifierDestructure } = crudResolveModifiersForType(
context,
type,
);
sources.unshift(`
/**
* Register controller implementations for the '${crudResolveGroup(
type,
)}' routes.
*
* This function accepts various optional hooks that will be called in the implementations.
*
* @param {{
* sql: Postgres,
${modifierJSDoc}
* }} options
*/
export function ${crudResolveGroup(
type,
)}RegisterCrud({ sql, ${modifierDestructure} }) {
`);
sources.push("}");
sources.unshift(importer.print());
context.outputFiles.push({
contents: partialAsString(sources),
relativePath: `./${type.group}/crud.js`,
});
}
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generator/utils.js").ImportCreator} importer
* @param {string[]} sources
* @param {import("../generated/common/types.js").CodeGenCrudType} type
*/
function crudGenerateRouteImplementationForType(
context,
importer,
sources,
type,
) {
crudCallFunctionsForRoutes(
{
listRoute: crudGenerateRouteImplementationListRoute,
singleRoute: crudGenerateRouteImplementationSingleRoute,
createRoute: crudGenerateRouteImplementationCreateRoute,
updateRoute: crudGenerateRouteImplementationUpdateRoute,
deleteRoute: crudGenerateRouteImplementationDeleteRoute,
},
type,
[context, importer, sources, type],
);
for (const relation of type.nestedRelations) {
crudGenerateRouteImplementationForType(
context,
importer,
sources,
relation,
);
}
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generated/common/types.js").CodeGenCrudType} type
* @returns {{
* modifierJSDoc: string,
* modifierDestructure: string,
* }}
*/
function crudResolveModifiersForType(context, type) {
let modifierJSDoc = "";
let modifierDestructure = "";
const crudName =
crudResolveGroup(type) + upperCaseFirst(crudCreateName(type, ""));
const upperCrudName = upperCaseFirst(crudName);
if (type.routeOptions.listRoute) {
// @ts-expect-error
modifierJSDoc += ` * ${crudName}ListPreModifier?: (event: InsightEvent, ctx: ${upperCrudName}ListCtx, countBuilder: ${type.entity.reference.uniqueName}QueryBuilder, listBuilder: ${type.entity.reference.uniqueName}QueryBuilder) => void|Promise<void>,\n`;
modifierDestructure += `${crudName}ListPreModifier,\n`;
}
if (type.routeOptions.singleRoute) {
// @ts-expect-error
modifierJSDoc += ` * ${crudName}SinglePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}SingleCtx, singleBuilder: ${type.entity.reference.uniqueName}QueryBuilder) => void|Promise<void>,\n`;
modifierDestructure += `${crudName}SinglePreModifier,\n`;
}
if (type.routeOptions.createRoute) {
modifierJSDoc += ` * ${crudName}CreatePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}CreateCtx ${
type.internalSettings.usedRelation?.subType === "oneToOneReverse"
? `, singleBuilder: ${
// @ts-expect-error
type.entity.reference.uniqueName
}QueryBuilder`
: ""
}) => void|Promise<void>,\n`;
modifierDestructure += `${crudName}CreatePreModifier,\n`;
}
if (type.routeOptions.updateRoute) {
// @ts-expect-error
modifierJSDoc += ` * ${crudName}UpdatePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}SingleCtx, singleBuilder: ${type.entity.reference.uniqueName}QueryBuilder) => void|Promise<void>,\n`;
modifierDestructure += `${crudName}UpdatePreModifier,\n`;
}
if (type.routeOptions.deleteRoute) {
// @ts-expect-error
modifierJSDoc += ` * ${crudName}DeletePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}SingleCtx, singleBuilder: ${type.entity.reference.uniqueName}QueryBuilder) => void|Promise<void>,\n`;
modifierDestructure += `${crudName}DeletePreModifier,\n`;
}
for (const relation of type.nestedRelations) {
const result = crudResolveModifiersForType(context, relation);
modifierJSDoc += result.modifierJSDoc;
modifierDestructure += result.modifierDestructure;
}
return {
modifierDestructure,
modifierJSDoc,
};
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generator/utils.js").ImportCreator} importer
* @param {string[]} sources
* @param {import("../generated/common/types.js").CodeGenCrudType} type
*/
function crudGenerateRouteImplementationListRoute(
context,
importer,
sources,
type,
) {
const data = {
handlerName: `${crudResolveGroup(type)}Handlers.${crudCreateName(
type,
"list",
)}`,
crudName: crudResolveGroup(type) + upperCaseFirst(crudCreateName(type, "")),
countBuilder: crudFormatBuilder(
crudGetBuilder(type, {
includeOwnParam: false,
includeJoins: false,
traverseParents: true,
partial: {
// @ts-expect-error
select: [`'${type.internalSettings.primaryKey.key}'`],
orderBy: "ctx.validatedBody.orderBy",
orderBySpec: "ctx.validatedBody.orderBySpec",
},
}),
),
listBuilder: crudFormatBuilder(
crudGetBuilder(type, {
includeOwnParam: false,
includeJoins: true,
traverseParents: false,
partial: {
orderBy: "ctx.validatedBody.orderBy",
orderBySpec: "ctx.validatedBody.orderBySpec",
},
}),
),
// @ts-expect-error
primaryKey: type.internalSettings.primaryKey.key,
};
importer.destructureImport(`newEventFromEvent`, "@compas/stdlib");
importer.destructureImport(`${data.crudName}Count`, "./events.js");
importer.destructureImport(`${data.crudName}List`, "./events.js");
importer.destructureImport(`${data.crudName}Transform`, "./events.js");
importer.destructureImport(
`${crudResolveGroup(type)}Handlers`,
"./controller.js",
);
sources.push(crudPartialRouteList(data));
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generator/utils.js").ImportCreator} importer
* @param {string[]} sources
* @param {import("../generated/common/types.js").CodeGenCrudType} type
*/
function crudGenerateRouteImplementationSingleRoute(
context,
importer,
sources,
type,
) {
const data = {
handlerName: `${crudResolveGroup(type)}Handlers.${crudCreateName(
type,
"single",
)}`,
crudName: crudResolveGroup(type) + upperCaseFirst(crudCreateName(type, "")),
builder: crudFormatBuilder(
crudGetBuilder(type, {
includeOwnParam: true,
includeJoins: true,
traverseParents: true,
}),
),
};
importer.destructureImport(`newEventFromEvent`, "@compas/stdlib");
importer.destructureImport(`${data.crudName}Transform`, "./events.js");
importer.destructureImport(`${data.crudName}Single`, "./events.js");
importer.destructureImport(
`${crudResolveGroup(type)}Handlers`,
"./controller.js",
);
sources.push(crudPartialRouteSingle(data));
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generator/utils.js").ImportCreator} importer
* @param {string[]} sources
* @param {import("../generated/common/types.js").CodeGenCrudType} type
*/
function crudGenerateRouteImplementationCreateRoute(
context,
importer,
sources,
type,
) {
const data = {
handlerName: `${crudResolveGroup(type)}Handlers.${crudCreateName(
type,
"create",
)}`,
crudName: crudResolveGroup(type) + upperCaseFirst(crudCreateName(type, "")),
applyParams: type.fromParent
? {
// @ts-expect-error
bodyKey: type.internalSettings.usedRelation.referencedKey,
// @ts-expect-error
paramsKey: crudCreateRouteParam(type.internalSettings.parent),
}
: undefined,
oneToOneChecks:
type.internalSettings.usedRelation?.subType === "oneToOneReverse"
? {
builder: crudFormatBuilder(
crudGetBuilder(type, {
includeOwnParam: true,
includeJoins: false,
traverseParents: true,
}),
),
}
: undefined,
};
importer.destructureImport(`newEventFromEvent`, "@compas/stdlib");
importer.destructureImport(`AppError`, "@compas/stdlib");
importer.destructureImport(`${data.crudName}Transform`, "./events.js");
importer.destructureImport(`${data.crudName}Create`, "./events.js");
importer.destructureImport(
`${crudResolveGroup(type)}Handlers`,
"./controller.js",
);
// @ts-expect-error
sources.push(crudPartialRouteCreate(data));
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generator/utils.js").ImportCreator} importer
* @param {string[]} sources
* @param {import("../generated/common/types.js").CodeGenCrudType} type
*/
function crudGenerateRouteImplementationUpdateRoute(
context,
importer,
sources,
type,
) {
const data = {
handlerName: `${crudResolveGroup(type)}Handlers.${crudCreateName(
type,
"update",
)}`,
crudName: crudResolveGroup(type) + upperCaseFirst(crudCreateName(type, "")),
builder: crudFormatBuilder(
crudGetBuilder(type, {
includeOwnParam: true,
includeJoins: true,
traverseParents: true,
}),
),
};
importer.destructureImport(`newEventFromEvent`, "@compas/stdlib");
importer.destructureImport(`${data.crudName}Update`, "./events.js");
importer.destructureImport(`${data.crudName}Single`, "./events.js");
importer.destructureImport(
`${crudResolveGroup(type)}Handlers`,
"./controller.js",
);
sources.push(crudPartialRouteUpdate(data));
}
/**
* @param {import("../generated/common/types.js").CodeGenContext} context
* @param {import("../generator/utils.js").ImportCreator} importer
* @param {string[]} sources
* @param {import("../generated/common/types.js").CodeGenCrudType} type
*/
function crudGenerateRouteImplementationDeleteRoute(
context,
importer,
sources,
type,
) {
const data = {
handlerName: `${crudResolveGroup(type)}Handlers.${crudCreateName(
type,
"delete",
)}`,
crudName: crudResolveGroup(type) + upperCaseFirst(crudCreateName(type, "")),
builder: crudFormatBuilder(
crudGetBuilder(type, {
includeOwnParam: true,
includeJoins: false,
traverseParents: true,
}),
),
};
importer.destructureImport(`newEventFromEvent`, "@compas/stdlib");
importer.destructureImport(`${data.crudName}Delete`, "./events.js");
importer.destructureImport(`${data.crudName}Single`, "./events.js");
importer.destructureImport(
`${crudResolveGroup(type)}Handlers`,
"./controller.js",
);
sources.push(crudPartialRouteDelete(data));
}
/**
* @param {any} builder
* @returns {string}
*/
export function crudFormatBuilder(builder) {
return JSON.stringify(builder, null, 2).replace(/"/gi, "");
}
/**
*
* @param {import("../generated/common/types.js").CodeGenCrudType} type
* @param {{
* includeOwnParam: boolean,
* includeJoins: boolean,
* traverseParents: boolean,
* partial?: any }} opts
* @returns {any}
*/
export function crudGetBuilder(
type,
{ includeOwnParam, includeJoins, traverseParents, partial },
) {
const result = {
...partial,
};
if (!result.where) {
result.where = {};
}
const crudType = type;
if (includeJoins) {
for (const relation of crudType.inlineRelations) {
// @ts-expect-error
result[relation.fromParent.field] = crudGetBuilder(relation, {
includeOwnParam: false,
includeJoins: true,
traverseParents: false,
});
}
}
if (
includeOwnParam &&
type.internalSettings?.usedRelation?.subType !== "oneToOneReverse"
) {
result.where[ // @ts-expect-error
crudType.internalSettings.primaryKey.key
] = `ctx.validatedParams.${crudCreateRouteParam(crudType)}`;
}
if (traverseParents && type.internalSettings.parent) {
result.where[ // @ts-expect-error
`via${upperCaseFirst(type.internalSettings.usedRelation.referencedKey)}`
] = crudGetBuilder(type.internalSettings.parent, {
includeOwnParam: true,
includeJoins: false,
traverseParents: true,
});
}
return result;
}