gen-jhipster
Version:
VHipster - Spring Boot + Angular/React/Vue in one handy generator
160 lines (159 loc) • 6.89 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 * as _ from 'lodash-es';
import { asWritingEntitiesTask } from "../base-application/support/task-type-inference.js";
import { SERVER_MAIN_PROTO_DIR, SERVER_MAIN_SRC_DIR } from "../generator-constants.js";
import { moveToJavaPackageSrcDir, moveToSrcMainProtoDir } from "../java/support/index.js";
import { GRPC_ENTITY_SUPPORT_FILE_DEFS } from "./files.js";
const defaultEntities = ['User', 'Authority'];
const protoFiles = [
{
condition: (ctx) => ctx.entityClass !== 'User',
path: `${SERVER_MAIN_PROTO_DIR}`,
renameTo: moveToSrcMainProtoDir,
templates: ['entity/_entityUnderscoredName_.proto'],
},
{
condition: (ctx) => ctx.entityClass === 'User',
path: `${SERVER_MAIN_PROTO_DIR}`,
renameTo: moveToSrcMainProtoDir,
templates: ['entity/user_.proto'],
},
];
const javaFiles = [
{
condition: (ctx) => ctx.entityClass === 'Authority',
path: `${SERVER_MAIN_SRC_DIR}_package_`,
renameTo: moveToJavaPackageSrcDir,
templates: ['web/grpc/service/AuthorityGrpcService_.java', 'web/grpc/mapper/AuthorityProtoMapper_.java'],
},
{
condition: (ctx) => ctx.entityClass === 'User',
path: `${SERVER_MAIN_SRC_DIR}_package_`,
renameTo: moveToJavaPackageSrcDir,
templates: ['web/grpc/service/UserGrpcService_.java', 'web/grpc/mapper/UserProtoMapper_.java'],
},
{
condition: (ctx) => !defaultEntities.includes(ctx.entityClass ?? ''),
path: `${SERVER_MAIN_SRC_DIR}_package_`,
renameTo: moveToJavaPackageSrcDir,
templates: ['web/grpc/service/_entityClass_GrpcService.java', 'web/grpc/mapper/_entityClass_ProtoMapper.java'],
},
];
export const grpcFiles = {
protoFiles,
javaFiles,
};
export function cleanupEntitiesTask() { }
/**
* Break protobuf import cycles:
* 1) Self-import: never import the current entity's own .proto (e.g. otherEntityFileName was undefined).
* 2) Bidirectional embeds (A <-> B both use nested *Proto): on the lexicographically larger entity,
* emit a scalar FK field instead of embedding the other message so only one file imports the other.
*/
function applyGrpcProtoAcyclicRelationships(entities) {
const list = entities.filter(e => !e.skipServer);
for (const e of list) {
for (const r of e.relationships ?? []) {
delete r.grpcUseScalarFk;
delete r.grpcFkProtoType;
}
}
const relationshipEmbedsOtherProto = (rel) => {
if (rel.relationshipType === 'many-to-many' && rel.relationshipSide === 'left')
return true;
if (rel.relationshipType === 'many-to-one')
return true;
if (rel.relationshipType === 'one-to-one' && rel.ownerSide)
return true;
return false;
};
const entityKey = (ent) => _.snakeCase(ent.entityClass ?? ent.name).toLowerCase();
const otherKey = (rel) => {
if (String(rel.otherEntityName).toLowerCase() === 'user')
return 'user';
return _.snakeCase(rel.otherEntityName).toLowerCase();
};
const embeds = (fromKey, toKey) => {
const ent = list.find(e => entityKey(e) === fromKey);
if (!ent?.relationships)
return false;
return ent.relationships.some((r) => relationshipEmbedsOtherProto(r) && otherKey(r) === toKey);
};
const known = new Set(list.map(entityKey));
for (const entity of list) {
const ek = entityKey(entity);
for (const rel of entity.relationships ?? []) {
if (!relationshipEmbedsOtherProto(rel))
continue;
const ok = otherKey(rel);
if (ek === ok)
continue;
if (!known.has(ok))
continue;
if (embeds(ek, ok) && embeds(ok, ek) && ek > ok) {
rel.grpcUseScalarFk = true;
const other = rel.otherEntity;
const pk = other?.primaryKey;
let grpcFkProtoType = 'string';
if (pk?.typeLong || pk?.fields?.some(f => f.fieldType === 'Long')) {
grpcFkProtoType = 'int64';
}
else if (pk?.fields?.some(f => f.fieldType === 'Integer')) {
grpcFkProtoType = 'int32';
}
rel.grpcFkProtoType = grpcFkProtoType;
}
}
}
}
export default asWritingEntitiesTask(async function writeEntitiesTasks({ application, entities }) {
if (application.enableGrpc) {
applyGrpcProtoAcyclicRelationships(entities);
// Shared gRPC pieces (util/*.proto, ProtobufMapper, ProtoMapper, ProtoValidateUtils, error handlers) are normally
// written by writeGrpcFilesTask on initial app gen. If the project was created with enableGrpc:false and later
// enabled, entity regen must still emit these or *ProtoMapper and protoc will fail.
await this.writeFiles({
sections: { grpcEntitySupport: GRPC_ENTITY_SUPPORT_FILE_DEFS },
context: application,
});
for (const entity of entities.filter(e => !e.skipServer)) {
const entityWithClass = entity;
const instanceType = entityWithClass.dto === 'mapstruct' ? `${entityWithClass.entityClass}DTO` : entityWithClass.entityClass;
const instanceName = entityWithClass.dto === 'mapstruct' ? `${entityWithClass.entityInstance}DTO` : entityWithClass.entityInstance;
const entityUnderscoredName = _.snakeCase(entityWithClass.entityClass ?? '').toLowerCase();
let idProtoWrappedType;
let idProtoType;
idProtoType = 'string';
idProtoWrappedType = 'StringValue';
const newApplication = {
...application,
instanceType,
instanceName,
entityUnderscoredName,
idProtoType,
idProtoWrappedType,
};
await this.writeFiles({
sections: grpcFiles,
context: { ...newApplication, ...entity },
});
}
}
});