@compas/code-gen
Version:
Generate various boring parts of your server
352 lines (302 loc) • 12.5 kB
JavaScript
import { isNil } from "@compas/stdlib";
import { fileContextCreateGeneric } from "../file/context.js";
import { fileWrite } from "../file/write.js";
import {
crudInformationGetHasCustomReadableType,
crudInformationGetModel,
crudInformationGetName,
crudInformationGetReadableType,
crudInformationGetRelation,
crudInformationGetWritableType,
} from "../processors/crud-information.js";
import { crudRouteSwitch, structureCrud } from "../processors/crud.js";
import { modelKeyGetPrimary } from "../processors/model-keys.js";
import { structureResolveReference } from "../processors/structure.js";
import { JavascriptImportCollector } from "../target/javascript.js";
import { upperCaseFirst } from "../utils.js";
import {
crudPartialEventCount,
crudPartialEventCreate,
crudPartialEventDelete,
crudPartialEventList,
crudPartialEventSingle,
crudPartialEventTransformer,
crudPartialEventUpdate,
} from "./partials/events.js";
/**
* Generate events that are necessary for CRUD. This currently only works with js and Koa.
*
* @param {import("../generate.js").GenerateContext} generateContext
*/
export function crudEventsGenerate(generateContext) {
if (isNil(generateContext.options.generators.router?.target?.library)) {
return;
}
// TODO: the types used in the generated events expect global types, handle support for global or
// imported types. The types will be generated tho by for example the router generator, but that
// is implicitly done instead of the explicitness that we want.
for (const crud of structureCrud(generateContext)) {
const file = crudEventsFile(generateContext, crud);
crudEventsGenerateForType(generateContext, file, crud);
}
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsGenerateForType(generateContext, file, crud) {
crudRouteSwitch(
crud,
{
listRoute: crudEventsList,
singleRoute: crudEventsSingle,
createRoute: crudEventsCreate,
updateRoute: crudEventsUpdate,
deleteRoute: crudEventsDelete,
},
[generateContext, file, crud],
);
if (
!crud.routeOptions.singleRoute &&
(crud.routeOptions.createRoute ||
crud.routeOptions.updateRoute ||
crud.routeOptions.deleteRoute)
) {
crudEventsSingle(generateContext, file, crud);
}
if (
crudInformationGetHasCustomReadableType(crud) === false &&
(crud.routeOptions.listRoute ||
crud.routeOptions.singleRoute ||
crud.routeOptions.createRoute)
) {
crudEventsTransform(generateContext, file, crud);
}
for (const nestedCrud of crud.nestedRelations) {
// @ts-expect-error
crudEventsGenerateForType(generateContext, file, nestedCrud);
}
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsFile(generateContext, crud) {
return fileContextCreateGeneric(
generateContext,
`${crud.group}/events.${generateContext.options.targetLanguage}`,
{
importCollector: new JavascriptImportCollector(),
contents: "// @ts-nocheck\n\n",
},
);
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsList(generateContext, file, crud) {
const importCollector = JavascriptImportCollector.getImportCollector(file);
const model = crudInformationGetModel(crud);
const { primaryKeyName, primaryKeyDefinition } = modelKeyGetPrimary(model);
const primaryKeyType =
primaryKeyDefinition.type === "reference" ?
structureResolveReference(generateContext.structure, primaryKeyDefinition)
.type
: primaryKeyDefinition.type;
importCollector.destructure("@compas/stdlib", "eventStart");
importCollector.destructure("@compas/stdlib", "eventStop");
importCollector.destructure(
`../database/${model.name}.${generateContext.options.forceTsExtensionImports ? "ts" : "js"}`,
`query${upperCaseFirst(model.name)}`,
);
const data = {
crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")),
entityName: model.name,
entityUniqueName: upperCaseFirst(model.group) + upperCaseFirst(model.name),
primaryKey: primaryKeyName,
primaryKeyType: primaryKeyType === "number" ? "number" : "string",
};
// @ts-expect-error
fileWrite(file, crudPartialEventCount(data));
// @ts-expect-error
fileWrite(file, crudPartialEventList(data));
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsSingle(generateContext, file, crud) {
const importCollector = JavascriptImportCollector.getImportCollector(file);
const model = crudInformationGetModel(crud);
importCollector.destructure("@compas/stdlib", "AppError");
importCollector.destructure("@compas/stdlib", "eventStart");
importCollector.destructure("@compas/stdlib", "eventStop");
importCollector.destructure(
`../database/${model.name}.${generateContext.options.forceTsExtensionImports ? "ts" : "js"}`,
`query${upperCaseFirst(model.name)}`,
);
const data = {
crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")),
entityName: model.name,
entityUniqueName: upperCaseFirst(model.group) + upperCaseFirst(model.name),
};
// @ts-expect-error
fileWrite(file, crudPartialEventSingle(data));
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsCreate(generateContext, file, crud) {
const importCollector = JavascriptImportCollector.getImportCollector(file);
const model = crudInformationGetModel(crud);
const { primaryKeyName } = modelKeyGetPrimary(model);
importCollector.destructure("@compas/stdlib", "eventStart");
importCollector.destructure("@compas/stdlib", "eventStop");
importCollector.destructure("@compas/stdlib", "newEventFromEvent");
importCollector.destructure(
`../common/database.${generateContext.options.forceTsExtensionImports ? "ts" : "js"}`,
`queries`,
);
const data = {
crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")),
entityName: model.name,
entityUniqueName: upperCaseFirst(model.group) + upperCaseFirst(model.name),
primaryKey: primaryKeyName,
writableType: crudInformationGetWritableType(crud),
inlineRelations: crudEventsGetInlineRelations(crud),
};
// @ts-expect-error
fileWrite(file, crudPartialEventCreate(data));
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsUpdate(generateContext, file, crud) {
const importCollector = JavascriptImportCollector.getImportCollector(file);
const model = crudInformationGetModel(crud);
const { primaryKeyName } = modelKeyGetPrimary(model);
importCollector.destructure("@compas/stdlib", "eventStart");
importCollector.destructure("@compas/stdlib", "eventStop");
importCollector.destructure(
`../common/database.${generateContext.options.forceTsExtensionImports ? "ts" : "js"}`,
`queries`,
);
const data = {
crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")),
entityName: model.name,
entityUniqueName: upperCaseFirst(model.group) + upperCaseFirst(model.name),
primaryKey: primaryKeyName,
writableType: crudInformationGetWritableType(crud),
inlineRelations: crudEventsGetInlineRelations(crud),
};
// @ts-expect-error
fileWrite(file, crudPartialEventUpdate(data));
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsDelete(generateContext, file, crud) {
const importCollector = JavascriptImportCollector.getImportCollector(file);
const model = crudInformationGetModel(crud);
const { primaryKeyName } = modelKeyGetPrimary(model);
importCollector.destructure("@compas/stdlib", "eventStart");
importCollector.destructure("@compas/stdlib", "eventStop");
importCollector.destructure(
`../common/database.${generateContext.options.forceTsExtensionImports ? "ts" : "js"}`,
`queries`,
);
const data = {
crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")),
entityName: model.name,
entityUniqueName: upperCaseFirst(model.group) + upperCaseFirst(model.name),
primaryKey: primaryKeyName,
};
// @ts-expect-error
fileWrite(file, crudPartialEventDelete(data));
}
/**
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsTransform(generateContext, file, crud) {
const model = crudInformationGetModel(crud);
const data = {
crudName: crud.group + upperCaseFirst(crudInformationGetName(crud, "")),
entityName: model.name,
entityUniqueName: upperCaseFirst(model.group) + upperCaseFirst(model.name),
readableType: crudInformationGetReadableType(crud),
entity: crudEventsGetEntityTransformer(crud),
};
// @ts-expect-error
fileWrite(file, crudPartialEventTransformer(data));
}
/**
* Get metadata about inline relations for create and update events
*
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsGetInlineRelations(crud) {
const result = [];
const parentModel = crudInformationGetModel(crud);
const { primaryKeyName } = modelKeyGetPrimary(parentModel);
for (const inlineCrud of crud.inlineRelations) {
const model = crudInformationGetModel(inlineCrud);
const relation = crudInformationGetRelation(inlineCrud);
result.push({
// @ts-expect-error
name: inlineCrud.fromParent.field,
referencedKey: relation.referencedKey,
entityName: model.name,
isInlineArray: relation.subType === "oneToMany",
isOwningSideOfRelation:
relation.subType === "manyToOne" || relation.subType === "oneToOne",
// @ts-expect-error
inlineRelations: crudEventsGetInlineRelations(inlineCrud),
isOptional: inlineCrud.isOptional,
parentPrimaryKey: primaryKeyName,
});
}
return result;
}
/**
* Get the transformer mapping for the provided crud
*
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureCrudDefinition>} crud
*/
function crudEventsGetEntityTransformer(crud) {
const model = crudInformationGetModel(crud);
let keys = Object.keys(model.keys);
// @ts-expect-error
if (crud.fieldOptions?.readable?.$pick?.length > 0) {
keys = crud.fieldOptions?.readable?.$pick ?? [];
}
for (const omit of crud.fieldOptions?.readable?.$omit ?? []) {
if (keys.includes(omit)) {
keys.splice(keys.indexOf(omit), 1);
}
}
const result = {};
for (const key of keys) {
result[key] = true;
}
for (const inlineCrud of crud.inlineRelations) {
const relation = crudInformationGetRelation(inlineCrud);
// @ts-expect-error
const nested = crudEventsGetEntityTransformer(inlineCrud);
// @ts-expect-error
result[inlineCrud.fromParent.field] =
relation.subType === "oneToMany" ? [nested] : nested;
}
return result;
}