UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

418 lines (361 loc) 14.2 kB
import { isNil } from "@compas/stdlib"; import { upperCaseFirst } from "../../utils.js"; import { fileBlockEnd, fileBlockStart } from "../file/block.js"; import { fileContextAddLinePrefix, fileContextCreateGeneric, fileContextRemoveLinePrefix, fileContextSetIndent, } from "../file/context.js"; import { fileFormatInlineComment } from "../file/format.js"; import { fileWrite } from "../file/write.js"; import { crudInformationGetHasCustomReadableType, crudInformationGetModel, crudInformationGetName, crudInformationGetParamName, crudInformationGetParent, crudInformationGetRelation, } from "../processors/crud-information.js"; import { crudRouteSwitch, structureCrud } from "../processors/crud.js"; import { modelKeyGetPrimary } from "../processors/model-keys.js"; import { JavascriptImportCollector } from "../target/javascript.js"; import { crudPartialRouteCreate, crudPartialRouteDelete, crudPartialRouteList, crudPartialRouteSingle, crudPartialRouteUpdate, } from "./partials/routes.js"; import { crudQueryBuilderGet } from "./query-builder.js"; /** * Generate the handler implementations that are necessary for CRUD. This currently only * works with js and Koa. * * @param {import("../generate").GenerateContext} generateContext */ export function crudHandlersGenerate(generateContext) { if (generateContext.options.targetLanguage !== "js") { return; } if (isNil(generateContext.options.generators.router?.target?.library)) { return; } for (const crud of structureCrud(generateContext)) { const file = crudHandlersFile(generateContext, crud); crudHandlersStart(generateContext, file, crud); crudHandlersGenerateForType(generateContext, file, crud); crudHandlersEnd(generateContext, file, crud); } } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersFile(generateContext, crud) { return fileContextCreateGeneric(generateContext, `${crud.group}/crud.js`, { importCollector: new JavascriptImportCollector(), }); } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersStart(generateContext, file, crud) { const { modifierDocs, modifierDestructure } = crudHandlersGetModifiers(crud); fileWrite(file, `/**`); fileContextAddLinePrefix(file, ` * `); fileWrite( file, `Register controller implementation for the '${crud.group}' routes.`, ); fileWrite(file, `@param {{`); fileContextSetIndent(file, 1); fileWrite(file, `sql: Postgres,`); fileWrite(file, modifierDocs.join("\n")); fileContextSetIndent(file, -1); fileWrite(file, `}} options`); fileContextRemoveLinePrefix(file, 3); fileWrite(file, `*/`); fileWrite(file, `export function ${crud.group}RegisterCrud({`); fileContextSetIndent(file, 1); fileWrite(file, `sql,`); fileWrite(file, modifierDestructure.join("\n")); fileContextSetIndent(file, -1); fileBlockStart(file, `})`); } /** * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud * @returns {{ * modifierDocs: string[], * modifierDestructure: string[], * }} */ function crudHandlersGetModifiers(crud) { const modifierDocs = []; const modifierDestructure = []; const model = crudInformationGetModel(crud); const relation = crudInformationGetRelation(crud); const modelUniqueName = upperCaseFirst(model.group) + upperCaseFirst(model.name); const crudName = crud.group + upperCaseFirst(crudInformationGetName(crud, "")); const upperCrudName = upperCaseFirst(crudName); if (crudInformationGetHasCustomReadableType(crud)) { modifierDocs.push( `${crudName}Transform: (entity: QueryResult${modelUniqueName}) => ${upperCaseFirst( // @ts-expect-error crud.fieldOptions.readableType.reference.group, )}${upperCaseFirst( // @ts-expect-error crud.fieldOptions.readableType.reference.name, )},`, ); modifierDestructure.push(`${crudName}Transform,`); } if (crud.routeOptions.listRoute) { modifierDocs.push( `${crudName}ListPreModifier?: (event: InsightEvent, ctx: ${upperCrudName}ListCtx, countBuilder: ${modelUniqueName}QueryBuilder, listBuilder: ${modelUniqueName}QueryBuilder) => void|Promise<void>,`, ); modifierDestructure.push(`${crudName}ListPreModifier,`); } if (crud.routeOptions.singleRoute) { modifierDocs.push( `${crudName}SinglePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}SingleCtx, singleBuilder: ${modelUniqueName}QueryBuilder) => void|Promise<void>,`, ); modifierDestructure.push(`${crudName}SinglePreModifier,`); } if (crud.routeOptions.createRoute) { modifierDocs.push( `${crudName}CreatePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}CreateCtx ${ relation?.subType === "oneToMany" ? "" : `, singleBuilder: ${modelUniqueName}QueryBuilder` }) => void|Promise<void>,`, ); modifierDestructure.push(`${crudName}CreatePreModifier,`); } if (crud.routeOptions.updateRoute) { modifierDocs.push( `${crudName}UpdatePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}SingleCtx, singleBuilder: ${modelUniqueName}QueryBuilder) => void|Promise<void>,`, ); modifierDestructure.push(`${crudName}UpdatePreModifier,`); } if (crud.routeOptions.deleteRoute) { modifierDocs.push( `${crudName}DeletePreModifier?: (event: InsightEvent, ctx: ${upperCrudName}SingleCtx, singleBuilder: ${modelUniqueName}QueryBuilder) => void|Promise<void>,`, ); modifierDestructure.push(`${crudName}DeletePreModifier,`); } for (const nestedCrud of crud.nestedRelations) { // @ts-expect-error const result = crudHandlersGetModifiers(nestedCrud); modifierDocs.push(...result.modifierDocs); modifierDestructure.push(...result.modifierDestructure); } return { modifierDocs, modifierDestructure, }; } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersEnd(generateContext, file, crud) { fileWrite(file, `\n${fileFormatInlineComment(file, crud.group)}`); fileBlockEnd(file); } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersGenerateForType(generateContext, file, crud) { crudRouteSwitch( crud, { listRoute: crudHandlersList, singleRoute: crudHandlersSingle, createRoute: crudHandlersCreate, updateRoute: crudHandlersUpdate, deleteRoute: crudHandlersDelete, }, [generateContext, file, crud], ); for (const nestedCrud of crud.nestedRelations) { // @ts-expect-error crudHandlersGenerateForType(generateContext, file, nestedCrud); } } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersList(generateContext, file, crud) { const importCollector = JavascriptImportCollector.getImportCollector(file); const model = crudInformationGetModel(crud); const { primaryKeyName } = modelKeyGetPrimary(model); const data = { handlerName: `${crud.group}Handlers.${crudInformationGetName( crud, "list", )}`, crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")), countBuilder: crudQueryBuilderGet(crud, { includeOwnParam: false, includeJoins: false, traverseParents: true, partial: { select: [`'${primaryKeyName}'`], orderBy: "ctx.validatedBody.orderBy", orderBySpec: "ctx.validatedBody.orderBySpec", }, }), listBuilder: crudQueryBuilderGet(crud, { includeOwnParam: false, includeJoins: true, traverseParents: false, partial: { orderBy: "ctx.validatedBody.orderBy", orderBySpec: "ctx.validatedBody.orderBySpec", }, }), primaryKey: primaryKeyName, }; importCollector.destructure("@compas/stdlib", "newEventFromEvent"); importCollector.destructure("./events.js", `${data.crudName}Count`); importCollector.destructure("./events.js", `${data.crudName}List`); importCollector.destructure("./controller.js", `${crud.group}Handlers`); if (!crudInformationGetHasCustomReadableType(crud)) { importCollector.destructure("./events.js", `${data.crudName}Transform`); } fileWrite(file, crudPartialRouteList(data)); } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersSingle(generateContext, file, crud) { const importCollector = JavascriptImportCollector.getImportCollector(file); const model = crudInformationGetModel(crud); const { primaryKeyName } = modelKeyGetPrimary(model); const data = { handlerName: `${crud.group}Handlers.${crudInformationGetName( crud, "single", )}`, crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")), builder: crudQueryBuilderGet(crud, { includeOwnParam: true, includeJoins: true, traverseParents: true, }), primaryKey: primaryKeyName, }; importCollector.destructure("@compas/stdlib", "newEventFromEvent"); importCollector.destructure("./events.js", `${data.crudName}Single`); importCollector.destructure("./controller.js", `${crud.group}Handlers`); if (!crudInformationGetHasCustomReadableType(crud)) { importCollector.destructure("./events.js", `${data.crudName}Transform`); } fileWrite(file, crudPartialRouteSingle(data)); } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersCreate(generateContext, file, crud) { const importCollector = JavascriptImportCollector.getImportCollector(file); const parent = crudInformationGetParent(crud); const relation = crudInformationGetRelation(crud); const data = { handlerName: `${crud.group}Handlers.${crudInformationGetName( crud, "create", )}`, crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")), applyParams: crud.fromParent ? { bodyKey: relation.referencedKey, paramsKey: crudInformationGetParamName(parent), } : undefined, oneToOneChecks: relation?.subType === "oneToOneReverse" ? { builder: crudQueryBuilderGet(crud, { includeOwnParam: true, includeJoins: false, traverseParents: true, }), } : undefined, }; importCollector.destructure("@compas/stdlib", "newEventFromEvent"); importCollector.destructure("./events.js", `${data.crudName}Create`); importCollector.destructure("./controller.js", `${crud.group}Handlers`); if (!crudInformationGetHasCustomReadableType(crud)) { importCollector.destructure("./events.js", `${data.crudName}Transform`); } if (relation?.subType === "oneToOneReverse") { importCollector.destructure("@compas/stdlib", "AppError"); } // @ts-expect-error fileWrite(file, crudPartialRouteCreate(data)); } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersUpdate(generateContext, file, crud) { const importCollector = JavascriptImportCollector.getImportCollector(file); const data = { handlerName: `${crud.group}Handlers.${crudInformationGetName( crud, "update", )}`, crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")), builder: crudQueryBuilderGet(crud, { includeOwnParam: true, includeJoins: true, traverseParents: true, }), }; importCollector.destructure("@compas/stdlib", "newEventFromEvent"); importCollector.destructure("./events.js", `${data.crudName}Update`); importCollector.destructure("./events.js", `${data.crudName}Single`); importCollector.destructure("./controller.js", `${crud.group}Handlers`); fileWrite(file, crudPartialRouteUpdate(data)); } /** * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalCrudDefinition>} crud */ function crudHandlersDelete(generateContext, file, crud) { const importCollector = JavascriptImportCollector.getImportCollector(file); const data = { handlerName: `${crud.group}Handlers.${crudInformationGetName( crud, "delete", )}`, crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")), builder: crudQueryBuilderGet(crud, { includeOwnParam: true, includeJoins: false, traverseParents: true, }), }; importCollector.destructure("@compas/stdlib", "newEventFromEvent"); importCollector.destructure("./events.js", `${data.crudName}Delete`); importCollector.destructure("./events.js", `${data.crudName}Single`); importCollector.destructure("./controller.js", `${crud.group}Handlers`); fileWrite(file, crudPartialRouteDelete(data)); }