diff --git a/generators/base-application/generator.mts b/generators/base-application/generator.mts index 0e915cab23f3..e1ab9a0b2c42 100644 --- a/generators/base-application/generator.mts +++ b/generators/base-application/generator.mts @@ -322,9 +322,9 @@ export default class BaseApplicationGenerator< } /** - * @private + * @protected */ - getTaskFirstArgForPriority(priorityName): any { + protected getTaskFirstArgForPriority(priorityName): any { if ( ![ LOADING, diff --git a/generators/base-application/tasks.d.mts b/generators/base-application/tasks.d.mts index 2f26f343bdf6..634e3fd4c638 100644 --- a/generators/base-application/tasks.d.mts +++ b/generators/base-application/tasks.d.mts @@ -3,6 +3,7 @@ import { ControlTaskParam, BaseGeneratorDefinition, SourceTaskParam, GenericSour import { CommonClientServerApplication } from './types.mjs'; import { Entity, Field, Relationship } from './types/index.mjs'; import { ClientSourceType } from '../client/types.mjs'; +import { BaseChangelog } from '../base-entity-changes/types.js'; export type GenericApplicationDefinition = { applicationType: ApplicationType; @@ -66,7 +67,6 @@ export type BaseApplicationGeneratorDefinition< | 'loadingTaskParam' | 'preparingTaskParam' | 'defaultTaskParam' - | 'writingTaskParam' | 'postWritingTaskParam' | 'preConflictsTaskParam' | 'installTaskParam' @@ -74,8 +74,9 @@ export type BaseApplicationGeneratorDefinition< | 'endTaskParam', ApplicationTaskParam > & + Record<'writingTaskParam', ApplicationTaskParam & { configChanges?: Record }> & // Add entities to existing priorities - Record<'defaultTaskParam', EntitiesTaskParam> & + Record<'defaultTaskParam', EntitiesTaskParam & { entityChanges?: BaseChangelog[] }> & // Add application and control to new priorities Record< | 'configuringEachEntityTaskParam' @@ -96,6 +97,6 @@ export type BaseApplicationGeneratorDefinition< preparingEachEntityFieldTaskParam: PreparingEachEntityFieldTaskParam; preparingEachEntityRelationshipTaskParam: PreparingEachEntityRelationshipTaskParam; postPreparingEachEntityTaskParam: EachEntityTaskParam; - writingEntitiesTaskParam: EntitiesTaskParam; - postWritingEntitiesTaskParam: SourceTaskParam & EntitiesTaskParam; + writingEntitiesTaskParam: EntitiesTaskParam & { entityChanges?: BaseChangelog[] }; + postWritingEntitiesTaskParam: SourceTaskParam & EntitiesTaskParam & { entityChanges?: BaseChangelog[] }; }; diff --git a/generators/base-core/generator.mts b/generators/base-core/generator.mts index 67f12eaa63b1..66dc7e99254d 100644 --- a/generators/base-core/generator.mts +++ b/generators/base-core/generator.mts @@ -19,7 +19,7 @@ import { basename, join as joinPath, dirname, relative, isAbsolute, join, extname } from 'path'; import { createHash } from 'crypto'; import { fileURLToPath } from 'url'; -import { statSync, rmSync, existsSync } from 'fs'; +import { statSync, rmSync, existsSync, readFileSync } from 'fs'; import assert from 'assert'; import { requireNamespace } from '@yeoman/namespace'; import chalk from 'chalk'; @@ -51,7 +51,7 @@ import { CommonClientServerApplication, type BaseApplication } from '../base-app import { GENERATOR_BOOTSTRAP } from '../generator-list.mjs'; import NeedleApi from '../needle-api.mjs'; import command from '../base/command.mjs'; -import { GENERATOR_JHIPSTER } from '../generator-constants.mjs'; +import { GENERATOR_JHIPSTER, YO_RC_FILE } from '../generator-constants.mjs'; const { merge } = _; const { INITIALIZING, PROMPTING, CONFIGURING, COMPOSING, LOADING, PREPARING, DEFAULT, WRITING, POST_WRITING, INSTALL, POST_INSTALL, END } = @@ -225,6 +225,28 @@ export default class CoreGenerator extends YeomanGenerator + Array.isArray(newConfig[key]) + ? newConfig[key].length === oldConfig[key].length && + newConfig[key].find((element, index) => element !== oldConfig[key][index]) + : newConfig[key] !== oldConfig[key], + ) + .map(key => [key, { newValue: newConfig[key], oldValue: oldConfig[key] }]), + ); + return [{ control, configChanges }]; + } catch { + // Fail to parse + } + } + } return [{ control }]; } diff --git a/generators/base-entity-changes/generator.mts b/generators/base-entity-changes/generator.mts new file mode 100644 index 000000000000..e4460de30799 --- /dev/null +++ b/generators/base-entity-changes/generator.mts @@ -0,0 +1,178 @@ +/** + * Copyright 2013-2023 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 { existsSync, readFileSync } from 'fs'; +import GeneratorBaseApplication from '../base-application/index.mjs'; +import { PRIORITY_NAMES } from '../base-application/priorities.mjs'; +import { loadEntitiesAnnotations, loadEntitiesOtherSide } from '../base-application/support/index.mjs'; +import { relationshipEquals, relationshipNeedsForeignKeyRecreationOnly } from '../liquibase/support/index.mjs'; +import { addEntitiesOtherRelationships } from '../server/support/index.mjs'; +import type { BaseChangelog } from './types.js'; + +const { DEFAULT, WRITING_ENTITIES, POST_WRITING_ENTITIES } = PRIORITY_NAMES; + +const baseChangelog: () => Omit = () => ({ + newEntity: false, + changedEntity: false, + incremental: false, + previousEntity: undefined, + addedFields: [], + removedFields: [], + addedRelationships: [], + removedRelationships: [], + relationshipsToRecreateForeignKeysOnly: [], + changelogData: {}, +}); + +/** + * This is the base class for a generator for every generator. + */ +export default abstract class GeneratorBaseEntityChanges extends GeneratorBaseApplication { + recreateInitialChangelog!: boolean; + private entityChanges!: any[]; + + abstract isChangelogNew({ entityName, changelogDate }): boolean; + + protected getTaskFirstArgForPriority(priorityName): any { + const firstArg = super.getTaskFirstArgForPriority(priorityName); + if ([DEFAULT, WRITING_ENTITIES, POST_WRITING_ENTITIES].includes(priorityName)) { + this.entityChanges = this.generateIncrementalChanges(); + } + if ([DEFAULT].includes(priorityName)) { + return { ...firstArg, entityChanges: this.entityChanges }; + } + if ([WRITING_ENTITIES, POST_WRITING_ENTITIES].includes(priorityName)) { + // const { entities = [] } = this.options; + // const filteredEntities = data.entities.filter(entity => entities.includes(entity.name)); + return { ...firstArg, entityChanges: this.entityChanges }; + } + return firstArg; + } + + /** + * Generate changelog from differences between the liquibase entity and current entity. + */ + protected generateIncrementalChanges(): BaseChangelog[] { + const recreateInitialChangelog = this.recreateInitialChangelog; + const { generateBuiltInUserEntity, incrementalChangelog } = this.sharedData.getApplication(); + const entityNames = this.getExistingEntityNames(); + + const entitiesByName = Object.fromEntries(entityNames.map(entityName => [entityName, this.sharedData.getEntity(entityName)])); + const entitiesWithExistingChangelog = entityNames.filter( + entityName => !this.isChangelogNew({ entityName, changelogDate: entitiesByName[entityName].changelogDate }), + ); + const previousEntitiesByName = Object.fromEntries( + entityNames + .filter(entityName => existsSync(this.getEntityConfigPath(entityName))) + .map(entityName => [ + entityName, + { name: entityName, ...JSON.parse(readFileSync(this.getEntityConfigPath(entityName)).toString()) }, + ]), + ); + if (generateBuiltInUserEntity) { + const user = this.sharedData.getEntity('User'); + previousEntitiesByName.User = user; + } + + const entities: any[] = Object.values(previousEntitiesByName); + loadEntitiesAnnotations(entities); + loadEntitiesOtherSide(entities); + addEntitiesOtherRelationships(entities); + + // Compare entity changes and create changelogs + return entityNames.map(entityName => { + const newConfig: any = entitiesByName[entityName]; + const newFields: any[] = (newConfig.fields || []).filter((field: any) => !field.transient); + const newRelationships: any[] = newConfig.relationships || []; + + const oldConfig: any = previousEntitiesByName[entityName]; + + if (!oldConfig || recreateInitialChangelog || !incrementalChangelog || !entitiesWithExistingChangelog.includes(entityName)) { + return { + ...baseChangelog(), + incremental: newConfig.incrementalChangelog, + changelogDate: newConfig.changelogDate, + newEntity: true, + entity: newConfig, + entityName, + }; + } + + (this as any)._debug(`Calculating diffs for ${entityName}`); + + const oldFields: any[] = (oldConfig.fields || []).filter((field: any) => !field.transient); + const oldFieldNames: string[] = oldFields.filter(field => !field.id).map(field => field.fieldName); + const newFieldNames: string[] = newFields.filter(field => !field.id).map(field => field.fieldName); + + // Calculate new fields + const addedFieldNames = newFieldNames.filter(fieldName => !oldFieldNames.includes(fieldName)); + const addedFields = addedFieldNames.map(fieldName => newFields.find(field => fieldName === field.fieldName)); + // Calculate removed fields + const removedFieldNames = oldFieldNames.filter(fieldName => !newFieldNames.includes(fieldName)); + const removedFields = removedFieldNames.map(fieldName => oldFields.find(field => fieldName === field.fieldName)); + + const oldRelationships: any[] = oldConfig.relationships || []; + + // Calculate changed/newly added relationships + const addedRelationships = newRelationships.filter( + newRelationship => + // id changes are not supported + !newRelationship.id && + // check if the same relationship wasn't already part of the old config + !oldRelationships.some(oldRelationship => relationshipEquals(oldRelationship, newRelationship)), + ); + + // Calculate to be removed relationships + const removedRelationships = oldRelationships.filter( + oldRelationship => + // id changes are not supported + !oldRelationship.id && + // check if there are relationships not anymore in the new config + !newRelationships.some(newRelationship => relationshipEquals(newRelationship, oldRelationship)), + ); + + // calcualte relationships that only need a foreign key recreation from the ones that are added + // we need both the added and the removed ones here + const relationshipsToRecreateForeignKeysOnly = addedRelationships + .filter(addedRelationship => + removedRelationships.some(removedRelationship => + relationshipNeedsForeignKeyRecreationOnly(removedRelationship, addedRelationship), + ), + ) + .concat( + removedRelationships.filter(removedRelationship => + addedRelationships.some(addedRelationship => relationshipNeedsForeignKeyRecreationOnly(addedRelationship, removedRelationship)), + ), + ); + + return { + ...baseChangelog(), + previousEntity: oldConfig, + entity: newConfig, + incremental: true, + changedEntity: true, + entityName, + addedFields, + removedFields, + addedRelationships, + removedRelationships, + relationshipsToRecreateForeignKeysOnly, + }; + }); + } +} diff --git a/generators/liquibase-changelogs/index.mts b/generators/base-entity-changes/index.mts similarity index 86% rename from generators/liquibase-changelogs/index.mts rename to generators/base-entity-changes/index.mts index 7bc60a5eb3e3..c17317764df8 100644 --- a/generators/liquibase-changelogs/index.mts +++ b/generators/base-entity-changes/index.mts @@ -16,5 +16,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { default } from './generator.mjs'; -export { addEntityFiles, updateEntityFiles, updateConstraintsFiles, updateMigrateFiles, fakeFiles } from './files.mjs'; diff --git a/generators/base-entity-changes/types-export.d.ts b/generators/base-entity-changes/types-export.d.ts new file mode 100644 index 000000000000..2e9141dc69d7 --- /dev/null +++ b/generators/base-entity-changes/types-export.d.ts @@ -0,0 +1 @@ +export type { default } from './index.mjs'; diff --git a/generators/base-entity-changes/types.d.ts b/generators/base-entity-changes/types.d.ts new file mode 100644 index 000000000000..c33cc9f7ac1a --- /dev/null +++ b/generators/base-entity-changes/types.d.ts @@ -0,0 +1,18 @@ +export type BaseChangelog = { + newEntity: boolean; + changedEntity: boolean; + incremental: boolean; + + entityName: string; + entity: any; + + changelogDate?: string; + previousEntity?: any; + + addedFields: any[]; + removedFields: any[]; + addedRelationships: any[]; + removedRelationships: any[]; + relationshipsToRecreateForeignKeysOnly: any[]; + changelogData: any; +}; diff --git a/generators/entities/__snapshots__/generator.spec.mts.snap b/generators/entities/__snapshots__/generator.spec.mts.snap index 4280ca493ea0..d131186567c9 100644 --- a/generators/entities/__snapshots__/generator.spec.mts.snap +++ b/generators/entities/__snapshots__/generator.spec.mts.snap @@ -325,12 +325,15 @@ exports[`generator - entities regenerating all entities should match source call "addLiquibaseChangelog": [ { "changelogName": "20160926101210_added_entity_Foo", + "section": "base", }, { "changelogName": "20160926101211_added_entity_Bar", + "section": "base", }, { "changelogName": "20160926101212_added_entity_Skip", + "section": "base", }, ], } @@ -661,12 +664,15 @@ exports[`generator - entities regenerating selected entities with writeEveryEnti "addLiquibaseChangelog": [ { "changelogName": "20160926101210_added_entity_Foo", + "section": "base", }, { "changelogName": "20160926101211_added_entity_Bar", + "section": "base", }, { "changelogName": "20160926101212_added_entity_Skip", + "section": "base", }, ], } @@ -902,9 +908,11 @@ exports[`generator - entities regenerating some entities should match source cal "addLiquibaseChangelog": [ { "changelogName": "20160926101210_added_entity_Foo", + "section": "base", }, { "changelogName": "20160926101211_added_entity_Bar", + "section": "base", }, ], } diff --git a/generators/entity/__snapshots__/single-entity.spec.mts.snap b/generators/entity/__snapshots__/single-entity.spec.mts.snap index a50c56d08043..be4141a56a85 100644 --- a/generators/entity/__snapshots__/single-entity.spec.mts.snap +++ b/generators/entity/__snapshots__/single-entity.spec.mts.snap @@ -19,6 +19,7 @@ exports[`generator - entity --single-entity when regenerating with default confi "addLiquibaseChangelog": [ { "changelogName": "20160926101210_added_entity_Foo", + "section": "base", }, ], } diff --git a/generators/generate-blueprint/__snapshots__/generator.spec.mjs.snap b/generators/generate-blueprint/__snapshots__/generator.spec.mjs.snap index f5154cb63b3b..eb121c51add4 100644 --- a/generators/generate-blueprint/__snapshots__/generator.spec.mjs.snap +++ b/generators/generate-blueprint/__snapshots__/generator.spec.mjs.snap @@ -107,6 +107,21 @@ exports[`generator - generate-blueprint with all option should match snapshot 1` "generators/base-application/templates/template-file-base-application.ejs": { "stateCleared": "modified", }, + "generators/base-core/command.mjs": { + "stateCleared": "modified", + }, + "generators/base-core/generator.mjs": { + "stateCleared": "modified", + }, + "generators/base-core/generator.spec.mjs": { + "stateCleared": "modified", + }, + "generators/base-core/index.mjs": { + "stateCleared": "modified", + }, + "generators/base-core/templates/template-file-base-core.ejs": { + "stateCleared": "modified", + }, "generators/base-docker/command.mjs": { "stateCleared": "modified", }, @@ -122,6 +137,21 @@ exports[`generator - generate-blueprint with all option should match snapshot 1` "generators/base-docker/templates/template-file-base-docker.ejs": { "stateCleared": "modified", }, + "generators/base-entity-changes/command.mjs": { + "stateCleared": "modified", + }, + "generators/base-entity-changes/generator.mjs": { + "stateCleared": "modified", + }, + "generators/base-entity-changes/generator.spec.mjs": { + "stateCleared": "modified", + }, + "generators/base-entity-changes/index.mjs": { + "stateCleared": "modified", + }, + "generators/base-entity-changes/templates/template-file-base-entity-changes.ejs": { + "stateCleared": "modified", + }, "generators/base/command.mjs": { "stateCleared": "modified", }, @@ -584,21 +614,6 @@ exports[`generator - generate-blueprint with all option should match snapshot 1` "generators/languages/templates/template-file-languages.ejs": { "stateCleared": "modified", }, - "generators/liquibase-changelogs/command.mjs": { - "stateCleared": "modified", - }, - "generators/liquibase-changelogs/generator.mjs": { - "stateCleared": "modified", - }, - "generators/liquibase-changelogs/generator.spec.mjs": { - "stateCleared": "modified", - }, - "generators/liquibase-changelogs/index.mjs": { - "stateCleared": "modified", - }, - "generators/liquibase-changelogs/templates/template-file-liquibase-changelogs.ejs": { - "stateCleared": "modified", - }, "generators/liquibase/command.mjs": { "stateCleared": "modified", }, diff --git a/generators/generator-constants.mts b/generators/generator-constants.mts index 17a5b92fb4cd..10511845b64d 100644 --- a/generators/generator-constants.mts +++ b/generators/generator-constants.mts @@ -97,6 +97,7 @@ export const TEST_DIR = 'src/test/'; export const CLIENT_DIST_DIR = 'static/'; export const GENERATOR_JHIPSTER = 'generator-jhipster'; +export const YO_RC_FILE = '.yo-rc.json'; export const JHIPSTER_CONFIG_DIR = '.jhipster'; export const DOCKER_DIR = `${MAIN_DIR}docker/`; diff --git a/generators/generator-list.mjs b/generators/generator-list.mjs index f076d8647b46..1f0928a3ac2d 100644 --- a/generators/generator-list.mjs +++ b/generators/generator-list.mjs @@ -24,7 +24,9 @@ export const GENERATOR_AZURE_APP_SERVICE = 'azure-app-service'; export const GENERATOR_AZURE_SPRING_CLOUD = 'azure-spring-cloud'; export const GENERATOR_BASE = 'base'; export const GENERATOR_BASE_APPLICATION = 'base-application'; +export const GENERATOR_BASE_CORE = 'base-core'; export const GENERATOR_BASE_DOCKER = 'base-docker'; +export const GENERATOR_BASE_ENTITY_CHANGES = 'base-entity-changes'; export const GENERATOR_BOOTSTRAP = 'bootstrap'; export const GENERATOR_BOOTSTRAP_APPLICATION = 'bootstrap-application'; export const GENERATOR_BOOTSTRAP_APPLICATION_BASE = 'bootstrap-application-base'; @@ -56,7 +58,6 @@ export const GENERATOR_KUBERNETES_HELM = 'kubernetes-helm'; export const GENERATOR_KUBERNETES_KNATIVE = 'kubernetes-knative'; export const GENERATOR_LANGUAGES = 'languages'; export const GENERATOR_LIQUIBASE = 'liquibase'; -export const GENERATOR_LIQUIBASE_CHANGELOGS = 'liquibase-changelogs'; export const GENERATOR_MAVEN = 'maven'; export const GENERATOR_OPENAPI_CLIENT = 'openapi-client'; export const GENERATOR_OPENSHIFT = 'openshift'; diff --git a/generators/liquibase-changelogs/generator.mjs b/generators/liquibase-changelogs/generator.mjs deleted file mode 100644 index e6449e588d57..000000000000 --- a/generators/liquibase-changelogs/generator.mjs +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Copyright 2013-2023 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 assert from 'assert'; -import _ from 'lodash'; - -import BaseApplication from '../base-application/index.mjs'; -import { addEntityFiles, updateEntityFiles, updateConstraintsFiles, updateMigrateFiles, fakeFiles } from './files.mjs'; -import { fieldTypes } from '../../jdl/jhipster/index.mjs'; -import { GENERATOR_LIQUIBASE_CHANGELOGS, GENERATOR_BOOTSTRAP_APPLICATION } from '../generator-list.mjs'; -import { liquibaseComment } from './support/index.mjs'; - -const { CommonDBTypes } = fieldTypes; -const TYPE_LONG = CommonDBTypes.LONG; - -export default class DatabaseChangelogLiquibase extends BaseApplication { - constructor(args, options, features) { - super(args, options, { unique: undefined, ...features }); - - if (this.options.help) return; - - assert(this.options.databaseChangelog, 'Changelog is required'); - this.databaseChangelog = this.options.databaseChangelog; - if (!this.databaseChangelog.changelogDate) { - this.databaseChangelog.changelogDate = this.dateFormatForLiquibase(); - } - - // Set number of rows to be generated - this.numberOfRows = 10; - this.entityChanges = {}; - this.recreateInitialChangelog = this.options.recreateInitialChangelog; - } - - async beforeQueue() { - // TODO V8 switch to GENERATOR_BOOTSTRAP_APPLICATION_SERVER - await this.dependsOnJHipster(GENERATOR_BOOTSTRAP_APPLICATION); - if (!this.fromBlueprint) { - await this.composeWithBlueprints(GENERATOR_LIQUIBASE_CHANGELOGS); - } - } - - get preparing() { - return this.asPreparingTaskGroup({ - prepareEntityForTemplates({ application }) { - const databaseChangelog = this.databaseChangelog; - const entity = this.sharedData.getEntity(databaseChangelog.entityName); - if (!entity) { - throw new Error(`Shared entity ${databaseChangelog.entityName} was not found`); - } - this.entity = entity; - const entityChanges = this.entityChanges; - entityChanges.skipFakeData = application.skipFakeData || entity.skipFakeData; - - entityChanges.allFields = entity.fields.filter(field => !field.transient); - - if (databaseChangelog.type === 'entity-new') { - entityChanges.fields = entityChanges.allFields; - } else { - entityChanges.addedFields = databaseChangelog.addedFields.filter(field => !field.transient); - entityChanges.removedFields = databaseChangelog.removedFields.filter(field => !field.transient); - } - }, - - prepareFakeData() { - const entity = this.entity; - const entityChanges = this.entityChanges; - const seed = `${entity.entityClass}-liquibase`; - this.resetEntitiesFakeData(seed); - - const databaseChangelog = this.databaseChangelog; - entity.liquibaseFakeData = []; - - // fakeDataCount must be limited to the size of required unique relationships. - Object.defineProperty(entity, 'fakeDataCount', { - get: () => { - const uniqueRelationships = entity.relationships.filter(rel => rel.unique && (rel.relationshipRequired || rel.id)); - return _.min([entity.liquibaseFakeData.length, ...uniqueRelationships.map(rel => rel.otherEntity.fakeDataCount)]); - }, - configurable: true, - }); - - for (let rowNumber = 0; rowNumber < this.numberOfRows; rowNumber++) { - const rowData = {}; - const fields = - databaseChangelog.type === 'entity-new' - ? // generate id fields first to improve reproducibility - [...entityChanges.fields.filter(f => f.id), ...entityChanges.fields.filter(f => !f.id)] - : [...entityChanges.allFields.filter(f => f.id), ...entityChanges.addedFields.filter(f => !f.id)]; - fields.forEach(field => { - if (field.derived) { - Object.defineProperty(rowData, field.fieldName, { - get: () => { - if (!field.derivedEntity.liquibaseFakeData || rowNumber >= field.derivedEntity.liquibaseFakeData.length) { - return undefined; - } - return field.derivedEntity.liquibaseFakeData[rowNumber][field.fieldName]; - }, - }); - return; - } - let data; - if (field.id && field.fieldType === TYPE_LONG) { - data = rowNumber + 1; - } else { - data = field.generateFakeData(); - } - rowData[field.fieldName] = data; - }); - - entity.liquibaseFakeData.push(rowData); - } - }, - }); - } - - get [BaseApplication.PREPARING]() { - return this.delegateTasksToBlueprint(() => this.preparing); - } - - get default() { - return { - prepareRelationshipsForTemplates() { - const entityChanges = this.entityChanges; - const databaseChangelog = this.databaseChangelog; - const entity = this.entity; - if (databaseChangelog.type === 'entity-new') { - entityChanges.relationships = entity.relationships; - } else { - entityChanges.addedRelationships = databaseChangelog.addedRelationships; - entityChanges.removedRelationships = databaseChangelog.removedRelationships; - entityChanges.relationshipsToRecreateForeignKeysOnly = databaseChangelog.relationshipsToRecreateForeignKeysOnly; - } - }, - }; - } - - get [BaseApplication.DEFAULT]() { - return this.delegateTasksToBlueprint(() => this.default); - } - - // Public API method used by the getter and also by Blueprints - get writingEntities() { - return { - writeLiquibaseFiles({ application }) { - const entity = this.entity; - if (entity.skipServer) { - return {}; - } - const entityChanges = this.entityChanges; - const databaseChangelog = this.databaseChangelog; - - /* Required by the templates */ - const writeContext = { - entity, - databaseChangelog, - changelogDate: databaseChangelog.changelogDate, - databaseType: entity.databaseType, - prodDatabaseType: entity.prodDatabaseType, - authenticationType: entity.authenticationType, - jhiPrefix: entity.jhiPrefix, - reactive: application.reactive, - incrementalChangelog: application.incrementalChangelog, - recreateInitialChangelog: this.recreateInitialChangelog, - }; - - if (databaseChangelog.type === 'entity-new') { - return this._writeLiquibaseFiles(writeContext, entityChanges); - } - - entityChanges.requiresUpdateChangelogs = - entityChanges.addedFields.length > 0 || - entityChanges.removedFields.length > 0 || - entityChanges.addedRelationships.some( - relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable, - ) || - entityChanges.removedRelationships.some( - relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable, - ); - - if (entityChanges.requiresUpdateChangelogs) { - entityChanges.hasFieldConstraint = entityChanges.addedFields.some(field => field.unique || !field.nullable); - entityChanges.hasRelationshipConstraint = entityChanges.addedRelationships.some( - relationship => - (relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) && - (relationship.unique || !relationship.nullable), - ); - entityChanges.shouldWriteAnyRelationship = entityChanges.addedRelationships.some( - relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable, - ); - - return this._writeUpdateFiles(writeContext, entityChanges); - } - return undefined; - }, - }; - } - - get [BaseApplication.WRITING_ENTITIES]() { - if (this.options.skipWriting) { - return {}; - } - return this.delegateTasksToBlueprint(() => this.writingEntities); - } - - // Public API method used by the getter and also by Blueprints - get postWritingEntities() { - return this.asPostWritingTaskGroup({ - writeLiquibaseFiles({ source }) { - const entity = this.entity; - if (entity.skipServer) { - return {}; - } - const databaseChangelog = this.databaseChangelog; - const entityChanges = this.entityChanges; - - if (databaseChangelog.type === 'entity-new') { - return this._addLiquibaseFilesReferences({ entity, databaseChangelog, source }); - } - if (entityChanges.requiresUpdateChangelogs) { - return this._addUpdateFilesReferences({ entity, databaseChangelog, entityChanges, source }); - } - return undefined; - }, - }); - } - - get [BaseApplication.POST_WRITING_ENTITIES]() { - if (this.options.skipWriting) { - return {}; - } - return this.delegateTasksToBlueprint(() => this.postWritingEntities); - } - - /** - * Write files for new entities. - */ - _writeLiquibaseFiles(writeContext, entityChanges) { - const promises = []; - const context = { - ...writeContext, - skipFakeData: entityChanges.skipFakeData, - fields: entityChanges.allFields, - allFields: entityChanges.allFields, - relationships: entityChanges.relationships, - }; - // Write initial liquibase files - promises.push(this.writeFiles({ sections: addEntityFiles, context })); - if (!entityChanges.skipFakeData) { - promises.push(this.writeFiles({ sections: fakeFiles, context })); - } - - return Promise.all(promises); - } - - /** - * Write files for new entities. - */ - _addLiquibaseFilesReferences({ entity, databaseChangelog, source }) { - const fileName = `${databaseChangelog.changelogDate}_added_entity_${entity.entityClass}`; - if (entity.incremental) { - source.addLiquibaseIncrementalChangelog({ changelogName: fileName }); - } else { - source.addLiquibaseChangelog({ changelogName: fileName }); - } - - if (entity.anyRelationshipIsOwnerSide) { - const constFileName = `${databaseChangelog.changelogDate}_added_entity_constraints_${entity.entityClass}`; - if (entity.incremental) { - source.addLiquibaseIncrementalChangelog({ changelogName: constFileName }); - } else { - source.addLiquibaseConstraintsChangelog({ changelogName: constFileName }); - } - } - } - - /** - * Write files for updated entities. - */ - _writeUpdateFiles(writeContext, entityChanges) { - const { - addedFields, - allFields, - removedFields, - addedRelationships, - removedRelationships, - hasFieldConstraint, - hasRelationshipConstraint, - shouldWriteAnyRelationship, - relationshipsToRecreateForeignKeysOnly, - } = entityChanges; - - const context = { - ...writeContext, - skipFakeData: entityChanges.skipFakeData, - addedFields, - removedFields, - fields: addedFields, - allFields, - hasFieldConstraint, - addedRelationships, - removedRelationships, - relationships: addedRelationships, - hasRelationshipConstraint, - shouldWriteAnyRelationship, - relationshipsToRecreateForeignKeysOnly, - }; - - const promises = []; - promises.push(this.writeFiles({ sections: updateEntityFiles, context })); - - if (!entityChanges.skipFakeData && (entityChanges.addedFields.length > 0 || shouldWriteAnyRelationship)) { - promises.push(this.writeFiles({ sections: fakeFiles, context })); - promises.push(this.writeFiles({ sections: updateMigrateFiles, context })); - } - - if (hasFieldConstraint || shouldWriteAnyRelationship) { - promises.push(this.writeFiles({ sections: updateConstraintsFiles, context })); - } - return Promise.all(promises); - } - - /** - * Write files for updated entities. - */ - _addUpdateFilesReferences({ entity, databaseChangelog, entityChanges, source }) { - source.addLiquibaseIncrementalChangelog({ changelogName: `${databaseChangelog.changelogDate}_updated_entity_${entity.entityClass}` }); - - if (!entityChanges.skipFakeData && (entityChanges.addedFields.length > 0 || entityChanges.shouldWriteAnyRelationship)) { - source.addLiquibaseIncrementalChangelog({ - changelogName: `${databaseChangelog.changelogDate}_updated_entity_migrate_${entity.entityClass}`, - }); - } - - if (entityChanges.hasFieldConstraint || entityChanges.shouldWriteAnyRelationship) { - source.addLiquibaseIncrementalChangelog({ - changelogName: `${databaseChangelog.changelogDate}_updated_entity_constraints_${entity.entityClass}`, - }); - } - } - - /** - * @private - * Format As Liquibase Remarks - * - * @param {string} text - text to format - * @param {boolean} addRemarksTag - add remarks tag - * @returns formatted liquibase remarks - */ - formatAsLiquibaseRemarks(text, addRemarksTag = false) { - return liquibaseComment(text, addRemarksTag); - } -} diff --git a/generators/liquibase-changelogs/generator.spec.mjs b/generators/liquibase-changelogs/generator.spec.mjs deleted file mode 100644 index 503e6ed783ef..000000000000 --- a/generators/liquibase-changelogs/generator.spec.mjs +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2013-2023 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 { basename, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { expect } from 'esmocha'; -import lodash from 'lodash'; - -import { shouldSupportFeatures, testBlueprintSupport } from '../../test/support/tests.mjs'; -import Generator from './index.mjs'; - -const { snakeCase } = lodash; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const generator = basename(__dirname); - -describe(`generator - ${generator}`, () => { - it('generator-list constant matches folder name', async () => { - await expect((await import('../generator-list.mjs'))[`GENERATOR_${snakeCase(generator).toUpperCase()}`]).toBe(generator); - }); - shouldSupportFeatures(Generator); - describe.skip('blueprint support', () => testBlueprintSupport(generator)); -}); diff --git a/generators/liquibase-changelogs/support/index.mts b/generators/liquibase-changelogs/support/index.mts deleted file mode 100644 index aeeeb905a77b..000000000000 --- a/generators/liquibase-changelogs/support/index.mts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013-2023 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. - */ -// eslint-disable-next-line import/prefer-default-export -export { default as liquibaseComment } from './formatting.mjs'; diff --git a/generators/liquibase-changelogs/types-export.d.ts b/generators/liquibase-changelogs/types-export.d.ts deleted file mode 100644 index 91ec213d25dd..000000000000 --- a/generators/liquibase-changelogs/types-export.d.ts +++ /dev/null @@ -1 +0,0 @@ -export type { default } from '../base-application/types-export.js'; diff --git a/generators/liquibase-changelogs/__snapshots__/incremental-liquibase.spec.mts.snap b/generators/liquibase/__snapshots__/incremental-liquibase.spec.mts.snap similarity index 100% rename from generators/liquibase-changelogs/__snapshots__/incremental-liquibase.spec.mts.snap rename to generators/liquibase/__snapshots__/incremental-liquibase.spec.mts.snap diff --git a/generators/liquibase-changelogs/files.mjs b/generators/liquibase/changelog-files.mjs similarity index 100% rename from generators/liquibase-changelogs/files.mjs rename to generators/liquibase/changelog-files.mjs diff --git a/generators/liquibase/generator.mts b/generators/liquibase/generator.mts index efb7e9346273..f4994b4791a0 100644 --- a/generators/liquibase/generator.mts +++ b/generators/liquibase/generator.mts @@ -17,26 +17,24 @@ * limitations under the License. */ import fs from 'fs'; +import _ from 'lodash'; -import BaseApplicationGenerator from '../base-application/index.mjs'; -import { GENERATOR_LIQUIBASE, GENERATOR_LIQUIBASE_CHANGELOGS, GENERATOR_BOOTSTRAP_APPLICATION_SERVER } from '../generator-list.mjs'; +import BaseEntityChangesGenerator from '../base-entity-changes/index.mjs'; +import { GENERATOR_LIQUIBASE, GENERATOR_BOOTSTRAP_APPLICATION_SERVER } from '../generator-list.mjs'; import { liquibaseFiles } from './files.mjs'; import { prepareField as prepareFieldForLiquibase, postPrepareEntity, - relationshipEquals, - relationshipNeedsForeignKeyRecreationOnly, prepareRelationshipForLiquibase, + liquibaseComment, } from './support/index.mjs'; -import { addEntitiesOtherRelationships, prepareEntity as prepareEntityForServer } from '../server/support/index.mjs'; +import { prepareEntity as prepareEntityForServer } from '../server/support/index.mjs'; import { - loadEntitiesOtherSide, prepareEntityPrimaryKeyForTemplates, prepareRelationship, prepareField, prepareEntity, loadRequiredConfigIntoEntity, - loadEntitiesAnnotations, } from '../base-application/support/index.mjs'; import mavenPlugin from './support/maven-plugin.mjs'; import { @@ -45,16 +43,17 @@ import { addLiquibaseIncrementalChangelogCallback, } from './internal/needles.mjs'; import { prepareSqlApplicationProperties } from '../spring-data-relational/support/index.mjs'; +import { addEntityFiles, updateEntityFiles, updateConstraintsFiles, updateMigrateFiles, fakeFiles } from './changelog-files.mjs'; +import { fieldTypes } from '../../jdl/jhipster/index.mjs'; -const BASE_CHANGELOG = { - addedFields: [], - removedFields: [], - addedRelationships: [], - removedRelationships: [], - relationshipsToRecreateForeignKeysOnly: [], -}; -export default class LiquibaseGenerator extends BaseApplicationGenerator { +const { + CommonDBTypes: { LONG: TYPE_LONG }, +} = fieldTypes; + +export default class LiquibaseGenerator extends BaseEntityChangesGenerator { recreateInitialChangelog: boolean; + numberOfRows: number; + databaseChangelogs: any[] = []; constructor(args: any, options: any, features: any) { super(args, options, { skipParseOptions: false, ...features }); @@ -66,6 +65,7 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); this.recreateInitialChangelog = this.options.recreateInitialChangelog ?? false; + this.numberOfRows = 10; } async beforeQueue() { @@ -78,8 +78,7 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { get preparing() { return this.asPreparingTaskGroup({ preparing({ application }) { - application.liquibaseDefaultSchemaName = - application.databaseTypeSql && application.devDatabaseTypeMysql && application.baseName ? application.baseName : ''; + application.liquibaseDefaultSchemaName = ''; }, checkDatabaseCompatibility({ application }) { if (!application.databaseTypeSql && !application.databaseTypeNeo4j) { @@ -102,7 +101,7 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); } - get [BaseApplicationGenerator.PREPARING]() { + get [BaseEntityChangesGenerator.PREPARING]() { return this.delegateTasksToBlueprint(() => this.preparing); } @@ -116,7 +115,7 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); } - get [BaseApplicationGenerator.PREPARING_EACH_ENTITY_FIELD]() { + get [BaseEntityChangesGenerator.PREPARING_EACH_ENTITY_FIELD]() { return this.delegateTasksToBlueprint(() => this.preparingEachEntityField); } @@ -128,7 +127,7 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); } - get [BaseApplicationGenerator.PREPARING_EACH_ENTITY_RELATIONSHIP]() { + get [BaseEntityChangesGenerator.PREPARING_EACH_ENTITY_RELATIONSHIP]() { return this.delegateTasksToBlueprint(() => this.preparingEachEntityRelationship); } @@ -140,43 +139,90 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); } - get [BaseApplicationGenerator.POST_PREPARING_EACH_ENTITY]() { + get [BaseEntityChangesGenerator.POST_PREPARING_EACH_ENTITY]() { return this.delegateTasksToBlueprint(() => this.postPreparingEachEntity); } get default() { return this.asDefaultTaskGroup({ - async calculateChangelogs({ application, entities }) { - if (!application.databaseTypeSql || this.options.skipDbChangelog) { + async calculateChangelogs({ application, entities, entityChanges }) { + if (!application.databaseTypeSql || this.options.skipDbChangelog || !entityChanges) { return; } + + for (const databaseChangelog of entityChanges) { + if (!databaseChangelog.newEntity) { + // Previous entities are not prepared using default jhipster priorities. + // Prepare them. + const { previousEntity: entity } = databaseChangelog; + loadRequiredConfigIntoEntity(entity, this.jhipsterConfigWithDefaults); + prepareEntity(entity, this, application); + prepareEntityForServer(entity); + if (!entity.embedded && !entity.primaryKey) { + prepareEntityPrimaryKeyForTemplates(entity, this); + } + for (const field of entity.fields ?? []) { + prepareField(entity, field, this); + prepareFieldForLiquibase(entity, field); + } + for (const relationship of entity.relationships ?? []) { + prepareRelationship(entity, relationship, this, true); + prepareRelationshipForLiquibase(entity, relationship); + } + postPrepareEntity({ application, entity }); + } + } + const entitiesToWrite = this.options.entities ?? entities.filter(entity => !entity.builtIn && !entity.skipServer).map(entity => entity.name); - const diffs = this._generateChangelogFromFiles(application); + // Write only specified entities changelogs. + const changes = entityChanges.filter( + databaseChangelog => entitiesToWrite!.length === 0 || entitiesToWrite!.includes(databaseChangelog.entityName), + ); - for (const [fieldChanges] of diffs) { - if (fieldChanges.type === 'entity-new') { - await this._composeWithIncrementalChangelogProvider(entitiesToWrite, fieldChanges); - } - if (fieldChanges.addedFields.length > 0 || fieldChanges.removedFields.length > 0) { - await this._composeWithIncrementalChangelogProvider(entitiesToWrite, fieldChanges); + for (const databaseChangelog of changes) { + if (databaseChangelog.newEntity) { + this.databaseChangelogs.push(this.prepareChangelog({ databaseChangelog, application })); + } else if (databaseChangelog.addedFields.length > 0 || databaseChangelog.removedFields.length > 0) { + this.databaseChangelogs.push( + this.prepareChangelog({ + databaseChangelog: { + ...databaseChangelog, + fieldChangelog: true, + addedRelationships: [], + removedRelationships: [], + relationshipsToRecreateForeignKeysOnly: [], + }, + application, + }), + ); } } - // eslint-disable-next-line no-unused-vars - for (const [_fieldChanges, relationshipChanges] of diffs) { + // Relationships needs to be added later to make sure every related field is already added. + for (const databaseChangelog of changes) { if ( - relationshipChanges && - relationshipChanges.incremental && - (relationshipChanges.addedRelationships.length > 0 || relationshipChanges.removedRelationships.length > 0) + databaseChangelog.incremental && + (databaseChangelog.addedRelationships.length > 0 || databaseChangelog.removedRelationships.length > 0) ) { - await this._composeWithIncrementalChangelogProvider(entitiesToWrite, relationshipChanges); + this.databaseChangelogs.push( + this.prepareChangelog({ + databaseChangelog: { + ...databaseChangelog, + relationshipChangelog: true, + addedFields: [], + removedFields: [], + }, + application, + }), + ); } } + this.databaseChangelogs = this.databaseChangelogs.filter(Boolean); }, }); } - get [BaseApplicationGenerator.DEFAULT]() { + get [BaseEntityChangesGenerator.DEFAULT]() { return this.delegateTasksToBlueprint(() => this.default); } @@ -195,10 +241,22 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); } - get [BaseApplicationGenerator.WRITING]() { + get [BaseEntityChangesGenerator.WRITING]() { return this.delegateTasksToBlueprint(() => this.writing); } + get writingEntities() { + return this.asWritingEntitiesTaskGroup({ + writeChangelogs() { + return Promise.all(this.databaseChangelogs.map(databaseChangelog => this.writeChangelog({ databaseChangelog }))); + }, + }); + } + + get [BaseEntityChangesGenerator.WRITING_ENTITIES]() { + return this.delegateTasksToBlueprint(() => this.writingEntities); + } + get postWriting() { return this.asPostWritingTaskGroup({ customizeSpring({ source }) { @@ -331,150 +389,279 @@ export default class LiquibaseGenerator extends BaseApplicationGenerator { }); } - get [BaseApplicationGenerator.POST_WRITING]() { + get [BaseEntityChangesGenerator.POST_WRITING]() { return this.asPostWritingTaskGroup(this.delegateTasksToBlueprint(() => this.postWriting)); } + get postWritingEntities() { + return this.asPostWritingEntitiesTaskGroup({ + postWriteChangelogs({ source }) { + return Promise.all(this.databaseChangelogs.map(databaseChangelog => this.postWriteChangelog({ source, databaseChangelog }))); + }, + }); + } + + get [BaseEntityChangesGenerator.POST_WRITING_ENTITIES]() { + return this.delegateTasksToBlueprint(() => this.postWritingEntities); + } + /* ======================================================================== */ /* private methods use within generator */ /* ======================================================================== */ - _composeWithIncrementalChangelogProvider(entities: any[], databaseChangelog: any) { - const skipWriting = entities!.length !== 0 && !entities!.includes(databaseChangelog.entityName); - return this.composeWithJHipster(GENERATOR_LIQUIBASE_CHANGELOGS, { - generatorOptions: { - databaseChangelog, - skipWriting, - } as any, - }); + isChangelogNew({ entityName, changelogDate }) { + return !fs.existsSync( + this.destinationPath(`src/main/resources/config/liquibase/changelog/${changelogDate}_added_entity_${entityName}.xml`), + ); } /** - * Generate changelog from differences between the liquibase entity and current entity. + * Write files for new entities. */ - _generateChangelogFromFiles(application: any) { - const oldEntitiesConfig = Object.fromEntries( - this.getExistingEntityNames() - .filter(entityName => fs.existsSync(this.getEntityConfigPath(entityName))) - .map(entityName => [ - entityName, - { name: entityName, ...JSON.parse(fs.readFileSync(this.getEntityConfigPath(entityName)).toString()) }, - ]), - ); + _writeLiquibaseFiles({ context: writeContext, changelogData }) { + const promises: any[] = []; + const context = { + ...writeContext, + skipFakeData: changelogData.skipFakeData, + fields: changelogData.allFields, + allFields: changelogData.allFields, + relationships: changelogData.relationships, + }; + // Write initial liquibase files + promises.push(this.writeFiles({ sections: addEntityFiles, context })); + if (!changelogData.skipFakeData) { + promises.push(this.writeFiles({ sections: fakeFiles, context })); + } - if (application.generateBuiltInUserEntity) { - const user = this.sharedData.getEntity('User'); - oldEntitiesConfig.User = user; + return Promise.all(promises); + } + + /** + * Write files for new entities. + */ + _addLiquibaseFilesReferences({ entity, databaseChangelog, source }) { + const fileName = `${databaseChangelog.changelogDate}_added_entity_${entity.entityClass}`; + source.addLiquibaseChangelog({ changelogName: fileName, section: entity.incremental ? 'incremental' : 'base' }); + + if (entity.anyRelationshipIsOwnerSide) { + const constFileName = `${databaseChangelog.changelogDate}_added_entity_constraints_${entity.entityClass}`; + source.addLiquibaseChangelog({ changelogName: constFileName, section: entity.incremental ? 'incremental' : 'constraints' }); } + } - const entities = Object.values(oldEntitiesConfig); - loadEntitiesAnnotations(entities); - loadEntitiesOtherSide(entities); - addEntitiesOtherRelationships(entities); - - for (const entity of entities.filter(entity => !entity.skipServer && !entity.builtIn)) { - loadRequiredConfigIntoEntity(entity, this.jhipsterConfigWithDefaults); - prepareEntity(entity, this, application); - prepareEntityForServer(entity); - if (!entity.embedded && !entity.primaryKey) { - prepareEntityPrimaryKeyForTemplates(entity, this); - } - for (const field of entity.fields ?? []) { - prepareField(entity, field, this); - prepareFieldForLiquibase(entity, field); - } - for (const relationship of entity.relationships ?? []) { - prepareRelationship(entity, relationship, this, true); - prepareRelationshipForLiquibase(entity, relationship); - } - postPrepareEntity({ application, entity }); + /** + * Write files for updated entities. + */ + _writeUpdateFiles({ context: writeContext, changelogData }) { + const { + addedFields, + allFields, + removedFields, + addedRelationships, + removedRelationships, + hasFieldConstraint, + hasRelationshipConstraint, + shouldWriteAnyRelationship, + relationshipsToRecreateForeignKeysOnly, + } = changelogData; + + const context = { + ...writeContext, + skipFakeData: changelogData.skipFakeData, + addedFields, + removedFields, + fields: addedFields, + allFields, + hasFieldConstraint, + addedRelationships, + removedRelationships, + relationships: addedRelationships, + hasRelationshipConstraint, + shouldWriteAnyRelationship, + relationshipsToRecreateForeignKeysOnly, + }; + + const promises: Promise[] = []; + promises.push(this.writeFiles({ sections: updateEntityFiles, context })); + + if (!changelogData.skipFakeData && (changelogData.addedFields.length > 0 || shouldWriteAnyRelationship)) { + promises.push(this.writeFiles({ sections: fakeFiles, context })); + promises.push(this.writeFiles({ sections: updateMigrateFiles, context })); } - // Compare entity changes and create changelogs - return this.getExistingEntityNames().map(entityName => { - const newConfig: any = this.sharedData.getEntity(entityName); - const newFields: any[] = (newConfig.fields || []).filter((field: any) => !field.transient); - const newRelationships: any[] = newConfig.relationships || []; - - if ( - this.recreateInitialChangelog || - !application.incrementalChangelog || - !oldEntitiesConfig[entityName] || - !fs.existsSync( - this.destinationPath(`src/main/resources/config/liquibase/changelog/${newConfig.changelogDate}_added_entity_${entityName}.xml`), - ) - ) { - return [ - { - ...BASE_CHANGELOG, - incremental: newConfig.incrementalChangelog, - changelogDate: newConfig.changelogDate, - type: 'entity-new', - entityName, - }, - ]; - } - (this as any)._debug(`Calculating diffs for ${entityName}`); - - const oldConfig: any = oldEntitiesConfig[entityName]; - - const oldFields: any[] = (oldConfig.fields || []).filter((field: any) => !field.transient); - const oldFieldNames: string[] = oldFields.filter(field => !field.id).map(field => field.fieldName); - const newFieldNames: string[] = newFields.filter(field => !field.id).map(field => field.fieldName); - - // Calculate new fields - const addedFieldNames = newFieldNames.filter(fieldName => !oldFieldNames.includes(fieldName)); - const addedFields = addedFieldNames.map(fieldName => newFields.find(field => fieldName === field.fieldName)); - // Calculate removed fields - const removedFieldNames = oldFieldNames.filter(fieldName => !newFieldNames.includes(fieldName)); - const removedFields = removedFieldNames.map(fieldName => oldFields.find(field => fieldName === field.fieldName)); - - const oldRelationships: any[] = oldConfig.relationships || []; - - // Calculate changed/newly added relationships - const addedRelationships = newRelationships.filter( - newRelationship => - // id changes are not supported - !newRelationship.id && - // check if the same relationship wasn't already part of the old config - !oldRelationships.some(oldRelationship => relationshipEquals(oldRelationship, newRelationship)), - ); + if (hasFieldConstraint || shouldWriteAnyRelationship) { + promises.push(this.writeFiles({ sections: updateConstraintsFiles, context })); + } + return Promise.all(promises); + } - // Calculate to be removed relationships - const removedRelationships = oldRelationships.filter( - oldRelationship => - // id changes are not supported - !oldRelationship.id && - // check if there are relationships not anymore in the new config - !newRelationships.some(newRelationship => relationshipEquals(newRelationship, oldRelationship)), - ); + /** + * Write files for updated entities. + */ + _addUpdateFilesReferences({ entity, databaseChangelog, changelogData, source }) { + source.addLiquibaseIncrementalChangelog({ changelogName: `${databaseChangelog.changelogDate}_updated_entity_${entity.entityClass}` }); - // calcualte relationships that only need a foreign key recreation from the ones that are added - // we need both the added and the removed ones here - const relationshipsToRecreateForeignKeysOnly = addedRelationships - .filter(addedRelationship => - removedRelationships.some(removedRelationship => - relationshipNeedsForeignKeyRecreationOnly(removedRelationship, addedRelationship), - ), - ) - .concat( - removedRelationships.filter(removedRelationship => - addedRelationships.some(addedRelationship => relationshipNeedsForeignKeyRecreationOnly(addedRelationship, removedRelationship)), - ), - ); + if (!changelogData.skipFakeData && (changelogData.addedFields.length > 0 || changelogData.shouldWriteAnyRelationship)) { + source.addLiquibaseIncrementalChangelog({ + changelogName: `${databaseChangelog.changelogDate}_updated_entity_migrate_${entity.entityClass}`, + }); + } + + if (changelogData.hasFieldConstraint || changelogData.shouldWriteAnyRelationship) { + source.addLiquibaseIncrementalChangelog({ + changelogName: `${databaseChangelog.changelogDate}_updated_entity_constraints_${entity.entityClass}`, + }); + } + } + + /** + * @private + * Format As Liquibase Remarks + * + * @param {string} text - text to format + * @param {boolean} addRemarksTag - add remarks tag + * @returns formatted liquibase remarks + */ + formatAsLiquibaseRemarks(text, addRemarksTag = false) { + return liquibaseComment(text, addRemarksTag); + } + + prepareChangelog({ databaseChangelog, application }) { + if (!databaseChangelog.changelogDate) { + databaseChangelog.changelogDate = this.dateFormatForLiquibase(); + } + const entity = databaseChangelog.entity; + + if (entity.skipServer) { + return undefined; + } + + // eslint-disable-next-line no-nested-ternary + const entityChanges = databaseChangelog.changelogData; + entityChanges.skipFakeData = application.skipFakeData || entity.skipFakeData; + + entityChanges.allFields = entity.fields.filter(field => !field.transient); - return [ - { ...BASE_CHANGELOG, incremental: true, type: 'entity-update', entityName, addedFields, removedFields }, - { - ...BASE_CHANGELOG, - incremental: true, - type: 'entity-update', - entityName, - addedRelationships, - removedRelationships, - relationshipsToRecreateForeignKeysOnly, - }, - ]; + if (databaseChangelog.newEntity) { + entityChanges.fields = entityChanges.allFields; + } else { + entityChanges.addedFields = databaseChangelog.addedFields.filter(field => !field.transient); + entityChanges.removedFields = databaseChangelog.removedFields.filter(field => !field.transient); + } + + const seed = `${entity.entityClass}-liquibase`; + this.resetEntitiesFakeData(seed); + + entity.liquibaseFakeData = []; + + // fakeDataCount must be limited to the size of required unique relationships. + Object.defineProperty(entity, 'fakeDataCount', { + get: () => { + const uniqueRelationships = entity.relationships.filter(rel => rel.unique && (rel.relationshipRequired || rel.id)); + return _.min([entity.liquibaseFakeData.length, ...uniqueRelationships.map(rel => rel.otherEntity.fakeDataCount)]); + }, + configurable: true, }); + + for (let rowNumber = 0; rowNumber < this.numberOfRows; rowNumber++) { + const rowData = {}; + const fields = databaseChangelog.newEntity + ? // generate id fields first to improve reproducibility + [...entityChanges.fields.filter(f => f.id), ...entityChanges.fields.filter(f => !f.id)] + : [...entityChanges.allFields.filter(f => f.id), ...entityChanges.addedFields.filter(f => !f.id)]; + fields.forEach(field => { + if (field.derived) { + Object.defineProperty(rowData, field.fieldName, { + get: () => { + if (!field.derivedEntity.liquibaseFakeData || rowNumber >= field.derivedEntity.liquibaseFakeData.length) { + return undefined; + } + return field.derivedEntity.liquibaseFakeData[rowNumber][field.fieldName]; + }, + }); + return; + } + let data; + if (field.id && field.fieldType === TYPE_LONG) { + data = rowNumber + 1; + } else { + data = field.generateFakeData(); + } + rowData[field.fieldName] = data; + }); + + entity.liquibaseFakeData.push(rowData); + } + + if (databaseChangelog.newEntity) { + entityChanges.relationships = entity.relationships; + } else { + entityChanges.addedRelationships = databaseChangelog.addedRelationships; + entityChanges.removedRelationships = databaseChangelog.removedRelationships; + entityChanges.relationshipsToRecreateForeignKeysOnly = databaseChangelog.relationshipsToRecreateForeignKeysOnly; + } + + /* Required by the templates */ + databaseChangelog.writeContext = { + entity, + databaseChangelog, + changelogDate: databaseChangelog.changelogDate, + databaseType: entity.databaseType, + prodDatabaseType: entity.prodDatabaseType, + authenticationType: entity.authenticationType, + jhiPrefix: entity.jhiPrefix, + reactive: application.reactive, + incrementalChangelog: application.incrementalChangelog, + recreateInitialChangelog: this.recreateInitialChangelog, + }; + + if (databaseChangelog.newEntity) { + return databaseChangelog; + } + + entityChanges.requiresUpdateChangelogs = + entityChanges.addedFields.length > 0 || + entityChanges.removedFields.length > 0 || + entityChanges.addedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) || + entityChanges.removedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable); + + if (entityChanges.requiresUpdateChangelogs) { + entityChanges.hasFieldConstraint = entityChanges.addedFields.some(field => field.unique || !field.nullable); + entityChanges.hasRelationshipConstraint = entityChanges.addedRelationships.some( + relationship => + (relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) && (relationship.unique || !relationship.nullable), + ); + entityChanges.shouldWriteAnyRelationship = entityChanges.addedRelationships.some( + relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable, + ); + } + + return databaseChangelog; + } + + writeChangelog({ databaseChangelog }) { + const { writeContext: context, changelogData } = databaseChangelog; + if (databaseChangelog.newEntity) { + return this._writeLiquibaseFiles({ context, changelogData }); + } + if (changelogData.requiresUpdateChangelogs) { + return this._writeUpdateFiles({ context, changelogData }); + } + return undefined; + } + + postWriteChangelog({ databaseChangelog, source }) { + const { entity, changelogData } = databaseChangelog; + if (entity.skipServer) { + return undefined; + } + + if (databaseChangelog.newEntity) { + return this._addLiquibaseFilesReferences({ entity, databaseChangelog, source }); + } + if (changelogData.requiresUpdateChangelogs) { + return this._addUpdateFilesReferences({ entity, databaseChangelog, changelogData, source }); + } + return undefined; } } diff --git a/generators/liquibase-changelogs/incremental-liquibase.spec.mts b/generators/liquibase/incremental-liquibase.spec.mts similarity index 100% rename from generators/liquibase-changelogs/incremental-liquibase.spec.mts rename to generators/liquibase/incremental-liquibase.spec.mts diff --git a/generators/liquibase/internal/needles.mts b/generators/liquibase/internal/needles.mts index dd58921657b3..95001386be98 100644 --- a/generators/liquibase/internal/needles.mts +++ b/generators/liquibase/internal/needles.mts @@ -18,7 +18,13 @@ */ import { createNeedleCallback } from '../../base/support/needles.mjs'; -import { LiquibaseChangelog } from '../types.mjs'; +import { LiquibaseChangelog, LiquibaseChangelogSection } from '../types.mjs'; + +const changelogType = { + base: 'liquibase-add-changelog', + incremental: 'liquibase-add-incremental-changelog', + constraints: 'liquibase-add-constraints-changelog', +}; const addLiquibaseChangelogToMasterCallback = ({ changelogName, needle }: LiquibaseChangelog & { needle: string }) => createNeedleCallback({ @@ -26,11 +32,11 @@ const addLiquibaseChangelogToMasterCallback = ({ changelogName, needle }: Liquib contentToAdd: ``, }); -export const addLiquibaseChangelogCallback = ({ changelogName }: LiquibaseChangelog) => - addLiquibaseChangelogToMasterCallback({ needle: 'liquibase-add-changelog', changelogName }); +export const addLiquibaseChangelogCallback = ({ changelogName, section = 'base' }: LiquibaseChangelogSection) => + addLiquibaseChangelogToMasterCallback({ needle: changelogType[section], changelogName }); export const addLiquibaseIncrementalChangelogCallback = ({ changelogName }: LiquibaseChangelog) => - addLiquibaseChangelogToMasterCallback({ needle: 'liquibase-add-incremental-changelog', changelogName }); + addLiquibaseChangelogCallback({ changelogName, section: 'incremental' }); export const addLiquibaseConstraintsChangelogCallback = ({ changelogName }: LiquibaseChangelog) => - addLiquibaseChangelogToMasterCallback({ needle: 'liquibase-add-constraints-changelog', changelogName }); + addLiquibaseChangelogCallback({ changelogName, section: 'constraints' }); diff --git a/generators/liquibase-changelogs/support/formatting.mjs b/generators/liquibase/support/formatting.mjs similarity index 100% rename from generators/liquibase-changelogs/support/formatting.mjs rename to generators/liquibase/support/formatting.mjs diff --git a/generators/liquibase-changelogs/support/formatting.spec.mts b/generators/liquibase/support/formatting.spec.mts similarity index 96% rename from generators/liquibase-changelogs/support/formatting.spec.mts rename to generators/liquibase/support/formatting.spec.mts index cdfa9b85a2b9..05dabcc950f1 100644 --- a/generators/liquibase-changelogs/support/formatting.spec.mts +++ b/generators/liquibase/support/formatting.spec.mts @@ -1,7 +1,7 @@ import { expect } from 'esmocha'; import formatAsLiquibaseRemarks from './formatting.mjs'; -describe('generator - liquibase-changelogs - support - formatting', () => { +describe('generator - liquibase - support - formatting', () => { describe('formatAsLiquibaseRemarks', () => { describe('when formatting a nil text', () => { it('returns it', () => { diff --git a/generators/liquibase/support/index.mts b/generators/liquibase/support/index.mts index cd89c04dbe89..0f2a4c394158 100644 --- a/generators/liquibase/support/index.mts +++ b/generators/liquibase/support/index.mts @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// eslint-disable-next-line import/prefer-default-export +export { default as liquibaseComment } from './formatting.mjs'; export { default as postPrepareEntity } from './post-prepare-entity.mjs'; export { default as prepareField } from './prepare-field.mjs'; export * from './relationship.mjs'; diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/added_entity_constraints.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity_constraints.xml.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/added_entity_constraints.xml.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity_constraints.xml.ejs diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/updated_entity_migrate.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_migrate.xml.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/changelog/updated_entity_migrate.xml.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_migrate.xml.ejs diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.png b/generators/liquibase/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.png similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.png rename to generators/liquibase/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.png diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.txt.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.txt.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.txt.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/fake-data/blob/hipster.txt.ejs diff --git a/generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/fake-data/table_entity.csv.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/fake-data/table_entity.csv.ejs similarity index 100% rename from generators/liquibase-changelogs/templates/src/main/resources/config/liquibase/fake-data/table_entity.csv.ejs rename to generators/liquibase/templates/src/main/resources/config/liquibase/fake-data/table_entity.csv.ejs diff --git a/generators/liquibase/types.d.mts b/generators/liquibase/types.d.mts index de909a87a9d7..a9185450f8a7 100644 --- a/generators/liquibase/types.d.mts +++ b/generators/liquibase/types.d.mts @@ -1,9 +1,10 @@ import type { Entity } from '../base-application/index.mjs'; export type LiquibaseChangelog = { changelogName: string }; +export type LiquibaseChangelogSection = LiquibaseChangelog & { section?: 'base' | 'incremental' | 'constraints' }; export type LiquibaseSourceType = { - addLiquibaseChangelog?(changelog: LiquibaseChangelog): void; + addLiquibaseChangelog?(changelog: LiquibaseChangelogSection): void; addLiquibaseIncrementalChangelog?(changelog: LiquibaseChangelog): void; addLiquibaseConstraintsChangelog?(changelog: LiquibaseChangelog): void; }; diff --git a/test-integration/scripts/99-build-changes.sh b/test-integration/scripts/99-build-changes.sh index b057e6c37511..cbaba577dfaf 100755 --- a/test-integration/scripts/99-build-changes.sh +++ b/test-integration/scripts/99-build-changes.sh @@ -89,12 +89,12 @@ echo "::endgroup::" echo "::group::Check Server" git -c color.ui=always diff --exit-code @~1 -- \ + 'generators/base-entity-changes' \ 'generators/bootstrap-application-server' \ 'generators/cucumber' \ 'generators/gatling' \ 'generators/gradle' \ 'generators/liquibase' \ - 'generators/liquibase-changelogs' \ 'generators/maven' \ 'generators/server' \ 'generators/spring-cache' \ @@ -114,7 +114,6 @@ git -c color.ui=always diff --exit-code @~1 -- \ '.github/actions' \ '.github/workflows' \ 'generators/app' \ - 'generators/base-application' \ 'generators/bootstrap-application' \ 'generators/bootstrap-application-base' \ 'generators/common' \ @@ -131,6 +130,7 @@ echo "::endgroup::" echo "::group::Check Base" git -c color.ui=always diff --exit-code @~1 -- \ 'generators/base' \ + 'generators/base-core' \ 'generators/base-application' \ 'generators/bootstrap' \ 'generators/bootstrap-application-base' \ diff --git a/test/__snapshots__/api.spec.mjs.snap b/test/__snapshots__/api.spec.mjs.snap index 4fe038e4b5af..65bd08dd6643 100644 --- a/test/__snapshots__/api.spec.mjs.snap +++ b/test/__snapshots__/api.spec.mjs.snap @@ -9,7 +9,9 @@ exports[`public api generator-jhipster/generators should match snapshot 1`] = ` "GENERATOR_AZURE_SPRING_CLOUD": "azure-spring-cloud", "GENERATOR_BASE": "base", "GENERATOR_BASE_APPLICATION": "base-application", + "GENERATOR_BASE_CORE": "base-core", "GENERATOR_BASE_DOCKER": "base-docker", + "GENERATOR_BASE_ENTITY_CHANGES": "base-entity-changes", "GENERATOR_BOOTSTRAP": "bootstrap", "GENERATOR_BOOTSTRAP_APPLICATION": "bootstrap-application", "GENERATOR_BOOTSTRAP_APPLICATION_BASE": "bootstrap-application-base", @@ -41,7 +43,6 @@ exports[`public api generator-jhipster/generators should match snapshot 1`] = ` "GENERATOR_KUBERNETES_KNATIVE": "kubernetes-knative", "GENERATOR_LANGUAGES": "languages", "GENERATOR_LIQUIBASE": "liquibase", - "GENERATOR_LIQUIBASE_CHANGELOGS": "liquibase-changelogs", "GENERATOR_MAVEN": "maven", "GENERATOR_OPENAPI_CLIENT": "openapi-client", "GENERATOR_OPENSHIFT": "openshift",