UNPKG

gen-jhipster

Version:

VHipster - Spring Boot + Angular/React/Vue in one handy generator

470 lines (469 loc) 23 kB
/** * 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(); }