@udraft/core
Version:
uDraft is a language and stack agnostic code-generation tool that simplifies full-stack development by converting a single YAML file into code for rapid development.
356 lines (308 loc) • 11.1 kB
text/typescript
import { UModel } from "../entities/model";
import { URenderer } from "../entities/renderer";
import { addPackageJsonDependency } from "../helpers/package";
import { $attr } from "../shortcuts/queries";
import { RenderContent, RenderPath, RenderSelection } from "../types/renderer";
import { closeCursor, writeToCursor } from "../utils/rendering";
import { UModule } from "../entities/module";
import TSClassRenderer from "./ts-class-renderer";
import {
_array,
_in,
_max,
_maxLength,
_min,
_minLength,
_notEmpty,
_notIn,
_required,
_size,
_regex,
_schema,
_ref,
_unique,
_index,
_noId,
_enum,
_virtual,
_rootModule,
_textIndex,
} from "../shortcuts/attributes";
import { UField } from "../entities/field";
import { MissingAttributeError } from "../errors/missing-attribute-error";
import * as path from "path";
import * as Case from "case";
const KEYS = {
packageJson: "packageJson",
};
export default class TSMongooseSchemaRenderer extends URenderer {
private _classRenderer!: TSClassRenderer;
private _where?: (module: UModule, model: UModel) => boolean;
private _schemaDir = "src/schemas";
private _includeModuleInDir = true;
private _updatePackageJson = false;
constructor(options?: {
where?: (module: UModule, model: UModel) => boolean;
schemaDir?: string;
includeModuleInDir?: boolean;
updatePackageJson?: boolean;
}) {
super("ts@mongoose-schema");
if (options?.where) this._where = options.where;
if (options?.schemaDir) this._schemaDir = options.schemaDir;
if (options?.includeModuleInDir)
this._includeModuleInDir = options.includeModuleInDir;
if (options?.updatePackageJson)
this._updatePackageJson = options.updatePackageJson;
}
$schema(model: UModel): string | null {
return $attr(model, _schema()) ?? null;
}
$key(model: UModel) {
return this.$schemaName(model);
}
$keys(models: UModel[]) {
return models.map((model) => this.$key(model));
}
$schemaName(model: UModel) {
return Case.pascal(model.$name()) + "Schema";
}
$modelName(model: UModel) {
return Case.pascal(model.$name()) + "Model";
}
$fileName(model: UModel, extension = true) {
return `${Case.kebab(model.$name())}-schema${extension ? ".ts" : ""}`;
}
$fieldName(field: UField) {
return Case.camel(field.$name());
}
$fieldType(field: UField) {
let type = field.$type() + "";
if (type == "reference") return "mongoose.Schema.ObjectId as any";
if (["int", "float"].includes(type)) return "Number";
if (type !== "nested") return Case.pascal(type);
}
$fieldDeclaration(field: UField) {
const type = field.$type() + "";
let transformedType = this.$fieldType(field);
let enumModel: UModel | null = null;
if (type == "nested") {
const referencedModel = $attr(field, _ref());
if (referencedModel) {
let enumDefinition = $attr(referencedModel, _enum());
if (enumDefinition) {
transformedType = Case.pascal(
typeof Object.values(enumDefinition)[0]
);
enumModel = referencedModel;
} else transformedType = this.$schemaName(referencedModel);
}
}
let properties = !!$attr(field, _array())
? ` type: [${transformedType}],`
: ` type: ${transformedType},`;
if (enumModel) {
properties += ` enum: ${this._classRenderer.$className(enumModel)},`;
}
if (type == "reference") {
const referencedModel = $attr(field, _ref());
if (referencedModel) {
const schema = $attr(referencedModel, _schema());
if (!schema)
throw new MissingAttributeError(
referencedModel.$name(),
"model",
"_schema()"
);
properties += ` ref: () => ${this.$modelName(referencedModel)},`;
}
}
const addProperty = (key: string, value: any) => {
properties += ` ${key}: ${value},`;
};
if (!!$attr(field, _required())) addProperty("required", "true");
if (!!$attr(field, _unique())) addProperty("unique", "true");
if (!!$attr(field, _index())) addProperty("index", $attr(field, _index()));
if ($attr(field, _min()) !== null) addProperty("min", $attr(field, _min()));
if ($attr(field, _max()) !== null) addProperty("max", $attr(field, _max()));
if ($attr(field, _minLength()) !== null)
addProperty("minlength", $attr(field, _minLength()));
if ($attr(field, _maxLength()) !== null)
addProperty("maxlength", $attr(field, _maxLength()));
if (!!$attr(field, _in()))
addProperty("enum", JSON.stringify($attr(field, _in())));
if (!!$attr(field, _regex()))
addProperty("match", $attr(field, _regex())!.toString());
return ` ${this.$fieldName(field)}: {${properties.replace(/,$/, "")} },\n`;
}
async select(): Promise<RenderSelection> {
this._classRenderer = this.$draft().$requireRenderer<TSClassRenderer>(
this,
"ts@classes"
);
const models = this.$models(this._where).filter(
(model) =>
!!this.$schema(model) &&
!!this._classRenderer.$output(this._classRenderer.$key(model))
);
const extraSchemas: string[] = [];
models.forEach((model) => {
model.$fields().forEach((field) => {
if (["nested", "reference"].includes(field.$type() + "")) {
const referencedModel = $attr(field, _ref());
if (
referencedModel &&
!models.some((m) => m.$name() === referencedModel.$name()) &&
!extraSchemas.includes(referencedModel.$name()) &&
!$attr(referencedModel, _enum())
) {
models.push(referencedModel);
extraSchemas.push(referencedModel.$name());
}
}
});
});
const paths: RenderPath[] = [];
if (this._updatePackageJson)
paths.push({
key: KEYS.packageJson,
path: "package.json",
});
models.forEach((model) => {
if (paths.some((p) => p.key === this.$key(model))) return;
const mod = $attr(model, _rootModule());
paths.push({
key: this.$key(model),
path: path.join(
this._schemaDir,
this._includeModuleInDir ? Case.kebab(mod ? mod.$name() : "") : "",
this.$fileName(model)
),
});
});
return {
paths,
models,
};
}
async render(): Promise<RenderContent[]> {
const output: RenderContent[] = [];
if (this._updatePackageJson)
output.push({
key: KEYS.packageJson,
content: addPackageJsonDependency(
this.$content("packageJson")!.content,
[
{
name: "mongoose",
version: "^8.11.0",
},
]
),
});
const models = this.$selection().models || [];
models.forEach((model) => {
const modelKey = this._classRenderer.$key(model);
const schemaKey = this.$key(model);
const classPath = this._classRenderer.$path(modelKey);
const schemaPath = this.$path(schemaKey);
if (!schemaPath || !classPath) return;
const importCursor = "#import-cursor\n";
const fieldCursor = "#field-cursor\n";
const optionsCursor = "#options-cursor\n";
const textIndexCursor = "#text-index-cursor\n";
let content =
`import mongoose from "mongoose";\nimport { ${this._classRenderer.$className(
model
)} } from "${this.$resolveRelativePath(
schemaPath.path,
classPath.path
).replace(".ts", "")}";\n${importCursor}\n` +
`export const ${this.$schemaName(
model
)} = new mongoose.Schema<${this._classRenderer.$className(
model
)}>({\n${fieldCursor}}${optionsCursor});${textIndexCursor}`;
if ($attr(model, _schema()))
content += `\n\nexport const ${this.$modelName(
model
)} = mongoose.model<${this._classRenderer.$className(model)}>("${$attr(
model,
_schema()
)}", ${this.$schemaName(model)});`;
const fields = model.$fields();
const importedSchemas: string[] = [];
let disableNoExplicityAny = false;
const textIndexes: string[] = [];
const customTextIndexes = $attr(model, _textIndex());
if (customTextIndexes && Array.isArray(customTextIndexes))
textIndexes.push(...customTextIndexes);
fields.forEach((field) => {
if (field.$name() == "_id" || $attr(field, _virtual())) return;
const fieldDeclaration = this.$fieldDeclaration(field);
if (!!$attr(field, _textIndex())) textIndexes.push(field.$name());
if (["nested", "reference"].includes(field.$type() + "")) {
const referencedModel = $attr(field, _ref());
if (referencedModel) {
if (
this.$schemaName(model) != this.$schemaName(referencedModel) &&
!importedSchemas.includes(this.$schemaName(referencedModel))
) {
const isRefId = field.$type() == "reference";
const enumDefinition = $attr(referencedModel, _enum());
if (isRefId) disableNoExplicityAny = true;
content = writeToCursor(
importCursor,
`import { ${
isRefId
? this.$modelName(referencedModel)
: enumDefinition
? this._classRenderer.$className(referencedModel)
: this.$schemaName(referencedModel)
} } from "${this.$resolveRelativePath(
schemaPath.path,
(enumDefinition
? this._classRenderer.$path(
this._classRenderer.$key(referencedModel)
)
: this.$path(this.$key(referencedModel)))!.path
).replace(".ts", "")}";\n`,
content
);
importedSchemas.push(this.$schemaName(referencedModel));
}
}
}
content = writeToCursor(fieldCursor, fieldDeclaration, content);
});
if (!!$attr(model, _noId())) {
content = writeToCursor(optionsCursor, `,\n{ _id: false }\n`, content);
}
if (textIndexes.length > 0) {
content = writeToCursor(
textIndexCursor,
`\n\n${this.$schemaName(model)}.index({ ` +
textIndexes
.map((textIndex) => `"${textIndex}": "text"`)
.join(", ") +
`});`,
content
);
}
content = closeCursor(fieldCursor, content);
content = closeCursor(importCursor, content);
content = closeCursor(optionsCursor, content);
content = closeCursor(textIndexCursor, content);
if (disableNoExplicityAny) {
content =
"/* eslint-disable @typescript-eslint/no-explicit-any */\n\n" +
content;
}
output.push({
key: schemaKey,
content,
});
});
return output;
}
}