gen-jhipster
Version:
VHipster - Spring Boot + Angular/React/Vue in one handy generator
470 lines (469 loc) • 23 kB
JavaScript
/**
* Copyright 2013-2026 the original author or authors from the JHipster project.
*
* This file is part of the JHipster project, see https://www.jhipster.tech/
* for more information.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { intersection, snakeCase, sortedUniq, uniq, upperFirst } from 'lodash-es';
import { APPLICATION_TYPE_GATEWAY, APPLICATION_TYPE_MICROSERVICE } from "../../../lib/core/application-types.js";
import { binaryOptions } from "../../../lib/jdl/core/built-in-options/index.js";
import { databaseTypes, fieldTypes, searchEngineTypes } from "../../../lib/jhipster/index.js";
import { getMicroserviceAppName, mutateData, stringHashCode } from "../../../lib/utils/index.js";
import { parseChangelog } from "../../base/support/timestamp.js";
import { getTypescriptKeyType } from "../../client/support/index.js";
import { getDatabaseTypeData, hibernateSnakeCase } from "../../server/support/index.js";
import { mutateEntity } from "../entity.js";
import { createFaker } from "./faker.js";
import { fieldIsEnum } from "./field-utils.js";
const NO_SEARCH_ENGINE = searchEngineTypes.NO;
const { CommonDBTypes } = fieldTypes;
const { BOOLEAN, LONG, STRING, UUID, INTEGER } = CommonDBTypes;
const { CASSANDRA, COUCHBASE, NEO4J, SQL, MONGODB } = databaseTypes;
const { INSTANT, ZONED_DATE_TIME, DURATION, LOCAL_DATE, BIG_DECIMAL, LOCAL_TIME } = fieldTypes.CommonDBTypes;
const { BYTES, BYTE_BUFFER } = fieldTypes.RelationalOnlyDBTypes;
const { IMAGE, TEXT } = fieldTypes.BlobTypes;
const BASE_TEMPLATE_DATA = {
primaryKey: undefined,
entityPackage: undefined,
skipUiGrouping: false,
anyFieldHasDocumentation: false,
existingEnum: false,
searchEngine: NO_SEARCH_ENGINE,
microserviceName: undefined,
entityAuthority: undefined,
entityReadAuthority: undefined,
adminEntity: undefined,
builtInUserManagement: undefined,
requiresPersistableImplementation: false,
updatableEntity: undefined,
anyFieldIsDateDerived: false,
anyFieldIsTimeDerived: false,
anyFieldIsInstant: false,
anyFieldIsUUID: false,
anyFieldIsUUIDString: false,
anyFieldIsZonedDateTime: false,
anyFieldIsDuration: false,
anyFieldIsLocalDate: false,
anyFieldIsBigDecimal: false,
anyFieldIsBlobDerived: false,
anyFieldHasImageContentType: false,
anyFieldHasTextContentType: false,
anyFieldHasFileBasedContentType: false,
anyPropertyHasValidation: false,
fieldsContainNoOwnerOneToOne: false,
get otherRelationships() {
return [];
},
get enums() {
return [];
},
// these variable hold field and relationship names for question options during update
get fieldNameChoices() {
return [];
},
get differentRelationships() {
return {};
},
};
export const entityDefaultConfig = {
pagination: binaryOptions.DefaultValues[binaryOptions.Options.PAGINATION],
anyPropertyHasValidation: undefined,
dto: binaryOptions.DefaultValues[binaryOptions.Options.DTO],
service: binaryOptions.DefaultValues[binaryOptions.Options.SERVICE],
jpaMetamodelFiltering: false,
readOnly: false,
embedded: false,
fluentMethods: true,
get fields() {
return [];
},
get relationships() {
return [];
},
};
export default function prepareEntity(entityWithConfig, generator) {
const entityName = upperFirst(entityWithConfig.name);
mutateData(entityWithConfig, entityDefaultConfig, BASE_TEMPLATE_DATA);
if (entityWithConfig.changelogDate) {
try {
entityWithConfig.changelogDateForRecent = parseChangelog(String(entityWithConfig.changelogDate));
}
catch (error) {
throw new Error(`Error parsing changelog date for entity ${entityName}: ${error.message}`, { cause: error });
}
}
entityWithConfig.useMicroserviceJson = entityWithConfig.useMicroserviceJson || entityWithConfig.microserviceName !== undefined;
entityWithConfig.microserviceAppName = '';
if (generator.jhipsterConfig.applicationType === APPLICATION_TYPE_GATEWAY && entityWithConfig.useMicroserviceJson) {
if (!entityWithConfig.microserviceName) {
throw new Error('Microservice name for the entity is not found. Entity cannot be generated!');
}
entityWithConfig.microserviceAppName = getMicroserviceAppName({ microserviceName: entityWithConfig.microserviceName });
entityWithConfig.skipServer = true;
}
mutateData(entityWithConfig, mutateEntity);
mutateData(entityWithConfig, {
__override__: false,
hasRelationshipWithBuiltInUser: ({ relationships }) => relationships.some(relationship => relationship.otherEntity.builtInUser),
});
entityWithConfig.generateFakeData = type => {
const fieldsToGenerate = type === 'cypress' ? entityWithConfig.fields.filter(field => !field.id || !field.autoGenerate) : entityWithConfig.fields;
const fieldEntries = fieldsToGenerate
.map(field => {
const fieldData = field.generateFakeData(type);
if (!field.nullable && fieldData === null)
return undefined;
return [field.fieldName, fieldData];
})
.filter(Boolean);
const withError = fieldEntries.find(entry => !entry);
if (withError) {
generator.log.warn(`Error generating a full sample for entity ${entityName}`);
return undefined;
}
return Object.fromEntries(fieldEntries);
};
return entityWithConfig;
}
export function derivedPrimaryKeyProperties(primaryKey) {
mutateData(primaryKey, {
hasUUID: primaryKey.fields?.some(field => field.fieldType === UUID),
hasLong: primaryKey.fields?.some(field => field.fieldType === LONG),
hasInteger: primaryKey.fields?.some(field => field.fieldType === INTEGER),
typeUUID: primaryKey.type === UUID,
typeString: primaryKey.type === STRING,
typeLong: primaryKey.type === LONG,
typeInteger: primaryKey.type === INTEGER,
typeNumeric: !primaryKey.composite && primaryKey.fields[0].fieldTypeNumeric,
});
}
export function prepareEntityPrimaryKeyForTemplates({ entity: entityWithConfig, enableCompositeId = true, application, }) {
const idFields = entityWithConfig.fields.filter(field => field.id);
const idRelationships = entityWithConfig.relationships.filter(relationship => relationship.id);
let idCount = idFields.length + idRelationships.length;
if (idCount === 0) {
let idField = entityWithConfig.fields.find(field => field.fieldName === 'id');
if (idField) {
idField.id = true;
idField.autoGenerate = idField.autoGenerate ?? true;
}
else {
if (entityWithConfig.microserviceName && !application?.microfrontend) {
this?.log.warn("Microservice entities should have the id field type specified (e.g., id String) to make sure gateway and microservice types don't conflict");
}
idField = {
fieldName: 'id',
id: true,
fieldNameHumanized: 'ID',
propertyTranslationKey: 'global.field.id',
autoGenerate: true,
};
entityWithConfig.fields.unshift(idField);
}
idFields.push(idField);
idCount++;
}
else if (idRelationships.length > 0) {
idRelationships.forEach(relationship => {
// relationships id data are not available at this point, so calculate it when needed.
relationship.derivedPrimaryKey = {
get derivedFields() {
return relationship.otherEntity.primaryKey.fields.map(field => ({
originalField: field,
...field,
derived: true,
derivedEntity: relationship.otherEntity,
jpaGeneratedValue: false,
liquibaseAutoIncrement: false,
// Mapsid is generated by relationship select
autoGenerate: true,
readonly: true,
get derivedPath() {
if (field.derivedPath) {
if (relationship.otherEntity.primaryKey.derived) {
return [relationship.relationshipName, ...field.derivedPath.splice(1)];
}
return [relationship.relationshipName, ...field.derivedPath];
}
return [relationship.relationshipName, field.fieldName];
},
get path() {
return [relationship.relationshipName, ...field.path];
},
get fieldName() {
return idCount === 1 ? field.fieldName : `${relationship.relationshipName}${field.fieldNameCapitalized}`;
},
get fieldNameCapitalized() {
return idCount === 1
? field.fieldNameCapitalized
: `${relationship.relationshipNameCapitalized}${field.fieldNameCapitalized}`;
},
get columnName() {
return idCount === 1
? field.columnName
: `${hibernateSnakeCase(relationship.relationshipName)}_${field.columnName}`;
},
}));
},
};
});
}
if (idCount === 1 && idRelationships.length === 1) {
const relationshipId = idRelationships[0];
// One-To-One relationships with id uses @MapsId.
// Almost every info is taken from the parent, except some info like autoGenerate and derived.
// calling fieldName as id is for backward compatibility, in the future we may want to prefix it with relationship name.
entityWithConfig.primaryKey = {
// fieldName: 'id',
derived: true,
// MapsId copy the id from the relationship.
autoGenerate: true,
get fields() {
return this.derivedFields;
},
get derivedFields() {
return relationshipId.derivedPrimaryKey.derivedFields;
},
get ownFields() {
return relationshipId.otherEntity.primaryKey.ownFields;
},
relationships: idRelationships,
get name() {
return relationshipId.otherEntity.primaryKey.name;
},
get hibernateSnakeCaseName() {
return hibernateSnakeCase(relationshipId.otherEntity.primaryKey.name);
},
get nameCapitalized() {
return relationshipId.otherEntity.primaryKey.nameCapitalized;
},
get type() {
return relationshipId.otherEntity.primaryKey.type;
},
get tsType() {
return relationshipId.otherEntity.primaryKey.tsType;
},
get composite() {
return relationshipId.otherEntity.primaryKey.composite;
},
get ids() {
return this.fields.map(field => fieldToId(field));
},
};
}
else {
const composite = enableCompositeId ? idCount > 1 : false;
let primaryKeyName;
let primaryKeyType;
if (composite) {
primaryKeyName = 'id';
primaryKeyType = `${entityWithConfig.entityClass}Id`;
}
else {
const idField = idFields[0];
// Allow ids type to be empty and fallback to default type for the database.
if (!idField.fieldType) {
idField.fieldType = application?.pkType ?? getDatabaseTypeData(entityWithConfig.databaseType).defaultPrimaryKeyType;
}
primaryKeyName = idField.fieldName;
primaryKeyType = idField.fieldType;
}
entityWithConfig.primaryKey = {
derived: false,
name: primaryKeyName,
hibernateSnakeCaseName: hibernateSnakeCase(primaryKeyName),
nameCapitalized: upperFirst(primaryKeyName),
type: primaryKeyType,
tsType: getTypescriptKeyType(primaryKeyType),
composite,
relationships: idRelationships,
// Fields declared in this entity
ownFields: idFields,
// Fields declared and inherited
get fields() {
return [...this.ownFields, ...this.derivedFields];
},
get autoGenerate() {
return this.composite ? false : this.fields[0].autoGenerate;
},
// Fields inherited from id relationships.
get derivedFields() {
return this.relationships.flatMap(rel => rel.derivedPrimaryKey.derivedFields);
},
get ids() {
return this.fields.map(field => fieldToId(field));
},
};
}
return entityWithConfig;
}
function fieldToId(field) {
return {
field,
get name() {
return field.fieldName;
},
get nameCapitalized() {
return field.fieldNameCapitalized;
},
get nameDotted() {
return field.derivedPath ? field.derivedPath.join('.') : field.fieldName;
},
get nameDottedAsserted() {
return field.derivedPath ? `${field.derivedPath.join('!.')}!` : `${field.fieldName}!`;
},
get setter() {
return `set${this.nameCapitalized}`;
},
get getter() {
return (field.fieldType === BOOLEAN ? 'is' : 'get') + this.nameCapitalized;
},
get autoGenerate() {
return !!field.autoGenerate;
},
};
}
/**
* Copy required application config into entity.
* Some entity features are related to the backend instead of the current app.
* This allows to entities files based on the backend features.
*/
export function loadRequiredConfigIntoEntity(entity, config) {
mutateData(entity, {
__override__: false,
// applicationType: config.applicationType,
// baseName: config.baseName,
// authenticationType: config.authenticationType,
reactive: config.reactive,
microfrontend: config.microfrontend,
// Workaround different paths
clientFramework: config.clientFramework,
databaseType: config.databaseType,
prodDatabaseType: config.prodDatabaseType,
searchEngine: config.searchEngine,
// jhiPrefix: config.jhiPrefix,
// entitySuffix: config.entitySuffix,
// dtoSuffix: config.dtoSuffix,
// packageName: config.packageName,
microserviceName: ({ builtIn }) => (!builtIn && config.applicationType === APPLICATION_TYPE_MICROSERVICE ? config.baseName : undefined),
});
if (entity.searchEngine === true && (!entity.microserviceName || entity.microserviceName === config.baseName)) {
// If the entity belongs to this application and searchEngine is true.
if (config.searchEngine && config.searchEngine !== NO_SEARCH_ENGINE) {
// Replace with the searchEngine from the application.
entity.searchEngine = config.searchEngine;
}
else {
entity.searchEngine = NO_SEARCH_ENGINE;
this?.log.warn('Search engine is enabled at entity level, but disabled at application level. Search engine will be disabled');
}
}
return entity;
}
export function preparePostEntityCommonDerivedProperties(entity) {
const { fields } = entity;
const fieldsType = sortedUniq(fields.map(({ fieldType }) => fieldType).filter(fieldType => !fieldIsEnum(fieldType)));
// TODO move to server generator
entity.anyFieldHasDocumentation = entity.fields.some(({ documentation }) => documentation);
entity.anyFieldIsZonedDateTime = fieldsType.includes(ZONED_DATE_TIME);
entity.anyFieldIsInstant = fieldsType.includes(INSTANT);
entity.anyFieldIsLocalTime = fieldsType.includes(LOCAL_TIME);
entity.anyFieldIsDuration = fieldsType.includes(DURATION);
entity.anyFieldIsLocalDate = fieldsType.includes(LOCAL_DATE);
entity.anyFieldIsBigDecimal = fieldsType.includes(BIG_DECIMAL);
entity.anyFieldIsUUID = fieldsType.includes(UUID);
entity.anyFieldIsUUIDString = entity.anyFieldIsUUID;
entity.entityUnderscoredName = snakeCase(entity.entityClass ?? entity.name).toLowerCase();
entity.anyFieldIsTimeDerived = entity.anyFieldIsZonedDateTime || entity.anyFieldIsInstant;
entity.anyFieldIsDateDerived = entity.anyFieldIsTimeDerived || entity.anyFieldIsLocalDate;
entity.anyFieldIsBlobDerived = intersection(fieldsType, [BYTES, BYTE_BUFFER]).length > 0;
if (entity.anyFieldIsBlobDerived) {
const blobFields = fields.filter(({ fieldType }) => [BYTES, BYTE_BUFFER].includes(fieldType));
const blobFieldsContentType = sortedUniq(blobFields.map(({ fieldTypeBlobContent }) => fieldTypeBlobContent));
entity.anyFieldHasImageContentType = blobFieldsContentType.includes(IMAGE);
entity.anyFieldHasFileBasedContentType = blobFieldsContentType.some(fieldTypeBlobContent => fieldTypeBlobContent !== TEXT);
entity.anyFieldHasTextContentType = blobFieldsContentType.includes(TEXT);
}
preparePostEntityCommonDerivedPropertiesNotTyped(entity);
}
function preparePostEntityCommonDerivedPropertiesNotTyped(entity) {
const { relationships, fields } = entity;
const oneToOneRelationships = relationships.filter(({ relationshipType }) => relationshipType === 'one-to-one');
entity.fieldsContainNoOwnerOneToOne = oneToOneRelationships.some(({ ownerSide }) => !ownerSide);
entity.anyPropertyHasValidation =
entity.anyPropertyHasValidation || relationships.some(({ relationshipValidate }) => relationshipValidate);
const relationshipsByOtherEntity = relationships
.map(relationship => [relationship.otherEntity.entityNameCapitalized, relationship])
.reduce((relationshipsByOtherEntity, [type, relationship]) => {
if (!relationshipsByOtherEntity[type]) {
relationshipsByOtherEntity[type] = [relationship];
}
else {
relationshipsByOtherEntity[type].push(relationship);
}
return relationshipsByOtherEntity;
}, {});
entity.relationshipsByOtherEntity = relationshipsByOtherEntity;
entity.differentRelationships = relationshipsByOtherEntity;
entity.anyPropertyHasValidation = entity.anyPropertyHasValidation || fields.some(({ fieldValidate }) => fieldValidate);
entity.otherEntities = uniq(entity.relationships.map(rel => rel.otherEntity));
entity.persistableRelationships = relationships.filter(({ persistableRelationship }) => persistableRelationship);
entity.otherEntitiesWithPersistableRelationship = uniq(entity.persistableRelationships.map(rel => rel.otherEntity));
entity.updatableEntity =
entity.fields.some(field => !field.id && !field.transient) ||
entity.relationships.some(relationship => !relationship.id && relationship.persistableRelationship);
entity.entityContainsCollectionField = entity.relationships.some(relationship => relationship.collection);
if (entity.primaryKey) {
derivedPrimaryKeyProperties(entity.primaryKey);
entity.requiresPersistableImplementation =
entity.requiresPersistableImplementation || entity.fields.some(field => field.requiresPersistableImplementation);
}
const types = entity.relationships
.filter(rel => rel.otherEntity.primaryKey)
.flatMap(rel => rel.otherEntity.primaryKey.fields.map(f => f.fieldType));
entity.otherEntityPrimaryKeyTypes = Array.from(new Set(types));
entity.otherEntityPrimaryKeyTypesIncludesUUID = types.includes(UUID);
entity.relationships.forEach(relationship => {
if (!relationship.otherEntity.primaryKey) {
relationship.bagRelationship = false;
relationship.relationshipEagerLoad = false;
return;
}
mutateData(relationship, {
bagRelationship: relationship.ownerSide && relationship.collection,
relationshipEagerLoad: ({ relationshipEagerLoad, bagRelationship, ownerSide, otherEntity, otherEntityField }) => relationshipEagerLoad ??
(bagRelationship ||
entity.eagerLoad ||
// Fetch relationships if otherEntityField differs otherwise the id is enough
(ownerSide && otherEntity.primaryKey.name !== otherEntityField)),
});
});
entity.relationshipsContainEagerLoad = entity.relationships.some(relationship => relationship.relationshipEagerLoad);
entity.containsBagRelationships = entity.relationships.some(relationship => relationship.bagRelationship);
entity.implementsEagerLoadApis = // Cassandra doesn't provides *WithEagerRelationships apis
![CASSANDRA, COUCHBASE, NEO4J].includes(entity.databaseType) &&
// Only sql and mongodb provides *WithEagerRelationships apis for imperative implementation
(entity.reactive || [SQL, MONGODB].includes(entity.databaseType)) &&
entity.relationshipsContainEagerLoad;
entity.eagerRelations = entity.relationships.filter(rel => rel.relationshipEagerLoad);
entity.regularEagerRelations = entity.eagerRelations.filter(rel => rel.id !== true);
entity.reactiveEagerRelations = entity.relationships.filter(rel => rel.relationshipType === 'many-to-one' || (rel.relationshipType === 'one-to-one' && rel.ownerSide === true));
entity.reactiveRegularEagerRelations = entity.reactiveEagerRelations.filter(rel => rel.id !== true);
}
export async function addFakerToEntity(entityWithConfig, nativeLanguage = 'en') {
entityWithConfig.faker = entityWithConfig.faker || (await createFaker(nativeLanguage));
entityWithConfig.resetFakerSeed = (suffix = '') => entityWithConfig.faker.seed(stringHashCode(entityWithConfig.name.toLowerCase() + suffix));
entityWithConfig.resetFakerSeed();
}