@mshima/generator-jhipster-nodejs 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/generators/node-server/entity-files.js +0 -15
- package/generators/node-server/generator.js +12 -9
- package/generators/node-server/generators/bootstrap/generator.js +2 -1
- package/generators/node-server/generators/migrations/command.js +5 -0
- package/generators/node-server/generators/migrations/files.js +42 -0
- package/generators/node-server/generators/migrations/generator.js +108 -0
- package/generators/node-server/generators/migrations/index.js +2 -0
- package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityRemovedMigration_.ts.ejs +15 -0
- package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityUpdatedConstraintsMigration_.ts.ejs +60 -0
- package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityUpdatedMigration_.ts.ejs +28 -0
- package/generators/node-server/resources/package.json +2 -2
- package/generators/node-server/support/migrations.js +196 -17
- package/generators/node-server/templates/package.json.ejs +0 -2
- package/generators/node-server/templates/server/README.md.ejs +13 -5
- package/generators/node-server/templates/server/e2e/session.e2e-spec.ts.ejs +17 -2
- package/generators/node-server/templates/server/src/app.config.ts.ejs +4 -4
- package/generators/node-server/templates/server/src/auth/csrf.middleware.spec.ts.ejs +15 -4
- package/generators/node-server/templates/server/src/auth/csrf.middleware.ts.ejs +18 -9
- package/generators/node-server/templates/server/src/auth/session.store.ts.ejs +2 -2
- package/generators/node-server/templates/server/src/auth/user.session.controller.ts.ejs +2 -2
- package/generators/node-server/templates/server/src/migrations/support/schema.ts.ejs +76 -24
- package/package.json +2 -2
- /package/generators/node-server/{templates → generators/migrations/templates}/server/src/migrations/_entityConstraintsMigration_.ts.ejs +0 -0
- /package/generators/node-server/{templates → generators/migrations/templates}/server/src/migrations/_entityMigration_.ts.ejs +0 -0
|
@@ -23,19 +23,4 @@ export const entityFiles = {
|
|
|
23
23
|
],
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
|
-
// The migrations of the entity, like its Liquibase changelogs: its table, then its constraints when it has some.
|
|
27
|
-
migrations: [
|
|
28
|
-
{
|
|
29
|
-
path: SERVER_NODEJS_DIR,
|
|
30
|
-
condition: ctx => ctx.databaseTypeSql,
|
|
31
|
-
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsMigration.fileName}.ts`,
|
|
32
|
-
templates: ['src/migrations/_entityMigration_.ts'],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
path: SERVER_NODEJS_DIR,
|
|
36
|
-
condition: ctx => ctx.databaseTypeSql && ctx.nodejsConstraintsMigration,
|
|
37
|
-
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsConstraintsMigration.fileName}.ts`,
|
|
38
|
-
templates: ['src/migrations/_entityConstraintsMigration_.ts'],
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
26
|
};
|
|
@@ -16,9 +16,7 @@ import {
|
|
|
16
16
|
apiFakeEntity,
|
|
17
17
|
apiFakeValue,
|
|
18
18
|
columnTransformer,
|
|
19
|
-
constraintsMigration,
|
|
20
19
|
contentTypeColumnOptions,
|
|
21
|
-
entityMigration,
|
|
22
20
|
fieldColumnOptions,
|
|
23
21
|
fieldColumnTypeImports,
|
|
24
22
|
fieldTypeValidators,
|
|
@@ -225,6 +223,12 @@ export default class extends BaseApplicationGenerator {
|
|
|
225
223
|
async composingTemplateTask() {
|
|
226
224
|
await this.composeWithJHipster('docker');
|
|
227
225
|
},
|
|
226
|
+
async composeMigrations() {
|
|
227
|
+
// The migrations of the entities of a SQL database, from their changelogs, like the incremental Liquibase changelogs.
|
|
228
|
+
if (this.jhipsterConfigWithDefaults.databaseType === 'sql') {
|
|
229
|
+
await this.composeWithJHipster('jhipster-nodejs:node-server:migrations');
|
|
230
|
+
}
|
|
231
|
+
},
|
|
228
232
|
async composeLanguages() {
|
|
229
233
|
// Like the spring-boot generator: the languages of the application to generate (languagesToGenerate), the server
|
|
230
234
|
// messages of server:i18n among them, without a client composing it.
|
|
@@ -434,9 +438,8 @@ export default class extends BaseApplicationGenerator {
|
|
|
434
438
|
application.nodejsUserPrimaryKeyColumn = primaryKeyColumn(application.user);
|
|
435
439
|
application.nodejsUserAuthorityJoinTable = userAuthorityJoinTable(application);
|
|
436
440
|
}
|
|
441
|
+
// The migrations of the entities are the ones of their changelogs (the migrations generator).
|
|
437
442
|
for (const entity of entities.filter(entity => !entity.skipServer && !entity.builtIn)) {
|
|
438
|
-
entity.nodejsMigration = entityMigration(entity);
|
|
439
|
-
entity.nodejsConstraintsMigration = constraintsMigration(entity);
|
|
440
443
|
// The join tables of the entity, its @JoinTable the same as its constraints migration.
|
|
441
444
|
for (const relationship of entity.relationships.filter(relationship => relationship.shouldWriteJoinTable)) {
|
|
442
445
|
relationship.nodejsJoinTable = joinTable(entity, relationship);
|
|
@@ -476,7 +479,7 @@ export default class extends BaseApplicationGenerator {
|
|
|
476
479
|
'server/src/migrations/1570200490072-SeedUsersRoles.ts',
|
|
477
480
|
],
|
|
478
481
|
],
|
|
479
|
-
'4.
|
|
482
|
+
'4.2.1': [
|
|
480
483
|
// The HTTP helpers of the server moved from src/client (a name taken by the client workspace) to src/common.
|
|
481
484
|
'server/src/client/header-util.ts',
|
|
482
485
|
'server/src/client/request.ts',
|
|
@@ -487,6 +490,9 @@ export default class extends BaseApplicationGenerator {
|
|
|
487
490
|
'server/webpack.server.prod.config.js',
|
|
488
491
|
// The Nest CLI builds and starts the server, and copies the configuration files (assets of nest-cli.json).
|
|
489
492
|
'server/scripts/copy-resources.ts',
|
|
493
|
+
// The development profile of 4.2.0 (BACKEND_ENV=dev) is in .env.dev: the .env file, loaded by every start of the
|
|
494
|
+
// server, would start the built server with the development profile.
|
|
495
|
+
'server/.env',
|
|
490
496
|
// The files of 4.2.0 moved to the feature modules (auth, users, management, common), their specs next to them.
|
|
491
497
|
'server/src/domain/authority.entity.ts',
|
|
492
498
|
'server/src/domain/user.entity.ts',
|
|
@@ -574,7 +580,7 @@ export default class extends BaseApplicationGenerator {
|
|
|
574
580
|
await control.cleanupFiles(this.oldNodejsVersion, {
|
|
575
581
|
'3.0.1': entities.filter(e => !e.skipServer).map(e => `server/src/repository/${e.entityFileName}.repository.ts`),
|
|
576
582
|
// The files of the entities, of 4.2.0, moved to their feature module (src/entities/<entity>).
|
|
577
|
-
'4.
|
|
583
|
+
'4.2.1': entities
|
|
578
584
|
.filter(entity => !entity.skipServer && !entity.builtIn)
|
|
579
585
|
.flatMap(({ entityFileName, fields }) => [
|
|
580
586
|
`server/src/domain/${entityFileName}.entity.ts`,
|
|
@@ -692,9 +698,6 @@ export default class extends BaseApplicationGenerator {
|
|
|
692
698
|
if (!entity.builtIn) {
|
|
693
699
|
source.addEntityToAppModule({ nodejsModulePath, entityClass });
|
|
694
700
|
}
|
|
695
|
-
for (const migration of [entity.nodejsMigration, entity.nodejsConstraintsMigration].filter(Boolean)) {
|
|
696
|
-
source.addMigrationToNodeConfig(migration);
|
|
697
|
-
}
|
|
698
701
|
}
|
|
699
702
|
},
|
|
700
703
|
});
|
|
@@ -75,7 +75,8 @@ export default class extends BaseApplicationGenerator {
|
|
|
75
75
|
return this.asDefaultTaskGroup({
|
|
76
76
|
postPreparing({ application }) {
|
|
77
77
|
if (application.authority) {
|
|
78
|
-
|
|
78
|
+
// The clients of JHipster with the screens of the built-in Authority, which their user management uses.
|
|
79
|
+
application.authority.skipClient = !application.clientFrameworkAngular && !application.clientFrameworkVue;
|
|
79
80
|
}
|
|
80
81
|
},
|
|
81
82
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SERVER_NODEJS_SRC_DIR } from '../../../generator-nodejs-constants.js';
|
|
2
|
+
|
|
3
|
+
const SERVER_NODEJS_DIR = `${SERVER_NODEJS_SRC_DIR}/`;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The migrations of a changelog of this generation, like the Liquibase changelogs of its entity: for its creation its table,
|
|
7
|
+
* then its constraints when it has some; for an update its fields, then its relationships; for its removal its tables.
|
|
8
|
+
*/
|
|
9
|
+
export const migrationFiles = {
|
|
10
|
+
migrations: [
|
|
11
|
+
{
|
|
12
|
+
path: SERVER_NODEJS_DIR,
|
|
13
|
+
condition: ctx => ctx.nodejsMigration,
|
|
14
|
+
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsMigration.fileName}.ts`,
|
|
15
|
+
templates: ['src/migrations/_entityMigration_.ts'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: SERVER_NODEJS_DIR,
|
|
19
|
+
condition: ctx => ctx.nodejsConstraintsMigration,
|
|
20
|
+
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsConstraintsMigration.fileName}.ts`,
|
|
21
|
+
templates: ['src/migrations/_entityConstraintsMigration_.ts'],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: SERVER_NODEJS_DIR,
|
|
25
|
+
condition: ctx => ctx.nodejsUpdatedMigration,
|
|
26
|
+
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsUpdatedMigration.fileName}.ts`,
|
|
27
|
+
templates: ['src/migrations/_entityUpdatedMigration_.ts'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: SERVER_NODEJS_DIR,
|
|
31
|
+
condition: ctx => ctx.nodejsUpdatedConstraintsMigration,
|
|
32
|
+
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsUpdatedConstraintsMigration.fileName}.ts`,
|
|
33
|
+
templates: ['src/migrations/_entityUpdatedConstraintsMigration_.ts'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: SERVER_NODEJS_DIR,
|
|
37
|
+
condition: ctx => ctx.nodejsRemovedMigration,
|
|
38
|
+
renameTo: data => `${SERVER_NODEJS_DIR}src/migrations/${data.nodejsRemovedMigration.fileName}.ts`,
|
|
39
|
+
templates: ['src/migrations/_entityRemovedMigration_.ts'],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import BaseApplicationGenerator from 'generator-jhipster/generators/base-application';
|
|
2
|
+
import {
|
|
3
|
+
CHANGELOGS_CUSTOM_OBJECT,
|
|
4
|
+
changelogsCustomObject,
|
|
5
|
+
getEntityChangelogs,
|
|
6
|
+
isNewEntityChangelog,
|
|
7
|
+
} from 'generator-jhipster/generators/base-application/generators/database-changelogs/support';
|
|
8
|
+
import { CONTEXT_DATA_APPLICATION_ENTITIES_KEY } from 'generator-jhipster/generators/base-application/support';
|
|
9
|
+
|
|
10
|
+
import { changelogMigrations, migrationOfFile } from '../../support/index.js';
|
|
11
|
+
|
|
12
|
+
import { migrationFiles } from './files.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The migrations of the entities of a SQL server, like the incremental Liquibase changelogs of JHipster Spring Boot: the
|
|
16
|
+
* migrations of the changelogs of the entities JHipster records (jhipster:base-application:database-changelogs, the
|
|
17
|
+
* changelogs custom object), a creation AddedEntity and AddedEntityConstraints, an update UpdatedEntity and
|
|
18
|
+
* UpdatedEntityConstraints (see support/migrations.js). A generation writes the migrations of its own changelogs, recorded
|
|
19
|
+
* in them, and registers every one: the ones of the former changelogs as they record them, written once.
|
|
20
|
+
*/
|
|
21
|
+
export default class extends BaseApplicationGenerator {
|
|
22
|
+
/** The migrations of the changelogs of this generation, with their entity. */
|
|
23
|
+
#migrations = [];
|
|
24
|
+
|
|
25
|
+
constructor(args, options, features) {
|
|
26
|
+
// The changelogs once the entities are prepared: their migrations are written from the entities and their fields.
|
|
27
|
+
super(args, options, { ...features, customObjects: [changelogsCustomObject] });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async beforeQueue() {
|
|
31
|
+
await this.dependsOnJHipster('jhipster-nodejs:node-server:bootstrap');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The changelogs of the entities, the ones of this generation recorded (see getEntityChangelogs of JHipster). */
|
|
35
|
+
get changelogs() {
|
|
36
|
+
return getEntityChangelogs(this);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The entities of the application with a table of the server, as JHipster prepares them. */
|
|
40
|
+
get #entities() {
|
|
41
|
+
return [...this.getContextData(CONTEXT_DATA_APPLICATION_ENTITIES_KEY, { factory: () => new Map() }).values()];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get [BaseApplicationGenerator.COMPOSING]() {
|
|
45
|
+
return this.asComposingTaskGroup({
|
|
46
|
+
async composeChangelogs() {
|
|
47
|
+
await this.composeWithJHipster('jhipster:base-application:database-changelogs');
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get [BaseApplicationGenerator.objectPriority(CHANGELOGS_CUSTOM_OBJECT, 'postPreparing')]() {
|
|
53
|
+
return {
|
|
54
|
+
prepareMigrations: ({ changelogs }) => {
|
|
55
|
+
const entities = this.#entities;
|
|
56
|
+
for (const changelog of changelogs.filter(changelog => isNewEntityChangelog(this, changelog))) {
|
|
57
|
+
// The entity of a creation or an update; a removal has none, its migration written from the entity before it.
|
|
58
|
+
const entity = entities.find(entity => entity.name === changelog.entityName);
|
|
59
|
+
if (!(entity ?? changelog.previousEntity).skipServer) {
|
|
60
|
+
this.#prepareMigrations(changelog, entity, entities);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get [BaseApplicationGenerator.WRITING_OBJECTS]() {
|
|
68
|
+
return this.asWritingObjectsTaskGroup({
|
|
69
|
+
async writeMigrations({ application }) {
|
|
70
|
+
for (const { entity, migrations } of this.#migrations) {
|
|
71
|
+
await this.writeFiles({ sections: migrationFiles, context: { ...application, ...entity, ...migrations } });
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get [BaseApplicationGenerator.POST_WRITING_OBJECTS]() {
|
|
78
|
+
return this.asPostWritingObjectsTaskGroup({
|
|
79
|
+
registerMigrations({ source }) {
|
|
80
|
+
// The migrations every changelog records, the ones of this generation included.
|
|
81
|
+
for (const changelog of this.changelogs) {
|
|
82
|
+
for (const fileName of this.getObjectConfig(changelogName(changelog))?.nodejsMigrations ?? []) {
|
|
83
|
+
source.addMigrationToNodeConfig(migrationOfFile(fileName));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The migrations of a changelog of this generation (support/migrations.js), written with its entity, and recorded in its
|
|
92
|
+
* object (nodejsMigrations), which the next generations register.
|
|
93
|
+
*/
|
|
94
|
+
#prepareMigrations(changelog, entity, entities) {
|
|
95
|
+
const migrations = changelogMigrations(changelog, entity, entities);
|
|
96
|
+
this.#migrations.push({ entity, migrations });
|
|
97
|
+
const name = changelogName(changelog);
|
|
98
|
+
this.setObjectConfig(name, {
|
|
99
|
+
...this.getObjectConfig(name),
|
|
100
|
+
nodejsMigrations: Object.values(migrations)
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.map(migration => migration.fileName),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The name of the object of a changelog, like JHipster names it. */
|
|
108
|
+
const changelogName = ({ changelogDate, entityName }) => `${changelogDate}_${entityName}`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
/** Removed the entity <%= nodejsRemovedMigration.entityName %>: its join tables and its table dropped. */
|
|
4
|
+
export class <%= nodejsRemovedMigration.className %> implements MigrationInterface {
|
|
5
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
6
|
+
<%_ for (const name of nodejsRemovedMigration.joinTables) { _%>
|
|
7
|
+
await queryRunner.dropTable('<%= name %>', true);
|
|
8
|
+
<%_ } _%>
|
|
9
|
+
await queryRunner.dropTable('<%= nodejsRemovedMigration.tableName %>', true);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public async down(_queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
// The table of the removed entity and its join tables are not restored, nor their values.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
<%_ if (nodejsUpdatedConstraintsMigration.schemaImports) { _%>
|
|
3
|
+
|
|
4
|
+
import { <%= nodejsUpdatedConstraintsMigration.schemaImports %> } from './support/schema.ts';
|
|
5
|
+
<%_ } _%>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Updated the relationships of the entity <%= entityClass %>: the join columns, their foreign keys and the join tables of its
|
|
9
|
+
* removed relationships dropped, the ones of its added relationships.
|
|
10
|
+
*/
|
|
11
|
+
export class <%= nodejsUpdatedConstraintsMigration.className %> implements MigrationInterface {
|
|
12
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
13
|
+
<%_ for (const name of nodejsUpdatedConstraintsMigration.removedJoinTables) { _%>
|
|
14
|
+
await queryRunner.dropTable('<%= name %>', true);
|
|
15
|
+
<%_ } _%>
|
|
16
|
+
<%_ if (nodejsUpdatedConstraintsMigration.removedJoinColumnNames) { _%>
|
|
17
|
+
await dropColumns(queryRunner, '<%= nodejsUpdatedConstraintsMigration.tableName %>', <%- nodejsUpdatedConstraintsMigration.removedJoinColumnNames %>);
|
|
18
|
+
<%_ } _%>
|
|
19
|
+
<%_ if (nodejsUpdatedConstraintsMigration.addedJoinColumnNames) { _%>
|
|
20
|
+
await addColumns(
|
|
21
|
+
queryRunner,
|
|
22
|
+
'<%= nodejsUpdatedConstraintsMigration.tableName %>',
|
|
23
|
+
[
|
|
24
|
+
<%_ for (const column of nodejsUpdatedConstraintsMigration.addedJoinColumns) { _%>
|
|
25
|
+
{ name: '<%= column.name %>', type: <%- column.type %>, nullable: true },
|
|
26
|
+
<%_ } _%>
|
|
27
|
+
],
|
|
28
|
+
<%_ if (nodejsUpdatedConstraintsMigration.relationUniques) { _%>
|
|
29
|
+
{ relationUniques: <%- nodejsUpdatedConstraintsMigration.relationUniques %> },
|
|
30
|
+
<%_ } _%>
|
|
31
|
+
);
|
|
32
|
+
await createForeignKeys(queryRunner, '<%= nodejsUpdatedConstraintsMigration.tableName %>', [
|
|
33
|
+
<%_ for (const foreignKey of nodejsUpdatedConstraintsMigration.addedForeignKeys) { _%>
|
|
34
|
+
{ columnName: '<%= foreignKey.columnName %>', referencedTableName: '<%= foreignKey.referencedTableName %>', referencedColumnName: '<%= foreignKey.referencedColumnName %>' },
|
|
35
|
+
<%_ } _%>
|
|
36
|
+
]);
|
|
37
|
+
<%_ } _%>
|
|
38
|
+
<%_ for (const joinTable of nodejsUpdatedConstraintsMigration.addedJoinTables) { _%>
|
|
39
|
+
await createJoinTable(
|
|
40
|
+
queryRunner,
|
|
41
|
+
'<%= joinTable.name %>',
|
|
42
|
+
<%_ for (const column of [joinTable.joinColumn, joinTable.inverseJoinColumn]) { _%>
|
|
43
|
+
{ name: '<%= column.name %>', type: <%- column.type %>, referencedTableName: '<%= column.referencedTableName %>', referencedColumnName: '<%= column.referencedColumnName %>' },
|
|
44
|
+
<%_ } _%>
|
|
45
|
+
);
|
|
46
|
+
<%_ } _%>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async down(<%= nodejsUpdatedConstraintsMigration.addedJoinColumnNames || nodejsUpdatedConstraintsMigration.addedJoinTables.length > 0 ? '' : '_' %>queryRunner: QueryRunner): Promise<void> {
|
|
50
|
+
<%_ if (nodejsUpdatedConstraintsMigration.removedJoinColumnNames || nodejsUpdatedConstraintsMigration.removedJoinTables.length > 0) { _%>
|
|
51
|
+
// The join columns and the join tables of the removed relationships are not restored, nor their values.
|
|
52
|
+
<%_ } _%>
|
|
53
|
+
<%_ for (const joinTable of nodejsUpdatedConstraintsMigration.addedJoinTables) { _%>
|
|
54
|
+
await queryRunner.dropTable('<%= joinTable.name %>', true);
|
|
55
|
+
<%_ } _%>
|
|
56
|
+
<%_ if (nodejsUpdatedConstraintsMigration.addedJoinColumnNames) { _%>
|
|
57
|
+
await dropColumns(queryRunner, '<%= nodejsUpdatedConstraintsMigration.tableName %>', <%- nodejsUpdatedConstraintsMigration.addedJoinColumnNames %>);
|
|
58
|
+
<%_ } _%>
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
import { <%= nodejsUpdatedMigration.schemaImports %> } from './support/schema.ts';
|
|
4
|
+
|
|
5
|
+
/** Updated the fields of the entity <%= entityClass %>: the columns of its added fields, the ones of its removed fields dropped. */
|
|
6
|
+
export class <%= nodejsUpdatedMigration.className %> implements MigrationInterface {
|
|
7
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
8
|
+
<%_ if (nodejsUpdatedMigration.addedColumnNames) { _%>
|
|
9
|
+
await addColumns(queryRunner, '<%= nodejsUpdatedMigration.tableName %>', [
|
|
10
|
+
<%_ for (const column of nodejsUpdatedMigration.addedColumns) { _%>
|
|
11
|
+
{ name: '<%= column.name %>', type: <%- column.type %><% if (column.enum) { %>, enum: <%- column.enum %><% } %><% if (column.length) { %>, length: <%= column.length %><% } %><% if (column.precision) { %>, precision: <%= column.precision %>, scale: <%= column.scale %><% } %><% if (column.nullable) { %>, nullable: true<% } %><% if (column.unique) { %>, unique: true<% } %> },
|
|
12
|
+
<%_ } _%>
|
|
13
|
+
]);
|
|
14
|
+
<%_ } _%>
|
|
15
|
+
<%_ if (nodejsUpdatedMigration.removedColumnNames) { _%>
|
|
16
|
+
await dropColumns(queryRunner, '<%= nodejsUpdatedMigration.tableName %>', <%- nodejsUpdatedMigration.removedColumnNames %>);
|
|
17
|
+
<%_ } _%>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async down(<%= nodejsUpdatedMigration.addedColumnNames ? '' : '_' %>queryRunner: QueryRunner): Promise<void> {
|
|
21
|
+
<%_ if (nodejsUpdatedMigration.removedColumnNames) { _%>
|
|
22
|
+
// The columns of the removed fields are not restored, nor their values.
|
|
23
|
+
<%_ } _%>
|
|
24
|
+
<%_ if (nodejsUpdatedMigration.addedColumnNames) { _%>
|
|
25
|
+
await dropColumns(queryRunner, '<%= nodejsUpdatedMigration.tableName %>', <%- nodejsUpdatedMigration.addedColumnNames %>);
|
|
26
|
+
<%_ } _%>
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"jwt-decode": "4.0.0",
|
|
30
30
|
"mongodb": "7.7.0",
|
|
31
31
|
"mongodb-memory-server": "11.3.0",
|
|
32
|
-
"mssql": "12.7.
|
|
32
|
+
"mssql": "12.7.13",
|
|
33
33
|
"mysql2": "3.24.6",
|
|
34
34
|
"oracledb": "7.0.1",
|
|
35
35
|
"passport": "0.7.0",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"sonarqube-scanner": "5.0.2",
|
|
69
69
|
"supertest": "7.3.1",
|
|
70
70
|
"typescript": "6.0.3",
|
|
71
|
-
"typescript-eslint": "8.72.
|
|
71
|
+
"typescript-eslint": "8.72.1",
|
|
72
72
|
"vite": "8.3.5",
|
|
73
73
|
"vitest": "5.0.5"
|
|
74
74
|
},
|
|
@@ -13,6 +13,14 @@ import { serverEnumInfo } from './enums.js';
|
|
|
13
13
|
* the timestamp of the changelogDate of their entity, the changelogs of the constraints the ones of the constraints at
|
|
14
14
|
* that timestamp plus CONSTRAINTS_TIMESTAMP_OFFSET, and the changelogs of the developers, at the end of master.xml, the
|
|
15
15
|
* migrations the typeorm:migration scripts create at their time plus MIGRATION_TIMESTAMP_OFFSET.
|
|
16
|
+
*
|
|
17
|
+
* Like the incremental changelogs of Liquibase, the migrations of an entity are the ones of its changelogs JHipster records
|
|
18
|
+
* (jhipster:base-application:database-changelogs, the changelog objects of the .yo-rc.json): its creation, AddedEntity
|
|
19
|
+
* and AddedEntityConstraints, then each update at the date of its changelog plus MIGRATION_TIMESTAMP_OFFSET (after the
|
|
20
|
+
* constraints, like the incremental changelogs at the end of master.xml): UpdatedEntity adds the columns of the added
|
|
21
|
+
* fields and drops the ones of the removed fields, then UpdatedEntityConstraints, 1 ms after, adds and drops the join
|
|
22
|
+
* columns, their foreign keys and the join tables of the relationships. A migration is written once, by the generation
|
|
23
|
+
* recording its changelog.
|
|
16
24
|
*/
|
|
17
25
|
|
|
18
26
|
/**
|
|
@@ -33,6 +41,22 @@ export const MIGRATION_TIMESTAMP_OFFSET = 6 * 10 ** 12;
|
|
|
33
41
|
/** The JavaScript timestamp (milliseconds, UTC) of the changelogDate of an entity, the date JHipster parses it to. */
|
|
34
42
|
const entityTimestamp = entity => entity.changelogDateForRecent.getTime();
|
|
35
43
|
|
|
44
|
+
/** The JavaScript timestamp (milliseconds, UTC) of a changelogDate of JHipster (yyyyMMddHHmmss). */
|
|
45
|
+
export const changelogDateToTimestamp = changelogDate => {
|
|
46
|
+
const [, year, month, day, hours, minutes, seconds] = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(changelogDate) ?? [];
|
|
47
|
+
if (!year) {
|
|
48
|
+
throw new Error(`The changelogDate ${changelogDate} is not a date of the yyyyMMddHHmmss format`);
|
|
49
|
+
}
|
|
50
|
+
return Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The class and the file of a migration of an entity, by the kind of the migration and its timestamp. */
|
|
54
|
+
const migrationName = (kind, entityClass, timestamp) => ({
|
|
55
|
+
timestamp,
|
|
56
|
+
className: `${kind}${entityClass}${timestamp}`,
|
|
57
|
+
fileName: `${timestamp}-${kind}${entityClass}`,
|
|
58
|
+
});
|
|
59
|
+
|
|
36
60
|
/** The TypeScript of the type of a column whose @Column decorator has none: the TypeScript type of its property. */
|
|
37
61
|
const typeOfFieldType = {
|
|
38
62
|
string: 'String',
|
|
@@ -75,12 +99,15 @@ export const fieldColumns = field => {
|
|
|
75
99
|
column.length = field.fieldValidateRulesMaxlength;
|
|
76
100
|
}
|
|
77
101
|
const columns = [column];
|
|
78
|
-
if (
|
|
102
|
+
if (hasContentTypeColumn(field)) {
|
|
79
103
|
columns.push({ name: `${field.fieldNameAsDatabaseColumn}_content_type`, type: 'String', nullable });
|
|
80
104
|
}
|
|
81
105
|
return columns;
|
|
82
106
|
};
|
|
83
107
|
|
|
108
|
+
/** Whether a field is a binary blob, stored with the column of its content type. */
|
|
109
|
+
const hasContentTypeColumn = field => ['byte[]', 'ByteBuffer'].includes(field.fieldType) && field.fieldTypeBlobContent !== 'text';
|
|
110
|
+
|
|
84
111
|
/**
|
|
85
112
|
* The camelCase of the default naming strategy of TypeORM, the names it gives the join columns and the columns of the join
|
|
86
113
|
* tables: unlike the one of lodash, it keeps the case of the letters it does not join (mainURL_id is mainURLId).
|
|
@@ -131,14 +158,11 @@ export const joinTable = (entity, relationship) => {
|
|
|
131
158
|
};
|
|
132
159
|
};
|
|
133
160
|
|
|
134
|
-
/** The migration adding the table of an entity, its name, its class and its columns. */
|
|
135
|
-
export const entityMigration = entity => {
|
|
136
|
-
const timestamp = entityTimestamp(entity);
|
|
161
|
+
/** The migration adding the table of an entity, its name, its class and its columns, at the timestamp of its creation. */
|
|
162
|
+
export const entityMigration = (entity, timestamp = entityTimestamp(entity)) => {
|
|
137
163
|
const relationships = joinColumnRelationships(entity);
|
|
138
164
|
return {
|
|
139
|
-
timestamp,
|
|
140
|
-
className: `AddedEntity${entity.entityClass}${timestamp}`,
|
|
141
|
-
fileName: `${timestamp}-AddedEntity${entity.entityClass}`,
|
|
165
|
+
...migrationName('AddedEntity', entity.entityClass, timestamp),
|
|
142
166
|
tableName: entity.entityTableName,
|
|
143
167
|
columns: [
|
|
144
168
|
primaryKeyColumn(entity),
|
|
@@ -158,25 +182,180 @@ export const entityMigration = entity => {
|
|
|
158
182
|
* The migration adding the constraints of an entity, its foreign keys and its join tables, when it has some: run after the
|
|
159
183
|
* migrations of every table, like the constraints changelogs of JHipster run after every table.
|
|
160
184
|
*/
|
|
161
|
-
export const constraintsMigration = entity => {
|
|
185
|
+
export const constraintsMigration = (entity, timestamp = entityTimestamp(entity)) => {
|
|
162
186
|
const joinColumns = joinColumnRelationships(entity);
|
|
163
187
|
const joinTables = joinTableRelationships(entity);
|
|
164
188
|
if (joinColumns.length === 0 && joinTables.length === 0) return undefined;
|
|
165
|
-
const timestamp = entityTimestamp(entity) + CONSTRAINTS_TIMESTAMP_OFFSET;
|
|
166
189
|
return {
|
|
167
|
-
timestamp,
|
|
168
|
-
className: `AddedEntityConstraints${entity.entityClass}${timestamp}`,
|
|
169
|
-
fileName: `${timestamp}-AddedEntityConstraints${entity.entityClass}`,
|
|
190
|
+
...migrationName('AddedEntityConstraints', entity.entityClass, timestamp + CONSTRAINTS_TIMESTAMP_OFFSET),
|
|
170
191
|
tableName: entity.entityTableName,
|
|
171
|
-
foreignKeys: joinColumns.map(
|
|
172
|
-
columnName: joinColumnName(relationship),
|
|
173
|
-
referencedTableName: relationship.otherEntity.entityTableName,
|
|
174
|
-
referencedColumnName: relationship.otherEntity.primaryKey.name,
|
|
175
|
-
})),
|
|
192
|
+
foreignKeys: joinColumns.map(foreignKey),
|
|
176
193
|
joinTables: joinTables.map(relationship => joinTable(entity, relationship)),
|
|
177
194
|
};
|
|
178
195
|
};
|
|
179
196
|
|
|
197
|
+
/** The foreign key of the join column of a relationship. */
|
|
198
|
+
const foreignKey = relationship => ({
|
|
199
|
+
columnName: joinColumnName(relationship),
|
|
200
|
+
referencedTableName: relationship.otherEntity.entityTableName,
|
|
201
|
+
referencedColumnName: relationship.otherEntity.primaryKey.name,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The migrations of a changelog of an entity, of this generation: for its creation the table of the entity at its date
|
|
206
|
+
* (nodejsMigration), then its constraints (nodejsConstraintsMigration); for an update its fields (nodejsUpdatedMigration),
|
|
207
|
+
* then its relationships (nodejsUpdatedConstraintsMigration), after the constraints of every entity; for its removal its
|
|
208
|
+
* join tables and its table (nodejsRemovedMigration), after the updates of the entities whose relationships referenced it.
|
|
209
|
+
* @param {any} changelog the changelog
|
|
210
|
+
* @param {any} entity the entity, prepared: none for a removal
|
|
211
|
+
* @param {any[]} entities the entities of the application, prepared
|
|
212
|
+
*/
|
|
213
|
+
export const changelogMigrations = (changelog, entity, entities) => {
|
|
214
|
+
const timestamp = changelogDateToTimestamp(changelog.changelogDate);
|
|
215
|
+
if (changelog.operation === 'create') {
|
|
216
|
+
return { nodejsMigration: entityMigration(entity, timestamp), nodejsConstraintsMigration: constraintsMigration(entity, timestamp) };
|
|
217
|
+
}
|
|
218
|
+
if (changelog.operation === 'update') {
|
|
219
|
+
const changes = { entity, ...changelogChanges(changelog, entity, entities) };
|
|
220
|
+
return {
|
|
221
|
+
nodejsUpdatedMigration: updatedEntityMigration(changes, timestamp + MIGRATION_TIMESTAMP_OFFSET),
|
|
222
|
+
// The relationships after the fields, whose join columns may reference them.
|
|
223
|
+
nodejsUpdatedConstraintsMigration: updatedConstraintsMigration(changes, timestamp + MIGRATION_TIMESTAMP_OFFSET + 1),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { nodejsRemovedMigration: removedEntityMigration(changelog, timestamp + MIGRATION_TIMESTAMP_OFFSET) };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The migration of the removal of an entity, like the dropTable of Liquibase: the join tables it owned, then its table, from
|
|
231
|
+
* the entity before the changelog (its previousEntity). The relationships of the other entities referencing it are removed
|
|
232
|
+
* with it, their join columns and their join tables dropped by their update migrations, dated before.
|
|
233
|
+
* @param {any} changelog the removal changelog of this generation
|
|
234
|
+
* @param {number} timestamp the timestamp of the migration
|
|
235
|
+
*/
|
|
236
|
+
export const removedEntityMigration = ({ entityName, previousEntity }, timestamp) => ({
|
|
237
|
+
...migrationName('RemovedEntity', `${entityName.charAt(0).toUpperCase()}${entityName.slice(1)}`, timestamp),
|
|
238
|
+
entityName,
|
|
239
|
+
tableName: previousEntity.entityTableName,
|
|
240
|
+
joinTables: previousEntity.relationships.filter(hasJoinTable).map(relationship => relationship.joinTable.name),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** The class of a migration, by the name of its file (`<timestamp>-<kind><Entity>`, its class `<kind><Entity><timestamp>`). */
|
|
244
|
+
export const migrationOfFile = fileName => ({ fileName, className: fileName.replace(/^(\d+)-(\w+)$/, '$2$1') });
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The fields and the relationships an update changelog of an entity adds and removes: the added ones the entity has,
|
|
248
|
+
* prepared; the removed ones the entity before the changelog has (its previousEntity, of this generation only), by the
|
|
249
|
+
* names of their columns. A relationship changed in its type, its other entity, its side or its join column
|
|
250
|
+
* (recreatedRelationships) is removed and added again; one changed in its options only (onDelete, onUpdate), which the
|
|
251
|
+
* foreign keys of the server do not declare, is left alone. A field changed (its type, its validations) is not migrated.
|
|
252
|
+
* @param {any} changelog the update changelog of this generation
|
|
253
|
+
* @param {any} entity the entity, prepared
|
|
254
|
+
* @param {any[]} entities the entities of the application, prepared
|
|
255
|
+
*/
|
|
256
|
+
export const changelogChanges = (changelog, entity, entities) => {
|
|
257
|
+
const { previousEntity, recreatedRelationships } = changelog;
|
|
258
|
+
const named = (items, names, key) => items.filter(item => names.includes(item[key]));
|
|
259
|
+
return {
|
|
260
|
+
addedFields: named(entity.fields, changelog.addedFields, 'fieldName'),
|
|
261
|
+
removedFields: named(previousEntity.fields, changelog.removedFields, 'fieldName'),
|
|
262
|
+
addedRelationships: named(entity.relationships, [...changelog.addedRelationships, ...recreatedRelationships], 'relationshipName'),
|
|
263
|
+
removedRelationships: named(
|
|
264
|
+
previousEntity.relationships,
|
|
265
|
+
[...changelog.removedRelationships, ...recreatedRelationships],
|
|
266
|
+
'relationshipName',
|
|
267
|
+
).map(relationship => removedRelationship(relationship, entities)),
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A relationship of the entity before a changelog, as the changelog records it (its type, its side, its other entity and its
|
|
273
|
+
* join table), with the properties of a relationship the migrations name its join column by: its field, and the primary key
|
|
274
|
+
* of its other entity (id when the other entity is no longer).
|
|
275
|
+
*/
|
|
276
|
+
const removedRelationship = (relationship, entities) => {
|
|
277
|
+
const otherEntity = entities.find(entity => entity.name.toLowerCase() === relationship.otherEntityName.toLowerCase());
|
|
278
|
+
return {
|
|
279
|
+
...relationship,
|
|
280
|
+
relationshipFieldName: `${relationship.relationshipName.charAt(0).toLowerCase()}${relationship.relationshipName.slice(1)}`,
|
|
281
|
+
otherEntity: otherEntity ?? { primaryKey: { name: 'id' } },
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/** The names of the columns of a field of the entity before a changelog, by the column name the changelog records. */
|
|
286
|
+
const removedFieldColumnNames = field => [field.columnName, ...(hasContentTypeColumn(field) ? [`${field.columnName}_content_type`] : [])];
|
|
287
|
+
|
|
288
|
+
/** The TypeScript of an array of names, none without a name. */
|
|
289
|
+
const namesLiteral = names => (names.length > 0 ? `[${names.map(literal).join(', ')}]` : undefined);
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The migration of the fields of an entity an update changelog changes, like the updated_entity changelog of Liquibase: the
|
|
293
|
+
* columns of the added fields, and the names of the columns of the removed fields. None without an added or a removed field.
|
|
294
|
+
* @param {{ entity: any, addedFields: any[], removedFields: any[] }} changes the changes of the changelog (changelogChanges)
|
|
295
|
+
* @param {number} timestamp the timestamp of the migration
|
|
296
|
+
*/
|
|
297
|
+
export const updatedEntityMigration = ({ entity, addedFields, removedFields }, timestamp) => {
|
|
298
|
+
if (addedFields.length === 0 && removedFields.length === 0) return undefined;
|
|
299
|
+
const addedColumns = addedFields.flatMap(fieldColumns);
|
|
300
|
+
return {
|
|
301
|
+
...migrationName('UpdatedEntity', entity.entityClass, timestamp),
|
|
302
|
+
tableName: entity.entityTableName,
|
|
303
|
+
addedColumns,
|
|
304
|
+
// The TypeScript of the names of the columns the migration adds and drops, and of the helpers it imports.
|
|
305
|
+
addedColumnNames: namesLiteral(addedColumns.map(column => column.name)),
|
|
306
|
+
removedColumnNames: namesLiteral(removedFields.flatMap(removedFieldColumnNames)),
|
|
307
|
+
schemaImports: addedColumns.length > 0 ? 'addColumns, dropColumns' : 'dropColumns',
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Whether a relationship has a join column: a many-to-one one, or a one-to-one one on its owner side. */
|
|
312
|
+
const hasJoinColumn = ({ relationshipType, ownerSide }) =>
|
|
313
|
+
relationshipType === 'many-to-one' || (relationshipType === 'one-to-one' && ownerSide);
|
|
314
|
+
|
|
315
|
+
/** Whether a relationship has a join table: a many-to-many one on its owner side. */
|
|
316
|
+
const hasJoinTable = ({ relationshipType, ownerSide }) => relationshipType === 'many-to-many' && ownerSide;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The migration of the relationships of an entity an update changelog changes, like the updated_entity_constraints changelog
|
|
320
|
+
* of Liquibase: the join columns with their foreign keys and the join tables of the added relationships, and the ones of
|
|
321
|
+
* the removed relationships dropped. None without a join column or a join table added or removed.
|
|
322
|
+
* @param {{ entity: any, addedRelationships: any[], removedRelationships: any[] }} changes the changes of the changelog (changelogChanges)
|
|
323
|
+
* @param {number} timestamp the timestamp of the migration
|
|
324
|
+
*/
|
|
325
|
+
export const updatedConstraintsMigration = ({ entity, addedRelationships, removedRelationships }, timestamp) => {
|
|
326
|
+
const addedJoinColumns = addedRelationships.filter(hasJoinColumn);
|
|
327
|
+
const changes = {
|
|
328
|
+
addedJoinColumns: addedJoinColumns.map(relationship => ({
|
|
329
|
+
...referenceColumn(joinColumnName(relationship), relationship.otherEntity),
|
|
330
|
+
nullable: true,
|
|
331
|
+
})),
|
|
332
|
+
addedForeignKeys: addedJoinColumns.map(foreignKey),
|
|
333
|
+
addedJoinTables: addedRelationships.filter(hasJoinTable).map(relationship => joinTable(entity, relationship)),
|
|
334
|
+
removedJoinColumns: removedRelationships.filter(hasJoinColumn).map(joinColumnName),
|
|
335
|
+
removedJoinTables: removedRelationships.filter(hasJoinTable).map(relationship => relationship.joinTable.name),
|
|
336
|
+
};
|
|
337
|
+
if (Object.values(changes).every(list => list.length === 0)) return undefined;
|
|
338
|
+
return {
|
|
339
|
+
...migrationName('UpdatedEntityConstraints', entity.entityClass, timestamp),
|
|
340
|
+
tableName: entity.entityTableName,
|
|
341
|
+
...changes,
|
|
342
|
+
// The TypeScript of the names of the join columns, of the unique ones of the one-to-one relationships, and of the helpers
|
|
343
|
+
// the migration imports.
|
|
344
|
+
addedJoinColumnNames: namesLiteral(changes.addedJoinColumns.map(column => column.name)),
|
|
345
|
+
relationUniques: namesLiteral(
|
|
346
|
+
addedJoinColumns.filter(relationship => relationship.relationshipType === 'one-to-one').map(joinColumnName),
|
|
347
|
+
),
|
|
348
|
+
removedJoinColumnNames: namesLiteral(changes.removedJoinColumns),
|
|
349
|
+
schemaImports: [
|
|
350
|
+
addedJoinColumns.length > 0 && 'addColumns, createForeignKeys',
|
|
351
|
+
changes.addedJoinTables.length > 0 && 'createJoinTable',
|
|
352
|
+
(addedJoinColumns.length > 0 || changes.removedJoinColumns.length > 0) && 'dropColumns',
|
|
353
|
+
]
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
.join(', '),
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
180
359
|
/**
|
|
181
360
|
* The join table of the authorities of the built-in users, named like TypeORM names the junction table of the @JoinTable()
|
|
182
361
|
* of User.authorities: the tables and the property, its columns the tables and their primary keys.
|
|
@@ -65,8 +65,6 @@
|
|
|
65
65
|
"docker:build": "docker build -t <%= baseName.toLowerCase() %> .",
|
|
66
66
|
"lint": "<%= clientPackageManager %> run lint --workspaces",
|
|
67
67
|
"lint:fix": "<%= clientPackageManager %> run lint:fix --workspaces",
|
|
68
|
-
"prettier:check": "prettier --check \"{,src/**/}*.{<%= prettierExtensions %>}\"",
|
|
69
|
-
"prettier:format": "prettier --write \"{,src/**/}*.{<%= prettierExtensions %>}\"",
|
|
70
68
|
<%_ if (clientFrameworkAny) { _%>
|
|
71
69
|
"start": "concurrently -k -s first -n application,client -c red,green npm:start:server npm:start:client",
|
|
72
70
|
<%_ } else { _%>
|
|
@@ -103,11 +103,19 @@ creates the tables of the users and of the authorities, then each entity has the
|
|
|
103
103
|
(`<timestamp>-AddedEntity<Entity>.ts`, the timestamp of its `changelogDate`) and, when it has relationships, the one adding
|
|
104
104
|
its foreign keys and its join tables (`<timestamp>-AddedEntityConstraints<Entity>.ts`, its timestamp plus 3000000000000),
|
|
105
105
|
run after the tables of every entity, like the constraints changelogs of Liquibase. A database the application ran them
|
|
106
|
-
on already is not changed by a regeneration of a migration
|
|
107
|
-
|
|
108
|
-
incremental changelogs, a regeneration
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
on already is not changed by a regeneration of a migration.
|
|
107
|
+
|
|
108
|
+
Like the incremental changelogs of Liquibase, a regeneration keeps the migrations of an entity and generates the ones of
|
|
109
|
+
its changes since, run after the others (their timestamp plus <%= nodejsMigrationTimestampOffset %>):
|
|
110
|
+
`<timestamp>-UpdatedEntity<Entity>.ts` adds the columns of the added fields and drops the ones of the removed fields,
|
|
111
|
+
then `<timestamp>-UpdatedEntityConstraints<Entity>.ts` adds and drops the join columns, their foreign keys and the join
|
|
112
|
+
tables of the relationships, and `<timestamp>-RemovedEntity<Entity>.ts` drops the join tables and the table of a removed
|
|
113
|
+
entity, after the updates of the entities whose relationships referenced it. The changes are the changelogs JHipster
|
|
114
|
+
records in the `objects` of `.yo-rc.json`, one for each change of the database schema of an entity: keep them with the
|
|
115
|
+
migrations, the migrations of the application are the ones of its changelogs. A field changed (its type, its
|
|
116
|
+
validations) and a table renamed are not migrated, a dropped column or table keeps none of its values, and a required
|
|
117
|
+
field added to a table having rows needs a migration of your own giving them a value
|
|
118
|
+
(`npm run typeorm:migration:generate`).
|
|
111
119
|
|
|
112
120
|
The application runs the migrations [src/orm.config.ts](src/orm.config.ts) lists: import the class of a migration of
|
|
113
121
|
your own and add it to `migrations`, so the application runs it at startup, like the ones JHipster adds. The
|
|
@@ -121,9 +121,14 @@ describe('Session authentication', () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('logs in the user of the login, in lower case, in a session of 30 minutes without requests', async () => {
|
|
124
|
+
const sessions = app.get<Repository<Session>>(getRepositoryToken(Session));
|
|
124
125
|
const response = await login({ username: 'Session-User', password, 'remember-me': 'false' });
|
|
125
126
|
expect(response.status).toBe(200);
|
|
126
|
-
|
|
127
|
+
// Like the JSESSIONID of JHipster Spring Boot, a cookie until the browser is closed: the session expires on the server.
|
|
128
|
+
expect(setCookie(response, 'JSESSIONID')).not.toMatch(/Expires|Max-Age/);
|
|
129
|
+
expect(setCookie(response, 'XSRF-TOKEN')).not.toMatch(/Expires|Max-Age/);
|
|
130
|
+
const [stored] = await sessions.find({ where: { login: 'session-user' } });
|
|
131
|
+
expect(stored.expires.getTime()).toBeLessThanOrEqual(Date.now() + 30 * 60 * 1000);
|
|
127
132
|
const { body } = await agent.get('/api/account').expect(200);
|
|
128
133
|
expect(body).toMatchObject({ login: 'session-user' });
|
|
129
134
|
expect(body.password).toBeUndefined();
|
|
@@ -158,7 +163,17 @@ describe('Session authentication', () => {
|
|
|
158
163
|
const response = await login({ username: 'session-user', password, 'remember-me': 'true' });
|
|
159
164
|
expect(response.status).toBe(200);
|
|
160
165
|
expect(cookieExpiry(response, 'JSESSIONID')).toBeGreaterThan(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
161
|
-
|
|
166
|
+
// The CSRF cookie lasts until the browser is closed, the next response giving the token of the session again.
|
|
167
|
+
expect(setCookie(response, 'XSRF-TOKEN')).not.toMatch(/Expires|Max-Age/);
|
|
168
|
+
const token = cookieValue(response, 'XSRF-TOKEN');
|
|
169
|
+
const reopened = request(app.getHttpServer()).get('/api/account').set('Cookie', setCookie(response, 'JSESSIONID')!.split(';')[0]);
|
|
170
|
+
expect(cookieValue(await reopened.expect(200), 'XSRF-TOKEN')).toEqual(token);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('sets no cookie in the responses of a logged session: a response of a session ended by a login meanwhile puts nothing back', async () => {
|
|
174
|
+
await login({ username: 'session-user', password });
|
|
175
|
+
const response = await agent.get('/api/account').expect(200);
|
|
176
|
+
expect(response.headers['set-cookie']).toBeUndefined();
|
|
162
177
|
});
|
|
163
178
|
|
|
164
179
|
it('refuses the unsafe requests of a session without its CSRF token, the logout included', async () => {
|
|
@@ -12,7 +12,6 @@ import { HeaderUtil } from './common/header-util.ts';
|
|
|
12
12
|
import { ProblemDetailsFilter } from './common/errors/problem-details.filter.ts';
|
|
13
13
|
import { ValidationException } from './common/errors/problem.exception.ts';
|
|
14
14
|
<%_ if (authenticationUsesCsrf) { _%>
|
|
15
|
-
import { SESSION_MAX_AGE } from './auth/csrf.middleware.ts';
|
|
16
15
|
import { SessionStore } from './auth/session.store.ts';
|
|
17
16
|
<%_ } _%>
|
|
18
17
|
|
|
@@ -142,9 +141,10 @@ export function configureApp(app: INestApplication): INestApplication {
|
|
|
142
141
|
store: app.get(SessionStore),
|
|
143
142
|
resave: false,
|
|
144
143
|
saveUninitialized: false,
|
|
145
|
-
// Like the
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// Like the JSESSIONID of JHipster Spring Boot, a cookie until the browser is closed, set when the session is created
|
|
145
|
+
// only: the session expires on the server, after 30 minutes without requests (SessionStore). A cookie set again by
|
|
146
|
+
// every response would let a response of a session ended by a login meanwhile put the ended session back.
|
|
147
|
+
cookie: { httpOnly: true, sameSite: 'lax', secure: 'auto' },
|
|
148
148
|
}),
|
|
149
149
|
);
|
|
150
150
|
<%_ } _%>
|
|
@@ -28,9 +28,13 @@ describe('CsrfMiddleware', () => {
|
|
|
28
28
|
expect(() => checkLoginCsrfToken({ headers: { cookie: 'SESSION=1; XSRF-TOKEN=t0k', 'x-xsrf-token': 't0k' } } as any)).not.toThrow();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it('gives the CSRF cookie of a remember-me session
|
|
31
|
+
it('gives the CSRF cookie of a remember-me session until the browser is closed, like JHipster Spring Boot', () => {
|
|
32
32
|
const { res } = call('GET', { user: { login: 'user' }, csrfToken: 't0k', cookie: { originalMaxAge: 1000 } });
|
|
33
|
-
expect(res.cookie).toHaveBeenCalledWith('XSRF-TOKEN', 't0k', expect.objectContaining({ maxAge:
|
|
33
|
+
expect(res.cookie).toHaveBeenCalledWith('XSRF-TOKEN', 't0k', expect.not.objectContaining({ maxAge: expect.anything() }));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('gives a CSRF cookie until the browser is closed to a client without a session, like JHipster Spring Boot', () => {
|
|
37
|
+
expect(call('GET', {}).res.cookie).toHaveBeenCalledWith('XSRF-TOKEN', expect.any(String), expect.not.objectContaining({ maxAge: expect.anything() }));
|
|
34
38
|
});
|
|
35
39
|
<%_ } else { _%>
|
|
36
40
|
it('lets the requests without a logged session pass, without the cookie', () => {
|
|
@@ -40,10 +44,17 @@ describe('CsrfMiddleware', () => {
|
|
|
40
44
|
});
|
|
41
45
|
<%_ } _%>
|
|
42
46
|
|
|
43
|
-
it('gives the token of a logged session
|
|
47
|
+
it('gives the token of a logged session as the XSRF-TOKEN cookie on its safe requests too, until the browser is closed', () => {
|
|
44
48
|
const { res, next } = call('GET', { user: { login: 'user' }, csrfToken: 't0k' });
|
|
45
49
|
expect(next).toHaveBeenCalled();
|
|
46
|
-
expect(res.cookie).toHaveBeenCalledWith('XSRF-TOKEN', 't0k', expect.objectContaining({ path: '/'
|
|
50
|
+
expect(res.cookie).toHaveBeenCalledWith('XSRF-TOKEN', 't0k', expect.objectContaining({ path: '/' }));
|
|
51
|
+
expect(res.cookie).toHaveBeenCalledWith('XSRF-TOKEN', 't0k', expect.not.objectContaining({ maxAge: expect.anything() }));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not set the cookie of a client with the token of its session: a response of a session ended meanwhile puts no old token back', () => {
|
|
55
|
+
const { res, next } = call('GET', { user: { login: 'user' }, csrfToken: 't0k' }, { cookie: 'JSESSIONID=s1; XSRF-TOKEN=t0k' });
|
|
56
|
+
expect(next).toHaveBeenCalled();
|
|
57
|
+
expect(res.cookie).not.toHaveBeenCalled();
|
|
47
58
|
});
|
|
48
59
|
|
|
49
60
|
it('refuses an unsafe request of a logged session without its token, the logout too, like Spring Security', () => {
|
|
@@ -2,13 +2,15 @@ import { randomUUID } from 'crypto';
|
|
|
2
2
|
import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common';
|
|
3
3
|
import { NextFunction, Request, Response } from 'express';
|
|
4
4
|
|
|
5
|
-
/** The idle lifetime of a session,
|
|
5
|
+
/** The idle lifetime of a session, on the server: its cookie lasts until the browser is closed. */
|
|
6
6
|
export const SESSION_MAX_AGE = 30 * 60 * 1000;
|
|
7
7
|
<%_ if (authenticationTypeSession) { _%>
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The idle lifetime of a session logged in with remember-me, like the validity of the remember-me tokens of JHipster
|
|
11
|
-
* Spring Boot (31 days): kept in the database (SessionStore), the session outlives a restart like those tokens.
|
|
11
|
+
* Spring Boot (31 days): kept in the database (SessionStore), the session outlives a restart like those tokens. Its
|
|
12
|
+
* cookie expires 31 days after the login, unlike the remember-me cookie of JHipster Spring Boot, renewed at each new
|
|
13
|
+
* session it logs in.
|
|
12
14
|
*/
|
|
13
15
|
export const REMEMBER_ME_MAX_AGE = 31 * 24 * 60 * 60 * 1000;
|
|
14
16
|
<%_ } _%>
|
|
@@ -16,11 +18,13 @@ export const REMEMBER_ME_MAX_AGE = 31 * 24 * 60 * 60 * 1000;
|
|
|
16
18
|
/** The methods that change nothing, which the CSRF token does not protect, like the ones of Spring Security. */
|
|
17
19
|
const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS', 'TRACE'];
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The XSRF-TOKEN cookie the clients (Angular HttpClient, axios) send back as the X-XSRF-TOKEN header: until the browser is
|
|
23
|
+
* closed, like the one of JHipster Spring Boot, a remember-me session included.
|
|
24
|
+
*/
|
|
25
|
+
export function setCsrfCookie(res: Response, token: string): void {
|
|
26
|
+
res.cookie('XSRF-TOKEN', token, { sameSite: 'lax', path: '/' });
|
|
22
27
|
}
|
|
23
|
-
<%_ if (authenticationTypeSession) { _%>
|
|
24
28
|
|
|
25
29
|
/** The XSRF-TOKEN cookie of a request. */
|
|
26
30
|
function csrfCookie(req: Request): string | undefined {
|
|
@@ -30,6 +34,7 @@ function csrfCookie(req: Request): string | undefined {
|
|
|
30
34
|
.find(value => value.startsWith('XSRF-TOKEN='));
|
|
31
35
|
return cookie ? decodeURIComponent(cookie.slice('XSRF-TOKEN='.length)) : undefined;
|
|
32
36
|
}
|
|
37
|
+
<%_ if (authenticationTypeSession) { _%>
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* The CSRF check of the session login, like the CookieCsrfTokenRepository of Spring Security: the X-XSRF-TOKEN header is
|
|
@@ -46,7 +51,7 @@ export function checkLoginCsrfToken(req: Request): void {
|
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
53
|
* The CSRF protection of the sessions, like the CSRF filter of Spring Security and the CookieCsrfFilter of JHipster Spring
|
|
49
|
-
* Boot:
|
|
54
|
+
* Boot: a logged session gives its token as the XSRF-TOKEN cookie to a client without it, and every unsafe request of it,
|
|
50
55
|
* the logout included, must send it as X-XSRF-TOKEN, else 403. A request with a bearer token has no session to protect.
|
|
51
56
|
<%_ if (authenticationTypeSession) { _%>
|
|
52
57
|
* A client without a logged session gets a token cookie, which its login must send back (checkLoginCsrfToken).
|
|
@@ -66,8 +71,12 @@ export class CsrfMiddleware implements NestMiddleware {
|
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
68
73
|
session.csrfToken ??= randomUUID();
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
if (csrfCookie(req) !== session.csrfToken) {
|
|
75
|
+
// Like Spring Security, the cookie is set when the client has not the token of its session only (a remember-me
|
|
76
|
+
// session after the browser is closed): a response of a session ended by a login meanwhile does not put the token
|
|
77
|
+
// of the ended session back.
|
|
78
|
+
setCsrfCookie(res, session.csrfToken);
|
|
79
|
+
}
|
|
71
80
|
if (!SAFE_METHODS.includes(req.method) && req.headers['x-xsrf-token'] !== session.csrfToken) {
|
|
72
81
|
throw new ForbiddenException('Invalid CSRF token');
|
|
73
82
|
}
|
|
@@ -59,7 +59,7 @@ export class SessionStore extends session.Store implements OnModuleInit, OnModul
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
touch(sid: string, data: session.SessionData, callback?: Callback): void {
|
|
62
|
-
// The
|
|
62
|
+
// The idle expiry of a session its request did not change.
|
|
63
63
|
this.track(this.repository.update({ sid }, { expires: expiresOf(data) })).then(() => callback?.(), error => callback?.(error));
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -104,7 +104,7 @@ export class SessionStore extends session.Store implements OnModuleInit, OnModul
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** The end of a session: the expiry of its cookie. */
|
|
107
|
+
/** The end of a session: the expiry of its cookie with remember-me, else 30 minutes after its last request. */
|
|
108
108
|
function expiresOf(data: session.SessionData): Date {
|
|
109
109
|
return data.cookie?.expires ? new Date(data.cookie.expires) : new Date(Date.now() + SESSION_MAX_AGE);
|
|
110
110
|
}
|
|
@@ -64,10 +64,10 @@ export class UserSessionController {
|
|
|
64
64
|
session.userAgent = req.get('user-agent');
|
|
65
65
|
if (REMEMBER_ME_VALUES.includes(String(form['remember-me']).toLowerCase())) {
|
|
66
66
|
// Like the remember-me of JHipster Spring Boot, the login is kept after the browser is closed, until the session
|
|
67
|
-
// is unused for 31 days.
|
|
67
|
+
// is unused for 31 days (its cookie expires 31 days after the login).
|
|
68
68
|
session.cookie.maxAge = REMEMBER_ME_MAX_AGE;
|
|
69
69
|
}
|
|
70
|
-
setCsrfCookie(res, session.csrfToken
|
|
70
|
+
setCsrfCookie(res, session.csrfToken);
|
|
71
71
|
await new Promise<void>((resolve, reject) => session.save((error?: Error) => (error ? reject(error) : resolve())));
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ColumnType, type QueryRunner, Table, type TableColumnOptions, TableForeignKey } from 'typeorm';
|
|
1
|
+
import { type ColumnType, type QueryRunner, Table, TableColumn, type TableColumnOptions, TableForeignKey, TableIndex, TableUnique } from 'typeorm';
|
|
2
2
|
import type { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata.js';
|
|
3
3
|
|
|
4
4
|
import { databaseColumnType, databaseDatetimePrecision } from '../../common/database/column-types.ts';
|
|
@@ -111,32 +111,87 @@ export const createTable = async (
|
|
|
111
111
|
columns: MigrationColumn[],
|
|
112
112
|
options: { relationUniques?: string[] } = {},
|
|
113
113
|
): Promise<void> => {
|
|
114
|
+
const { tableColumns, uniques, indices } = uniqueColumns(queryRunner, name, columns, options.relationUniques);
|
|
115
|
+
await queryRunner.createTable(new Table({ name, columns: tableColumns, uniques, indices }), true);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The columns of a table with their unique constraints, as TypeORM stores them: unique columns, unique constraints or
|
|
120
|
+
* unique indices, by database.
|
|
121
|
+
*/
|
|
122
|
+
const uniqueColumns = (queryRunner: QueryRunner, tableName: string, columns: MigrationColumn[], relationUniqueColumns: string[] = []) => {
|
|
114
123
|
const { driver, namingStrategy } = queryRunner.connection;
|
|
115
124
|
const uniqueIndices = uniqueIndexDatabases.includes(driver.options.type);
|
|
116
125
|
const relationUniqueIndices = uniqueIndices || driver.options.type === 'mssql';
|
|
117
126
|
// The unique constraint of the join column of a one-to-one relationship, named like TypeORM names it.
|
|
118
|
-
const relationUniques =
|
|
119
|
-
name: namingStrategy.relationConstraintName(
|
|
127
|
+
const relationUniques = relationUniqueColumns.map(columnName => ({
|
|
128
|
+
name: namingStrategy.relationConstraintName(tableName, [columnName]),
|
|
120
129
|
columnNames: [columnName],
|
|
121
130
|
}));
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
return {
|
|
132
|
+
tableColumns: columns.map(column => tableColumn(queryRunner, { ...column, unique: column.unique && !uniqueIndices })),
|
|
133
|
+
uniques: relationUniqueIndices ? [] : relationUniques,
|
|
134
|
+
indices: [
|
|
135
|
+
...(uniqueIndices ? columns.filter(column => column.unique).map(column => ({ columnNames: [column.name], isUnique: true })) : []),
|
|
136
|
+
...(relationUniqueIndices
|
|
137
|
+
? relationUniques.map(unique => ({
|
|
138
|
+
...unique,
|
|
139
|
+
isUnique: true,
|
|
140
|
+
where: driver.options.type === 'mssql' ? `${driver.escape(unique.columnNames[0])} IS NOT NULL` : undefined,
|
|
141
|
+
}))
|
|
142
|
+
: []),
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** The table of a migration, which the database has. */
|
|
148
|
+
const existingTable = async (queryRunner: QueryRunner, tableName: string): Promise<Table> => {
|
|
149
|
+
const table = await queryRunner.getTable(tableName);
|
|
150
|
+
if (!table) {
|
|
151
|
+
throw new Error(`The table ${tableName} does not exist`);
|
|
152
|
+
}
|
|
153
|
+
return table;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Adds the columns of a table missing in the database, like createTable creates them: a database synchronized from the
|
|
158
|
+
* entities has them already.
|
|
159
|
+
*/
|
|
160
|
+
export const addColumns = async (
|
|
161
|
+
queryRunner: QueryRunner,
|
|
162
|
+
tableName: string,
|
|
163
|
+
columns: MigrationColumn[],
|
|
164
|
+
options: { relationUniques?: string[] } = {},
|
|
165
|
+
): Promise<void> => {
|
|
166
|
+
const table = await existingTable(queryRunner, tableName);
|
|
167
|
+
const missing = columns.filter(column => !table.findColumnByName(column.name));
|
|
168
|
+
if (missing.length === 0) return;
|
|
169
|
+
const missingNames = missing.map(column => column.name);
|
|
170
|
+
const { tableColumns, uniques, indices } = uniqueColumns(
|
|
171
|
+
queryRunner,
|
|
172
|
+
tableName,
|
|
173
|
+
missing,
|
|
174
|
+
(options.relationUniques ?? []).filter(name => missingNames.includes(name)),
|
|
139
175
|
);
|
|
176
|
+
await queryRunner.addColumns(table, tableColumns.map(column => new TableColumn(column)));
|
|
177
|
+
if (uniques.length > 0) {
|
|
178
|
+
await queryRunner.createUniqueConstraints(tableName, uniques.map(unique => new TableUnique(unique)));
|
|
179
|
+
}
|
|
180
|
+
if (indices.length > 0) {
|
|
181
|
+
await queryRunner.createIndices(tableName, indices.map(index => new TableIndex(index)));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Drops the columns of a table the database has, with their foreign keys. */
|
|
186
|
+
export const dropColumns = async (queryRunner: QueryRunner, tableName: string, columnNames: string[]): Promise<void> => {
|
|
187
|
+
const table = await existingTable(queryRunner, tableName);
|
|
188
|
+
const existing = columnNames.filter(name => table.findColumnByName(name));
|
|
189
|
+
if (existing.length === 0) return;
|
|
190
|
+
const foreignKeys = table.foreignKeys.filter(foreignKey => foreignKey.columnNames.some(name => existing.includes(name)));
|
|
191
|
+
if (foreignKeys.length > 0) {
|
|
192
|
+
await queryRunner.dropForeignKeys(table, foreignKeys);
|
|
193
|
+
}
|
|
194
|
+
await queryRunner.dropColumns(tableName, existing);
|
|
140
195
|
};
|
|
141
196
|
|
|
142
197
|
/**
|
|
@@ -144,10 +199,7 @@ export const createTable = async (
|
|
|
144
199
|
* has them already.
|
|
145
200
|
*/
|
|
146
201
|
export const createForeignKeys = async (queryRunner: QueryRunner, tableName: string, foreignKeys: MigrationForeignKey[]): Promise<void> => {
|
|
147
|
-
const table = await queryRunner
|
|
148
|
-
if (!table) {
|
|
149
|
-
throw new Error(`The table ${tableName} does not exist`);
|
|
150
|
-
}
|
|
202
|
+
const table = await existingTable(queryRunner, tableName);
|
|
151
203
|
const missing = foreignKeys.filter(
|
|
152
204
|
foreignKey =>
|
|
153
205
|
!table.foreignKeys.some(existing => existing.columnNames.length === 1 && existing.columnNames[0] === foreignKey.columnName),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mshima/generator-jhipster-nodejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A NodeJS blueprint that creates the backend using NodeJS with NestJS framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yeoman-generator",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"chalk": "^5.3.0",
|
|
58
|
-
"generator-jhipster": "npm:@mshima/generator-jhipster@0.0.
|
|
58
|
+
"generator-jhipster": "npm:@mshima/generator-jhipster@0.0.10"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"ejs-lint": "2.0.1",
|
|
File without changes
|