@mshima/generator-jhipster-nodejs 0.0.0-stage → 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/LICENSE.txt +204 -0
- package/README.md +243 -2
- package/build-info.json +3 -0
- package/cli/cli-customizations.cjs +9 -0
- package/cli/cli.cjs +40 -0
- package/cli/logo.js +31 -0
- package/cli/version.cjs +12 -0
- package/generators/ci-cd/USAGE +5 -0
- package/generators/ci-cd/command.js +5 -0
- package/generators/ci-cd/generator.js +158 -0
- package/generators/ci-cd/index.js +2 -0
- package/generators/ci-cd/templates/.gitlab-ci.yml.ejs +137 -0
- package/generators/ci-cd/templates/azure-pipelines.yml.ejs +68 -0
- package/generators/ci-cd/templates/circle.yml.ejs +59 -0
- package/generators/ci-cd/templates/docker-registry.yml.ejs +58 -0
- package/generators/ci-cd/templates/github-ci.yml.ejs +90 -0
- package/generators/ci-cd/templates/jenkins/Jenkinsfile.ejs +104 -0
- package/generators/ci-cd/templates/jenkins/idea.gdsl +90 -0
- package/generators/ci-cd/templates/jenkins/jenkins.yml.ejs +33 -0
- package/generators/ci-cd/templates/travis.yml.ejs +75 -0
- package/generators/client/command.js +5 -0
- package/generators/client/generator.js +21 -0
- package/generators/client/generators/bootstrap/command.js +5 -0
- package/generators/client/generators/bootstrap/generator.js +28 -0
- package/generators/client/generators/bootstrap/index.js +2 -0
- package/generators/client/index.js +2 -0
- package/generators/common/generators/bootstrap/command.js +5 -0
- package/generators/common/generators/bootstrap/generator.js +37 -0
- package/generators/common/generators/bootstrap/index.js +2 -0
- package/generators/docker/command.js +5 -0
- package/generators/docker/generator.js +16 -0
- package/generators/docker/generators/bootstrap/command.js +5 -0
- package/generators/docker/generators/bootstrap/generator.js +56 -0
- package/generators/docker/generators/bootstrap/index.js +2 -0
- package/generators/docker/index.js +2 -0
- package/generators/docker/templates/docker/app.yml.ejs +25 -0
- package/generators/generator-nodejs-constants.js +32 -0
- package/generators/languages/command.js +5 -0
- package/generators/languages/generator.js +22 -0
- package/generators/languages/index.js +2 -0
- package/generators/languages/templates/src/main/webapp/i18n/en/home.json +19 -0
- package/generators/languages/templates/src/main/webapp/i18n/es/home.json +19 -0
- package/generators/languages/templates/src/main/webapp/i18n/it/home.json +19 -0
- package/generators/node-server/command.js +96 -0
- package/generators/node-server/entity-files.js +26 -0
- package/generators/node-server/files.js +254 -0
- package/generators/node-server/generator.js +705 -0
- package/generators/node-server/generators/bootstrap/command.js +5 -0
- package/generators/node-server/generators/bootstrap/generator.js +84 -0
- package/generators/node-server/generators/bootstrap/index.js +2 -0
- 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/_entityConstraintsMigration_.ts.ejs +40 -0
- package/generators/node-server/generators/migrations/templates/server/src/migrations/_entityMigration_.ts.ejs +26 -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/index.js +2 -0
- package/generators/node-server/resources/package.json +76 -0
- package/generators/node-server/support/api-fake-data.js +54 -0
- package/generators/node-server/support/columns.js +73 -0
- package/generators/node-server/support/database.js +24 -0
- package/generators/node-server/support/entity-authorities.js +32 -0
- package/generators/node-server/support/entity-paths.js +13 -0
- package/generators/node-server/support/enums.js +11 -0
- package/generators/node-server/support/index.js +9 -0
- package/generators/node-server/support/mail-templates.js +82 -0
- package/generators/node-server/support/migrations.js +376 -0
- package/generators/node-server/support/validators.js +25 -0
- package/generators/node-server/templates/.dockerignore.ejs +16 -0
- package/generators/node-server/templates/.gitignore.jhi.nodejs.ejs +24 -0
- package/generators/node-server/templates/Dockerfile.ejs +54 -0
- package/generators/node-server/templates/README.md.jhi.nodejs.ejs +220 -0
- package/generators/node-server/templates/package.json.ejs +82 -0
- package/generators/node-server/templates/server/README.md.ejs +277 -0
- package/generators/node-server/templates/server/e2e/_entityFileName_.database.e2e-spec.ts.ejs +192 -0
- package/generators/node-server/templates/server/e2e/_entityFileName_.e2e-spec.ts.ejs +402 -0
- package/generators/node-server/templates/server/e2e/account.e2e-spec.ts.ejs +349 -0
- package/generators/node-server/templates/server/e2e/app.e2e-spec.ts.ejs +188 -0
- package/generators/node-server/templates/server/e2e/authority.e2e-spec.ts.ejs +50 -0
- package/generators/node-server/templates/server/e2e/cors.e2e-spec.ts.ejs +110 -0
- package/generators/node-server/templates/server/e2e/management.e2e-spec.ts.ejs +180 -0
- package/generators/node-server/templates/server/e2e/oauth2-bearer.e2e-spec.ts.ejs +71 -0
- package/generators/node-server/templates/server/e2e/session.e2e-spec.ts.ejs +234 -0
- package/generators/node-server/templates/server/e2e/setup.test.js.ejs +1 -0
- package/generators/node-server/templates/server/e2e/user.e2e-spec.ts.ejs +318 -0
- package/generators/node-server/templates/server/env.ejs +1 -0
- package/generators/node-server/templates/server/eslint.config.mjs.ejs +48 -0
- package/generators/node-server/templates/server/nest-cli.json.ejs +11 -0
- package/generators/node-server/templates/server/ormconfig.ts.ejs +35 -0
- package/generators/node-server/templates/server/package.json.ejs +143 -0
- package/generators/node-server/templates/server/sonar-project.properties.ejs +10 -0
- package/generators/node-server/templates/server/src/app.config.spec.ts.ejs +13 -0
- package/generators/node-server/templates/server/src/app.config.ts.ejs +179 -0
- package/generators/node-server/templates/server/src/app.module.ts.ejs +80 -0
- package/generators/node-server/templates/server/src/application-config.ts.ejs +153 -0
- package/generators/node-server/templates/server/src/auth/account.controller.ts.ejs +183 -0
- package/generators/node-server/templates/server/src/auth/auth.module.ts.ejs +104 -0
- package/generators/node-server/templates/server/src/auth/auth.service.ts.ejs +294 -0
- package/generators/node-server/templates/server/src/auth/csrf.middleware.spec.ts.ejs +76 -0
- package/generators/node-server/templates/server/src/auth/csrf.middleware.ts.ejs +85 -0
- package/generators/node-server/templates/server/src/auth/decorators/auth-user.decorator.ts.ejs +8 -0
- package/generators/node-server/templates/server/src/auth/decorators/roles.decorator.ts.ejs +4 -0
- package/generators/node-server/templates/server/src/auth/dto/key-and-password.dto.ts.ejs +17 -0
- package/generators/node-server/templates/server/src/auth/dto/password-change.dto.ts.ejs +17 -0
- package/generators/node-server/templates/server/src/auth/dto/user-login.dto.ts.ejs +21 -0
- package/generators/node-server/templates/server/src/auth/guards/auth.guard.ts.ejs +50 -0
- package/generators/node-server/templates/server/src/auth/guards/oauth2-login.guard.ts.ejs +6 -0
- package/generators/node-server/templates/server/src/auth/guards/roles.guard.ts.ejs +25 -0
- package/generators/node-server/templates/server/src/auth/index.ts.ejs +8 -0
- package/generators/node-server/templates/server/src/auth/jwt.config.spec.ts.ejs +22 -0
- package/generators/node-server/templates/server/src/auth/jwt.config.ts.ejs +29 -0
- package/generators/node-server/templates/server/src/auth/oauth2.authorities.spec.ts.ejs +24 -0
- package/generators/node-server/templates/server/src/auth/oauth2.authorities.ts.ejs +33 -0
- package/generators/node-server/templates/server/src/auth/oauth2.config.ts.ejs +51 -0
- package/generators/node-server/templates/server/src/auth/passport.jwt.strategy.ts.ejs +45 -0
- package/generators/node-server/templates/server/src/auth/passport.oauth2-bearer.strategy.ts.ejs +32 -0
- package/generators/node-server/templates/server/src/auth/passport.oauth2.strategy.spec.ts.ejs +27 -0
- package/generators/node-server/templates/server/src/auth/passport.oauth2.strategy.ts.ejs +44 -0
- package/generators/node-server/templates/server/src/auth/password-util.ts.ejs +10 -0
- package/generators/node-server/templates/server/src/auth/password.service.ts.ejs +34 -0
- package/generators/node-server/templates/server/src/auth/payload.interface.ts.ejs +14 -0
- package/generators/node-server/templates/server/src/auth/role-type.ts.ejs +7 -0
- package/generators/node-server/templates/server/src/auth/session-user.middleware.ts.ejs +33 -0
- package/generators/node-server/templates/server/src/auth/session.entity.ts.ejs +36 -0
- package/generators/node-server/templates/server/src/auth/session.store.spec.ts.ejs +99 -0
- package/generators/node-server/templates/server/src/auth/session.store.ts.ejs +116 -0
- package/generators/node-server/templates/server/src/auth/user.jwt.controller.ts.ejs +26 -0
- package/generators/node-server/templates/server/src/auth/user.oauth2.controller.ts.ejs +78 -0
- package/generators/node-server/templates/server/src/auth/user.session.controller.ts.ejs +103 -0
- package/generators/node-server/templates/server/src/common/common.module.ts.ejs +19 -0
- package/generators/node-server/templates/server/src/common/database/base.entity.ts.ejs +13 -0
- package/generators/node-server/templates/server/src/common/database/column-transformers.spec.ts.ejs +46 -0
- package/generators/node-server/templates/server/src/common/database/column-transformers.ts.ejs +69 -0
- package/generators/node-server/templates/server/src/common/database/column-types.spec.ts.ejs +21 -0
- package/generators/node-server/templates/server/src/common/database/column-types.ts.ejs +29 -0
- package/generators/node-server/templates/server/src/common/database/pagination.entity.spec.ts.ejs +66 -0
- package/generators/node-server/templates/server/src/common/database/pagination.entity.ts.ejs +108 -0
- package/generators/node-server/templates/server/src/common/dto/base.dto.ts.ejs +19 -0
- package/generators/node-server/templates/server/src/common/errors/error-constants.ts.ejs +10 -0
- package/generators/node-server/templates/server/src/common/errors/problem-details.filter.ts.ejs +63 -0
- package/generators/node-server/templates/server/src/common/errors/problem.exception.ts.ejs +92 -0
- package/generators/node-server/templates/server/src/common/field_validators.ejs +77 -0
- package/generators/node-server/templates/server/src/common/header-util.ts.ejs +105 -0
- package/generators/node-server/templates/server/src/common/interceptors/logging.interceptor.spec.ts.ejs +93 -0
- package/generators/node-server/templates/server/src/common/interceptors/logging.interceptor.ts.ejs +48 -0
- package/generators/node-server/templates/server/src/common/logging.spec.ts.ejs +83 -0
- package/generators/node-server/templates/server/src/common/logging.ts.ejs +129 -0
- package/generators/node-server/templates/server/src/common/metrics.spec.ts.ejs +43 -0
- package/generators/node-server/templates/server/src/common/metrics.ts.ejs +198 -0
- package/generators/node-server/templates/server/src/common/request.ts.ejs +8 -0
- package/generators/node-server/templates/server/src/common/sortable-properties.ts.ejs +43 -0
- package/generators/node-server/templates/server/src/common/updatable-properties.ts.ejs +14 -0
- package/generators/node-server/templates/server/src/config/application-dev.yml.ejs +118 -0
- package/generators/node-server/templates/server/src/config/application-prod.yml.ejs +97 -0
- package/generators/node-server/templates/server/src/config/application-secret-samples.yml.ejs +42 -0
- package/generators/node-server/templates/server/src/config/application-test.yml.ejs +68 -0
- package/generators/node-server/templates/server/src/config/application-tls-samples.yml.ejs +15 -0
- package/generators/node-server/templates/server/src/config/application-tls.yml.ejs +15 -0
- package/generators/node-server/templates/server/src/config/application.yml.ejs +149 -0
- package/generators/node-server/templates/server/src/config.spec.ts.ejs +81 -0
- package/generators/node-server/templates/server/src/config.ts.ejs +255 -0
- package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.controller.ts.ejs +178 -0
- package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.entity.ts.ejs +110 -0
- package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.mapper.ts.ejs +37 -0
- package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.module.ts.ejs +14 -0
- package/generators/node-server/templates/server/src/entities/_entityFileName_/_entityFileName_.service.ts.ejs +123 -0
- package/generators/node-server/templates/server/src/entities/_entityFileName_/dto/_entityFileName_.dto.ts.ejs +99 -0
- package/generators/node-server/templates/server/src/mail/mail.module.ts.ejs +49 -0
- package/generators/node-server/templates/server/src/mail/mail.service.spec.ts.ejs +105 -0
- package/generators/node-server/templates/server/src/mail/mail.service.ts.ejs +120 -0
- package/generators/node-server/templates/server/src/main.ts.ejs +114 -0
- package/generators/node-server/templates/server/src/management/api-docs.controller.ts.ejs +27 -0
- package/generators/node-server/templates/server/src/management/api-docs.service.ts.ejs +8 -0
- package/generators/node-server/templates/server/src/management/configuration.controller.ts.ejs +80 -0
- package/generators/node-server/templates/server/src/management/health.controller.spec.ts.ejs +79 -0
- package/generators/node-server/templates/server/src/management/health.controller.ts.ejs +87 -0
- package/generators/node-server/templates/server/src/management/health.module.ts.ejs +9 -0
- package/generators/node-server/templates/server/src/management/loggers.controller.ts.ejs +40 -0
- package/generators/node-server/templates/server/src/management/management.controller.spec.ts.ejs +26 -0
- package/generators/node-server/templates/server/src/management/management.controller.ts.ejs +34 -0
- package/generators/node-server/templates/server/src/management/management.module.ts.ejs +21 -0
- package/generators/node-server/templates/server/src/management/metrics.controller.ts.ejs +85 -0
- package/generators/node-server/templates/server/src/migrations/1570200270081-CreateTables.ts.ejs +64 -0
- package/generators/node-server/templates/server/src/migrations/1570200270082-CreateSessions.ts.ejs +26 -0
- package/generators/node-server/templates/server/src/migrations/1570200490072-SeedUsersRoles.ts.ejs +74 -0
- package/generators/node-server/templates/server/src/migrations/support/schema.ts.ejs +254 -0
- package/generators/node-server/templates/server/src/orm.config.ts.ejs +147 -0
- package/generators/node-server/templates/server/src/registry/consul.service.ts.ejs +70 -0
- package/generators/node-server/templates/server/src/registry/eureka.service.ts.ejs +129 -0
- package/generators/node-server/templates/server/src/swagger.ts.ejs +24 -0
- package/generators/node-server/templates/server/src/tls.ts.ejs +27 -0
- package/generators/node-server/templates/server/src/users/authority.controller.ts.ejs +85 -0
- package/generators/node-server/templates/server/src/users/authority.entity.ts.ejs +22 -0
- package/generators/node-server/templates/server/src/users/dto/authority.dto.ts.ejs +10 -0
- package/generators/node-server/templates/server/src/users/dto/user.dto.ts.ejs +116 -0
- package/generators/node-server/templates/server/src/users/public.user.controller.ts.ejs +51 -0
- package/generators/node-server/templates/server/src/users/user.controller.ts.ejs +191 -0
- package/generators/node-server/templates/server/src/users/user.entity.ts.ejs +63 -0
- package/generators/node-server/templates/server/src/users/user.mapper.ts.ejs +26 -0
- package/generators/node-server/templates/server/src/users/user.module.ts.ejs +19 -0
- package/generators/node-server/templates/server/src/users/user.service.ts.ejs +156 -0
- package/generators/node-server/templates/server/tsconfig.build.json.ejs +9 -0
- package/generators/node-server/templates/server/tsconfig.json.ejs +23 -0
- package/generators/node-server/templates/server/typeorm-migration.ts.ejs +18 -0
- package/generators/node-server/templates/server/vitest.config.e2e.ts.ejs +16 -0
- package/generators/node-server/templates/server/vitest.config.ts.ejs +16 -0
- package/generators/server/command.js +7 -0
- package/generators/server/generator.js +20 -0
- package/generators/server/generators/bootstrap/command.js +5 -0
- package/generators/server/generators/bootstrap/generator.js +15 -0
- package/generators/server/generators/bootstrap/index.js +2 -0
- package/generators/server/index.js +2 -0
- package/package.json +75 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
3
|
+
import session from 'express-session';
|
|
4
|
+
import { <% if (databaseTypeSql) { %>LessThan, <% } %>Repository } from 'typeorm';
|
|
5
|
+
import { Session } from './session.entity.ts';
|
|
6
|
+
import { SESSION_MAX_AGE } from './csrf.middleware.ts';
|
|
7
|
+
|
|
8
|
+
/** The interval of the removal of the expired sessions. */
|
|
9
|
+
export const PRUNE_INTERVAL = 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
type Callback<T = unknown> = (error?: any, value?: T) => void;
|
|
12
|
+
|
|
13
|
+
/** A session of a user, its id and its data. */
|
|
14
|
+
export interface StoredSession {
|
|
15
|
+
sid: string;
|
|
16
|
+
data: session.SessionData & Record<string, any>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The sessions of the server in the database of the application, the store of express-session: a session outlives a
|
|
21
|
+
* restart of the application and is shared by its instances, unlike the MemoryStore of express-session, and the expired
|
|
22
|
+
* sessions are removed every hour. The application closes after the operations in progress: express-session ends a
|
|
23
|
+
* response in the callback of its touch, after the client has the whole response.
|
|
24
|
+
*/
|
|
25
|
+
@Injectable()
|
|
26
|
+
export class SessionStore extends session.Store implements OnModuleInit, OnModuleDestroy {
|
|
27
|
+
private readonly logger = new Logger('SessionStore');
|
|
28
|
+
private pruneTimer?: NodeJS.Timeout;
|
|
29
|
+
/** The operations in progress on the database, waited for before the database is closed. */
|
|
30
|
+
private readonly pending = new Set<Promise<unknown>>();
|
|
31
|
+
|
|
32
|
+
constructor(@InjectRepository(Session) private readonly repository: Repository<Session>) {
|
|
33
|
+
super();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onModuleInit(): void {
|
|
37
|
+
this.pruneTimer = setInterval(() => {
|
|
38
|
+
this.prune().catch((error: Error) => this.logger.warn(`The expired sessions could not be removed: ${error.message}`));
|
|
39
|
+
}, PRUNE_INTERVAL);
|
|
40
|
+
// The removal does not keep the process alive.
|
|
41
|
+
this.pruneTimer.unref();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Before TypeORM closes the database (onApplicationShutdown): the operations in progress end first. */
|
|
45
|
+
async onModuleDestroy(): Promise<void> {
|
|
46
|
+
clearInterval(this.pruneTimer);
|
|
47
|
+
await Promise.allSettled([...this.pending]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get(sid: string, callback: Callback<session.SessionData | null>): void {
|
|
51
|
+
this.track(this.repository.findOne({ where: { sid } })).then(
|
|
52
|
+
found => callback(null, found && found.expires.getTime() > Date.now() ? JSON.parse(found.data) : null),
|
|
53
|
+
callback,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
set(sid: string, data: session.SessionData, callback?: Callback): void {
|
|
58
|
+
this.track(this.save(sid, data)).then(() => callback?.(), error => callback?.(error));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
touch(sid: string, data: session.SessionData, callback?: Callback): void {
|
|
62
|
+
// The idle expiry of a session its request did not change.
|
|
63
|
+
this.track(this.repository.update({ sid }, { expires: expiresOf(data) })).then(() => callback?.(), error => callback?.(error));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
destroy(sid: string, callback?: Callback): void {
|
|
67
|
+
this.track(this.repository.delete({ sid })).then(() => callback?.(), error => callback?.(error));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
all(callback: Callback<Record<string, session.SessionData>>): void {
|
|
71
|
+
this.track(this.repository.find()).then(
|
|
72
|
+
sessions => callback(null, Object.fromEntries(alive(sessions).map(({ sid, data }) => [sid, data]))),
|
|
73
|
+
callback,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The sessions of a user, not expired. */
|
|
78
|
+
async sessionsOf(login: string): Promise<StoredSession[]> {
|
|
79
|
+
return alive(await this.repository.find({ where: { login } }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Removes the expired sessions. */
|
|
83
|
+
async prune(): Promise<void> {
|
|
84
|
+
<%_ if (databaseTypeSql) { _%>
|
|
85
|
+
await this.repository.delete({ expires: LessThan(new Date()) });
|
|
86
|
+
<%_ } else { _%>
|
|
87
|
+
await this.repository.delete({ expires: { $lt: new Date() } } as any);
|
|
88
|
+
<%_ } _%>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** An operation on the database, in progress until it settles. */
|
|
92
|
+
private track<T>(operation: Promise<T>): Promise<T> {
|
|
93
|
+
this.pending.add(operation);
|
|
94
|
+
const settled = (): void => {
|
|
95
|
+
this.pending.delete(operation);
|
|
96
|
+
};
|
|
97
|
+
operation.then(settled, settled);
|
|
98
|
+
return operation;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async save(sid: string, data: session.SessionData): Promise<void> {
|
|
102
|
+
const found = await this.repository.findOne({ where: { sid } });
|
|
103
|
+
await this.repository.save({ ...found, sid, login: (data as any).user?.login ?? null, expires: expiresOf(data), data: JSON.stringify(data) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The end of a session: the expiry of its cookie with remember-me, else 30 minutes after its last request. */
|
|
108
|
+
function expiresOf(data: session.SessionData): Date {
|
|
109
|
+
return data.cookie?.expires ? new Date(data.cookie.expires) : new Date(Date.now() + SESSION_MAX_AGE);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The sessions not expired, with their data. */
|
|
113
|
+
function alive(sessions: Session[]): StoredSession[] {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
return sessions.filter(stored => stored.expires.getTime() > now).map(({ sid, data }) => ({ sid, data: JSON.parse(data) }));
|
|
116
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Body, Controller, Logger, Post, Res, Req, HttpCode } from '@nestjs/common';
|
|
2
|
+
import { Response, Request } from 'express';
|
|
3
|
+
import { UserLoginDTO } from './dto/user-login.dto.ts';
|
|
4
|
+
import { AuthService } from './auth.service.ts';
|
|
5
|
+
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
|
6
|
+
|
|
7
|
+
@Controller('api')
|
|
8
|
+
@ApiTags('user-jwt-controller')
|
|
9
|
+
export class UserJWTController {
|
|
10
|
+
logger = new Logger('UserJWTController');
|
|
11
|
+
|
|
12
|
+
constructor(private readonly authService: AuthService) {}
|
|
13
|
+
|
|
14
|
+
@Post('/authenticate')
|
|
15
|
+
@HttpCode(200)
|
|
16
|
+
@ApiOperation({ summary: 'Authorization api retrieving token' })
|
|
17
|
+
@ApiResponse({
|
|
18
|
+
status: 200,
|
|
19
|
+
description: 'Authorized',
|
|
20
|
+
})
|
|
21
|
+
async authorize(@Req() req: Request, @Body() user: UserLoginDTO, @Res() res: Response): Promise<any> {
|
|
22
|
+
const jwt = await this.authService.login(user);
|
|
23
|
+
res.setHeader('Authorization', 'Bearer ' + jwt.id_token);
|
|
24
|
+
return res.json(jwt);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { Controller, Get, Logger, Post, Res, UseGuards, Req } from '@nestjs/common';
|
|
4
|
+
import { AuthService } from './auth.service.ts';
|
|
5
|
+
import { ApiTags, ApiResponse, ApiOperation, ApiExcludeEndpoint } from '@nestjs/swagger';
|
|
6
|
+
import { setCsrfCookie } from './csrf.middleware.ts';
|
|
7
|
+
import { OAuth2LoginGuard } from './guards/oauth2-login.guard.ts';
|
|
8
|
+
import { OAuth2Config } from './oauth2.config.ts';
|
|
9
|
+
|
|
10
|
+
@Controller()
|
|
11
|
+
@ApiTags('user-oauth2-controller')
|
|
12
|
+
export class UserOauth2Controller {
|
|
13
|
+
logger = new Logger('UserOauth2Controller');
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly authService: AuthService,
|
|
17
|
+
private readonly oauth2Config: OAuth2Config,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
@ApiExcludeEndpoint()
|
|
21
|
+
@UseGuards(OAuth2LoginGuard)
|
|
22
|
+
@Get('/login/oauth2/code/oidc')
|
|
23
|
+
@ApiOperation({ summary: 'Backend redirect' })
|
|
24
|
+
@ApiResponse({
|
|
25
|
+
status: 200,
|
|
26
|
+
description: 'Redirect oauth2 oidc after login',
|
|
27
|
+
})
|
|
28
|
+
redirect(@Req() req: any, @Res() res: any): void {
|
|
29
|
+
const { user } = req;
|
|
30
|
+
// Like Spring Security, a new session for the authenticated user (the id of the session before the login is not
|
|
31
|
+
// kept), with the CSRF token the clients send back as X-XSRF-TOKEN.
|
|
32
|
+
req.session.regenerate((error?: Error) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
this.logger.error(`The session of the login could not be created: ${error.message}`);
|
|
35
|
+
res.status(500).end();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
req.session.user = user;
|
|
39
|
+
req.session.csrfToken = randomUUID();
|
|
40
|
+
setCsrfCookie(res, req.session.csrfToken);
|
|
41
|
+
req.session.save(() => res.redirect('/'));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@ApiExcludeEndpoint()
|
|
46
|
+
@Get('/oauth2/authorization/oidc')
|
|
47
|
+
@UseGuards(OAuth2LoginGuard)
|
|
48
|
+
@ApiOperation({ summary: 'Perform login oauth2 oidc from client' })
|
|
49
|
+
@ApiResponse({
|
|
50
|
+
status: 200,
|
|
51
|
+
description: 'Redirect to login oauth2 oidc',
|
|
52
|
+
})
|
|
53
|
+
loginAuthOidc(): void {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@ApiExcludeEndpoint()
|
|
58
|
+
@Post('/api/logout')
|
|
59
|
+
@ApiOperation({ summary: 'Perform logout oauth2 oidc from client' })
|
|
60
|
+
@ApiResponse({
|
|
61
|
+
status: 201,
|
|
62
|
+
description: 'Logout oauth2 oidc',
|
|
63
|
+
})
|
|
64
|
+
logoutAuthOidc(@Req() req: any): any {
|
|
65
|
+
if (req.session && req.session.user) {
|
|
66
|
+
const idToken = req.session.user.idToken;
|
|
67
|
+
req.session.destroy();
|
|
68
|
+
// Like JHipster Spring Boot, the logout URL of the provider logs out the session of the user there, and redirects
|
|
69
|
+
// back to the application (the client goes to it).
|
|
70
|
+
const logoutUrl = new URL(this.oauth2Config.logoutUrl);
|
|
71
|
+
logoutUrl.searchParams.set('id_token_hint', idToken);
|
|
72
|
+
logoutUrl.searchParams.set('post_logout_redirect_uri', req.headers.origin ?? `${req.protocol}://${req.get('host')}`);
|
|
73
|
+
return { idToken, logoutUrl: logoutUrl.toString() };
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { Body, Controller, Delete, Get, HttpCode, Logger, Param, Post, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common';
|
|
4
|
+
import { ApiConsumes, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
|
5
|
+
import { Request, Response } from 'express';
|
|
6
|
+
import { AuthService } from './auth.service.ts';
|
|
7
|
+
import { checkLoginCsrfToken, REMEMBER_ME_MAX_AGE, setCsrfCookie } from './csrf.middleware.ts';
|
|
8
|
+
import { sessionUser } from './session-user.middleware.ts';
|
|
9
|
+
import { AuthGuard } from './guards/auth.guard.ts';
|
|
10
|
+
import { SessionStore } from './session.store.ts';
|
|
11
|
+
|
|
12
|
+
/** The form of the session login, the one the JHipster clients post, like the formLogin of JHipster Spring Boot. */
|
|
13
|
+
interface LoginForm {
|
|
14
|
+
username?: string;
|
|
15
|
+
password?: string;
|
|
16
|
+
'remember-me'?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The values of the remember-me parameter that ask for it, like the ones of the RememberMeServices of Spring Security. */
|
|
20
|
+
const REMEMBER_ME_VALUES = ['true', 'on', 'yes', '1'];
|
|
21
|
+
|
|
22
|
+
/** A session of the user, as the sessions page of the clients shows it, like the PersistentToken of JHipster Spring Boot. */
|
|
23
|
+
interface SessionDTO {
|
|
24
|
+
series: string;
|
|
25
|
+
tokenDate: string;
|
|
26
|
+
ipAddress?: string;
|
|
27
|
+
userAgent?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Controller('api')
|
|
31
|
+
@ApiTags('user-session-controller')
|
|
32
|
+
export class UserSessionController {
|
|
33
|
+
logger = new Logger('UserSessionController');
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly authService: AuthService,
|
|
37
|
+
private readonly sessionStore: SessionStore,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
@Post('/authentication')
|
|
41
|
+
@HttpCode(200)
|
|
42
|
+
@ApiConsumes('application/x-www-form-urlencoded')
|
|
43
|
+
@ApiOperation({ summary: 'Log in, in a new session' })
|
|
44
|
+
@ApiResponse({ status: 200, description: 'Logged in' })
|
|
45
|
+
@ApiResponse({ status: 401, description: 'Invalid login, password, or a user not activated' })
|
|
46
|
+
@ApiResponse({ status: 403, description: 'Without the CSRF token of the XSRF-TOKEN cookie' })
|
|
47
|
+
async authenticate(@Req() req: Request, @Res({ passthrough: true }) res: Response, @Body() form: LoginForm): Promise<void> {
|
|
48
|
+
checkLoginCsrfToken(req);
|
|
49
|
+
if (typeof form?.username !== 'string' || typeof form.password !== 'string' || !form.username || !form.password) {
|
|
50
|
+
throw new UnauthorizedException();
|
|
51
|
+
}
|
|
52
|
+
const user = sessionUser(await this.authService.authenticate(form.username, form.password));
|
|
53
|
+
// Like Spring Security, a new session for the authenticated user (the id of the session before the login is not
|
|
54
|
+
// kept), with the CSRF token the clients send back as X-XSRF-TOKEN.
|
|
55
|
+
await new Promise<void>((resolve, reject) => req.session.regenerate((error?: Error) => (error ? reject(error) : resolve())));
|
|
56
|
+
const session = req.session as any;
|
|
57
|
+
session.user = user;
|
|
58
|
+
session.csrfToken = randomUUID();
|
|
59
|
+
// The session as the sessions page shows it, like the PersistentToken of JHipster Spring Boot: its series (random,
|
|
60
|
+
// not its id), the date, the address and the browser of its login.
|
|
61
|
+
session.series = randomBytes(16).toString('base64url');
|
|
62
|
+
session.tokenDate = new Date().toISOString().slice(0, 10);
|
|
63
|
+
session.ipAddress = req.ip;
|
|
64
|
+
session.userAgent = req.get('user-agent');
|
|
65
|
+
if (REMEMBER_ME_VALUES.includes(String(form['remember-me']).toLowerCase())) {
|
|
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 (its cookie expires 31 days after the login).
|
|
68
|
+
session.cookie.maxAge = REMEMBER_ME_MAX_AGE;
|
|
69
|
+
}
|
|
70
|
+
setCsrfCookie(res, session.csrfToken);
|
|
71
|
+
await new Promise<void>((resolve, reject) => session.save((error?: Error) => (error ? reject(error) : resolve())));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Post('/logout')
|
|
75
|
+
@HttpCode(200)
|
|
76
|
+
@ApiOperation({ summary: 'Log out, ending the session' })
|
|
77
|
+
@ApiResponse({ status: 200, description: 'Logged out' })
|
|
78
|
+
async logout(@Req() req: Request): Promise<void> {
|
|
79
|
+
await new Promise<void>(resolve => req.session.destroy(() => resolve()));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Get('/account/sessions')
|
|
83
|
+
@UseGuards(AuthGuard)
|
|
84
|
+
@ApiOperation({ summary: 'The sessions of the current user, like the persistent tokens of JHipster Spring Boot' })
|
|
85
|
+
@ApiResponse({ status: 200, description: 'The sessions of the user' })
|
|
86
|
+
async getCurrentSessions(@Req() req: Request): Promise<SessionDTO[]> {
|
|
87
|
+
const { login } = (req.session as any).user;
|
|
88
|
+
return (await this.sessionStore.sessionsOf(login))
|
|
89
|
+
.filter(({ data }) => data.series)
|
|
90
|
+
.map(({ data: { series, tokenDate, ipAddress, userAgent } }) => ({ series, tokenDate, ipAddress, userAgent }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Delete('/account/sessions/:series')
|
|
94
|
+
@UseGuards(AuthGuard)
|
|
95
|
+
@ApiOperation({ summary: 'Ends a session of the current user, like the invalidation of a persistent token of JHipster Spring Boot' })
|
|
96
|
+
@ApiResponse({ status: 200, description: 'The session of the series, if any, ended' })
|
|
97
|
+
async invalidateSession(@Req() req: Request, @Param('series') series: string): Promise<void> {
|
|
98
|
+
const { login } = (req.session as any).user;
|
|
99
|
+
for (const { sid } of (await this.sessionStore.sessionsOf(login)).filter(({ data }) => data.series === series)) {
|
|
100
|
+
await new Promise<void>((resolve, reject) => this.sessionStore.destroy(sid, error => (error ? reject(error as Error) : resolve())));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Global, Module } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { HeaderUtil } from './header-util.ts';
|
|
4
|
+
<%_ if (authenticationTypeOauth2) { _%>
|
|
5
|
+
import { OAuth2Config } from '../auth/oauth2.config.ts';
|
|
6
|
+
<%_ } _%>
|
|
7
|
+
<%_ if (generateUserManagement) { _%>
|
|
8
|
+
import { PasswordService } from '../auth/password.service.ts';
|
|
9
|
+
<%_ } _%>
|
|
10
|
+
|
|
11
|
+
const providers = [HeaderUtil<% if (authenticationTypeOauth2) { %>, OAuth2Config<% } %><% if (generateUserManagement) { %>, PasswordService<% } %>];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The providers every module of the application uses, reading the configuration with ConfigService, like the beans of
|
|
15
|
+
* JHipster Spring Boot: the headers of the responses<% if (authenticationTypeOauth2) { %>, the endpoints of the OpenID Connect provider<% } %><% if (generateUserManagement) { %>, the passwords of the users<% } %>.
|
|
16
|
+
*/
|
|
17
|
+
@Global()
|
|
18
|
+
@Module({ providers, exports: providers })
|
|
19
|
+
export class CommonModule {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
export abstract class BaseEntity {
|
|
4
|
+
// The creation audit columns are readable but never updated, like the updatable = false columns of JHipster Spring Boot.
|
|
5
|
+
@Column({ nullable: true, update: false })
|
|
6
|
+
createdBy?: string;
|
|
7
|
+
@CreateDateColumn({ nullable: true, update: false })
|
|
8
|
+
createdDate?: Date;
|
|
9
|
+
@Column({ nullable: true })
|
|
10
|
+
lastModifiedBy?: string;
|
|
11
|
+
@UpdateDateColumn({ nullable: true })
|
|
12
|
+
lastModifiedDate?: Date;
|
|
13
|
+
}
|
package/generators/node-server/templates/server/src/common/database/column-transformers.spec.ts.ejs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
blobTransformer,
|
|
5
|
+
durationTransformer,
|
|
6
|
+
formatDuration,
|
|
7
|
+
numberTransformer,
|
|
8
|
+
parseDuration,
|
|
9
|
+
} from './column-transformers.ts';
|
|
10
|
+
|
|
11
|
+
describe('column transformers', () => {
|
|
12
|
+
it('should read a bigint or a decimal given as a string as a number', () => {
|
|
13
|
+
expect(numberTransformer.from('25179')).toBe(25179);
|
|
14
|
+
expect(numberTransformer.from('25829.80')).toBe(25829.8);
|
|
15
|
+
expect(numberTransformer.from(null)).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should read a blob given as a Buffer as the string it stored', () => {
|
|
19
|
+
expect(blobTransformer.from(Buffer.from('aGlwc3Rlcg=='))).toBe('aGlwc3Rlcg==');
|
|
20
|
+
expect(blobTransformer.from('aGlwc3Rlcg==')).toBe('aGlwc3Rlcg==');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should store a duration as its nanoseconds and read it back as an ISO-8601 duration', () => {
|
|
24
|
+
expect(durationTransformer.to('PT1H30M')).toBe('5400000000000');
|
|
25
|
+
expect(durationTransformer.from('5400000000000')).toBe('PT1H30M');
|
|
26
|
+
expect(durationTransformer.to(null)).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it.each([
|
|
30
|
+
['PT0S', 'PT0S'],
|
|
31
|
+
['PT90M', 'PT1H30M'],
|
|
32
|
+
['PT0.5S', 'PT0.5S'],
|
|
33
|
+
['PT-0.5S', 'PT-0.5S'],
|
|
34
|
+
['PT-1.5S', 'PT-1.5S'],
|
|
35
|
+
['P2DT3H', 'PT51H'],
|
|
36
|
+
['PT-90M', 'PT-1H-30M'],
|
|
37
|
+
['-PT1H', 'PT-1H'],
|
|
38
|
+
['PT1.000000001S', 'PT1.000000001S'],
|
|
39
|
+
])('should format the duration %s as %s, like Duration of Java', (text, formatted) => {
|
|
40
|
+
expect(formatDuration(parseDuration(text))).toBe(formatted);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it.each(['PT', 'P', 'P1DT', '1H', 'PTXS'])('should refuse %s, not an ISO-8601 duration', text => {
|
|
44
|
+
expect(() => parseDuration(text)).toThrow('is not an ISO-8601 duration');
|
|
45
|
+
});
|
|
46
|
+
});
|
package/generators/node-server/templates/server/src/common/database/column-transformers.ts.ejs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ValueTransformer } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The transformers of the columns whose values the database drivers do not give as the API exchanges them, the values
|
|
5
|
+
* of the JSON of JHipster Spring Boot.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A bigint or a decimal, which the PostgreSQL and MySQL drivers give as a string: a number, like the clients read it. */
|
|
9
|
+
export const numberTransformer: ValueTransformer = {
|
|
10
|
+
to: (value: number | null | undefined) => value,
|
|
11
|
+
from: (value: string | number | null) => (value === null || value === undefined ? value : Number(value)),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** A blob, which the drivers give as a Buffer: the base64 or text string the API received. */
|
|
15
|
+
export const blobTransformer: ValueTransformer = {
|
|
16
|
+
to: (value: string | null | undefined) => value,
|
|
17
|
+
from: (value: Buffer | string | null) => (Buffer.isBuffer(value) ? value.toString() : value),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const NANOS_PER_SECOND = 1_000_000_000n;
|
|
21
|
+
const ISO_DURATION =
|
|
22
|
+
/^([-+]?)P(?:([-+]?\d+)D)?(?:T(?:([-+]?\d+)H)?(?:([-+]?\d+)M)?(?:([-+]?)(\d+)(?:[.,](\d{0,9}))?S)?)?$/i;
|
|
23
|
+
|
|
24
|
+
/** The nanoseconds of an ISO-8601 duration, like Duration.parse of Java: PT1H30M, P2DT3H, PT-0.5S. */
|
|
25
|
+
export function parseDuration(text: string): bigint {
|
|
26
|
+
const match = ISO_DURATION.exec(text);
|
|
27
|
+
if (!match || /T$/i.test(text) || text.toUpperCase() === 'P') {
|
|
28
|
+
throw new Error(`${text} is not an ISO-8601 duration, like PT1H30M`);
|
|
29
|
+
}
|
|
30
|
+
const [, sign, days = '0', hours = '0', minutes = '0', secondsSign = '', seconds = '0', fraction = ''] = match;
|
|
31
|
+
const secondsNanos = BigInt(seconds) * NANOS_PER_SECOND + BigInt(fraction.padEnd(9, '0'));
|
|
32
|
+
const total =
|
|
33
|
+
((BigInt(days) * 24n + BigInt(hours)) * 60n + BigInt(minutes)) * 60n * NANOS_PER_SECOND +
|
|
34
|
+
(secondsSign === '-' ? -secondsNanos : secondsNanos);
|
|
35
|
+
return sign === '-' ? -total : total;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The ISO-8601 duration of nanoseconds, like Duration.toString of Java: PT1H30M, PT0.5S, PT-1H. */
|
|
39
|
+
export function formatDuration(nanos: bigint): string {
|
|
40
|
+
if (nanos === 0n) return 'PT0S';
|
|
41
|
+
// Like Java, the seconds rounded down and the positive nanoseconds of the second.
|
|
42
|
+
let seconds = nanos / NANOS_PER_SECOND;
|
|
43
|
+
let nanosOfSecond = nanos % NANOS_PER_SECOND;
|
|
44
|
+
if (nanosOfSecond < 0n) {
|
|
45
|
+
seconds -= 1n;
|
|
46
|
+
nanosOfSecond += NANOS_PER_SECOND;
|
|
47
|
+
}
|
|
48
|
+
const effectiveSeconds = seconds < 0n && nanosOfSecond > 0n ? seconds + 1n : seconds;
|
|
49
|
+
const hours = effectiveSeconds / 3600n;
|
|
50
|
+
const minutes = (effectiveSeconds % 3600n) / 60n;
|
|
51
|
+
const secs = effectiveSeconds % 60n;
|
|
52
|
+
let text = 'PT';
|
|
53
|
+
if (hours !== 0n) text += `${hours}H`;
|
|
54
|
+
if (minutes !== 0n) text += `${minutes}M`;
|
|
55
|
+
if (secs === 0n && nanosOfSecond === 0n && text.length > 2) return text;
|
|
56
|
+
text += seconds < 0n && nanosOfSecond > 0n && secs === 0n ? '-0' : `${secs}`;
|
|
57
|
+
if (nanosOfSecond > 0n) {
|
|
58
|
+
const digits = `${seconds < 0n ? 2n * NANOS_PER_SECOND - nanosOfSecond : nanosOfSecond + NANOS_PER_SECOND}`;
|
|
59
|
+
text += `.${digits.slice(1).replace(/0+$/, '')}`;
|
|
60
|
+
}
|
|
61
|
+
return `${text}S`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A duration, an ISO-8601 text in the API and its nanoseconds in a bigint column, like JHipster Spring Boot. */
|
|
65
|
+
export const durationTransformer: ValueTransformer = {
|
|
66
|
+
to: (value: string | null | undefined) => (value === null || value === undefined ? value : parseDuration(value).toString()),
|
|
67
|
+
from: (value: string | number | bigint | null) =>
|
|
68
|
+
value === null || value === undefined ? value : formatDuration(BigInt(value)),
|
|
69
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { databaseColumnType, databaseDatetimePrecision } from './column-types.ts';
|
|
3
|
+
|
|
4
|
+
describe('column types', () => {
|
|
5
|
+
it('should give the long blobs and texts of MySQL and MariaDB, like JHipster Spring Boot', () => {
|
|
6
|
+
expect(databaseColumnType('mysql', 'blob')).toBe('longblob');
|
|
7
|
+
expect(databaseColumnType('mariadb', 'text')).toBe('longtext');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should keep the types better-sqlite3 has and the other types', () => {
|
|
11
|
+
expect(databaseColumnType('better-sqlite3', 'blob')).toBe('blob');
|
|
12
|
+
expect(databaseColumnType('better-sqlite3', 'text')).toBe('text');
|
|
13
|
+
expect(databaseColumnType('mysql', 'varchar')).toBe('varchar');
|
|
14
|
+
expect(databaseColumnType('mysql', Date)).toBe(Date);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should give the microseconds of the instants of MySQL and MariaDB only', () => {
|
|
18
|
+
expect(databaseDatetimePrecision('mysql')).toBe(6);
|
|
19
|
+
expect(databaseDatetimePrecision('better-sqlite3')).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ColumnType } from 'typeorm';
|
|
2
|
+
|
|
3
|
+
import { config } from '../../config.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The databases with the column types of JHipster Spring Boot better-sqlite3 (dev and test) has not: the blobs and the
|
|
7
|
+
* texts up to 4 GB (longblob, longtext; blob and text keep 64 KB) and the microseconds of the instants (datetime(6)) of
|
|
8
|
+
* MySQL and MariaDB, like the blobType, clobType and datetimeType properties of its Liquibase changelogs.
|
|
9
|
+
*/
|
|
10
|
+
const LONG_TYPES_DATABASES = ['mysql', 'mariadb', 'aurora-mysql'];
|
|
11
|
+
|
|
12
|
+
const LONG_TYPES: Partial<Record<string, ColumnType>> = { blob: 'longblob', text: 'longtext' };
|
|
13
|
+
|
|
14
|
+
/** The type of a column on a database: a long blob or text on MySQL and MariaDB, the type itself on the others. */
|
|
15
|
+
export const databaseColumnType = (databaseType: string, type: ColumnType): ColumnType =>
|
|
16
|
+
(LONG_TYPES_DATABASES.includes(databaseType) && typeof type === 'string' && LONG_TYPES[type]) || type;
|
|
17
|
+
|
|
18
|
+
/** The precision of an instant column on a database: the microseconds on MySQL and MariaDB, the default on the others. */
|
|
19
|
+
export const databaseDatetimePrecision = (databaseType: string): number | undefined =>
|
|
20
|
+
LONG_TYPES_DATABASES.includes(databaseType) ? 6 : undefined;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The type of a column on the database of the active profiles, for the @Column decorators: the migrations give the same
|
|
24
|
+
* one with the driver of their connection (migrations/support/schema.ts), so the tables are the ones the entities map.
|
|
25
|
+
*/
|
|
26
|
+
export const columnType = (type: ColumnType): ColumnType => databaseColumnType(config.get('database.type'), type);
|
|
27
|
+
|
|
28
|
+
/** The precision of an instant column on the database of the active profiles, for the @Column decorators. */
|
|
29
|
+
export const datetimePrecision = (): number | undefined => databaseDatetimePrecision(config.get('database.type'));
|
package/generators/node-server/templates/server/src/common/database/pagination.entity.spec.ts.ejs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { PageRequest } from './pagination.entity.ts';
|
|
4
|
+
|
|
5
|
+
describe('PageRequest', () => {
|
|
6
|
+
it('passing default number parameters', () => {
|
|
7
|
+
const pageRequest = new PageRequest(
|
|
8
|
+
0,
|
|
9
|
+
20,
|
|
10
|
+
'id,ASC',
|
|
11
|
+
);
|
|
12
|
+
expect(pageRequest.page).toBe(0);
|
|
13
|
+
expect(pageRequest.size).toBe(20);
|
|
14
|
+
expect(pageRequest.asOrder()).toMatchObject({ id: 'ASC' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('passing default strings parameters', () => {
|
|
18
|
+
const pageRequest = new PageRequest(
|
|
19
|
+
'0',
|
|
20
|
+
'20',
|
|
21
|
+
'id,ASC',
|
|
22
|
+
);
|
|
23
|
+
expect(pageRequest.page).toBe(0);
|
|
24
|
+
expect(pageRequest.size).toBe(20);
|
|
25
|
+
expect(pageRequest.asOrder()).toMatchObject({ id: 'ASC' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('passing non default strings parameters', () => {
|
|
29
|
+
const pageRequest = new PageRequest(
|
|
30
|
+
'10',
|
|
31
|
+
'30',
|
|
32
|
+
'foo,DESC',
|
|
33
|
+
);
|
|
34
|
+
expect(pageRequest.page).toBe(10);
|
|
35
|
+
expect(pageRequest.size).toBe(30);
|
|
36
|
+
expect(pageRequest.asOrder()).toMatchObject({ foo: 'DESC' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('caps the size like Spring Data, and ignores a negative page or a size below 1', () => {
|
|
40
|
+
expect(new PageRequest(0, 100000).size).toBe(2000);
|
|
41
|
+
const pageRequest = new PageRequest(-1, 0);
|
|
42
|
+
expect(pageRequest.page).toBe(0);
|
|
43
|
+
expect(pageRequest.size).toBe(20);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('sorts on several properties, the direction case insensitive and ascending by default', () => {
|
|
47
|
+
const pageRequest = new PageRequest(0, 20, ['name,desc', 'id']);
|
|
48
|
+
expect(pageRequest.asOrder()).toEqual({ name: 'DESC', id: 'ASC' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('sorts on the properties of a relationship', () => {
|
|
52
|
+
const pageRequest = new PageRequest(0, 20, ['region.name,asc', 'region.id,desc']);
|
|
53
|
+
expect(pageRequest.asOrder()).toEqual({ region: { name: 'ASC', id: 'DESC' } });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects a property out of the sort properties', () => {
|
|
57
|
+
expect(() => new PageRequest(0, 20, 'password,asc', ['id', 'name'])).toThrow('Invalid sort property: password');
|
|
58
|
+
expect(new PageRequest(0, 20, 'name,asc', ['id', 'name']).asOrder()).toEqual({ name: 'ASC' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects a property the sort check refuses', () => {
|
|
62
|
+
const isSortable = (property: string): boolean => property !== 'password';
|
|
63
|
+
expect(() => new PageRequest(0, 20, 'password,asc', isSortable)).toThrow('Invalid sort property: password');
|
|
64
|
+
expect(new PageRequest(0, 20, 'name,asc', isSortable).asOrder()).toEqual({ name: 'ASC' });
|
|
65
|
+
});
|
|
66
|
+
});
|