refactor(core): Use mixins to delete redundant code between Entity classes (no-changelog) (#6616)

* db entities don't need an ID before they are inserted

* don't define constructors on entity classes, use repository.create instead

* use mixins to reduce duplicate code in db entity classes
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-07-27 11:53:37 +02:00
committed by GitHub
parent e6903a87b5
commit dc2ba743eb
16 changed files with 87 additions and 138 deletions

View File

@@ -1,6 +1,13 @@
import { BeforeUpdate, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import {
BeforeInsert,
BeforeUpdate,
CreateDateColumn,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
import { IsDate, IsOptional } from 'class-validator';
import config from '@/config';
import { generateNanoId } from '../utils/generators';
const dbType = config.getEnv('database.type');
@@ -14,26 +21,53 @@ const timestampSyntax = {
export const jsonColumnType = dbType === 'sqlite' ? 'simple-json' : 'json';
export const datetimeColumnType = dbType === 'postgresdb' ? 'timestamptz' : 'datetime';
export abstract class AbstractEntity {
@CreateDateColumn({
precision: 3,
default: () => timestampSyntax,
})
@IsOptional() // ignored by validation because set at DB level
@IsDate()
createdAt: Date;
const tsColumnOptions = {
precision: 3,
default: () => timestampSyntax,
};
@UpdateDateColumn({
precision: 3,
default: () => timestampSyntax,
onUpdate: timestampSyntax,
})
@IsOptional() // ignored by validation because set at DB level
@IsDate()
updatedAt: Date;
type Constructor<T> = new (...args: any[]) => T;
@BeforeUpdate()
setUpdateDate(): void {
this.updatedAt = new Date();
function mixinStringId<T extends Constructor<{}>>(base: T) {
class Derived extends base {
@PrimaryColumn('varchar')
id: string;
@BeforeInsert()
generateId() {
if (!this.id) {
this.id = generateNanoId();
}
}
}
return Derived;
}
function mixinTimestamps<T extends Constructor<{}>>(base: T) {
class Derived extends base {
@CreateDateColumn(tsColumnOptions)
@IsOptional() // ignored by validation because set at DB level
@IsDate()
createdAt: Date;
@UpdateDateColumn({
...tsColumnOptions,
onUpdate: timestampSyntax,
})
@IsOptional() // ignored by validation because set at DB level
@IsDate()
updatedAt: Date;
@BeforeUpdate()
setUpdateDate(): void {
this.updatedAt = new Date();
}
}
return Derived;
}
class BaseEntity {}
/* eslint-disable @typescript-eslint/naming-convention */
export const WithStringId = mixinStringId(BaseEntity);
export const WithTimestamps = mixinTimestamps(BaseEntity);
export const WithTimestampsAndStringId = mixinStringId(WithTimestamps);

View File

@@ -1,12 +1,12 @@
import { Column, Entity, ManyToOne, PrimaryColumn, Unique } from 'typeorm';
import { AbstractEntity } from './AbstractEntity';
import { WithTimestamps } from './AbstractEntity';
import { User } from './User';
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
@Entity()
@Unique(['providerId', 'providerType'])
export class AuthIdentity extends AbstractEntity {
export class AuthIdentity extends WithTimestamps {
@Column()
userId: string;

View File

@@ -1,30 +1,12 @@
import type { ICredentialNodeAccess } from 'n8n-workflow';
import { BeforeInsert, Column, Entity, Index, OneToMany, PrimaryColumn } from 'typeorm';
import { Column, Entity, Index, OneToMany } from 'typeorm';
import { IsArray, IsObject, IsString, Length } from 'class-validator';
import type { SharedCredentials } from './SharedCredentials';
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import { WithTimestampsAndStringId, jsonColumnType } from './AbstractEntity';
import type { ICredentialsDb } from '@/Interfaces';
import { generateNanoId } from '../utils/generators';
@Entity()
export class CredentialsEntity extends AbstractEntity implements ICredentialsDb {
constructor(data?: Partial<CredentialsEntity>) {
super();
Object.assign(this, data);
if (!this.id) {
this.id = generateNanoId();
}
}
@BeforeInsert()
nanoId(): void {
if (!this.id) {
this.id = generateNanoId();
}
}
@PrimaryColumn('varchar')
id: string;
export class CredentialsEntity extends WithTimestampsAndStringId implements ICredentialsDb {
@Column({ length: 128 })
@IsString({ message: 'Credential `name` must be of type string.' })
@Length(3, 128, {

View File

@@ -1,9 +1,9 @@
import { MessageEventBusDestinationOptions } from 'n8n-workflow';
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import { WithTimestamps, jsonColumnType } from './AbstractEntity';
@Entity({ name: 'event_destinations' })
export class EventDestinations extends AbstractEntity {
export class EventDestinations extends WithTimestamps {
@PrimaryColumn('uuid')
id: string;

View File

@@ -1,9 +1,9 @@
import { Column, Entity, JoinColumn, OneToMany, PrimaryColumn } from 'typeorm';
import type { InstalledNodes } from './InstalledNodes';
import { AbstractEntity } from './AbstractEntity';
import { WithTimestamps } from './AbstractEntity';
@Entity()
export class InstalledPackages extends AbstractEntity {
export class InstalledPackages extends WithTimestamps {
@PrimaryColumn()
packageName: string;

View File

@@ -4,7 +4,7 @@ import { IsString, Length } from 'class-validator';
import type { User } from './User';
import type { SharedWorkflow } from './SharedWorkflow';
import type { SharedCredentials } from './SharedCredentials';
import { AbstractEntity } from './AbstractEntity';
import { WithTimestamps } from './AbstractEntity';
import { idStringifier } from '../utils/transformers';
export type RoleNames = 'owner' | 'member' | 'user' | 'editor';
@@ -12,7 +12,7 @@ export type RoleScopes = 'global' | 'workflow' | 'credential';
@Entity()
@Unique(['scope', 'name'])
export class Role extends AbstractEntity {
export class Role extends WithTimestamps {
@PrimaryColumn({ transformer: idStringifier })
id: string;

View File

@@ -2,10 +2,10 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { CredentialsEntity } from './CredentialsEntity';
import { User } from './User';
import { Role } from './Role';
import { AbstractEntity } from './AbstractEntity';
import { WithTimestamps } from './AbstractEntity';
@Entity()
export class SharedCredentials extends AbstractEntity {
export class SharedCredentials extends WithTimestamps {
@ManyToOne('Role', 'sharedCredentials', { nullable: false })
role: Role;

View File

@@ -2,10 +2,10 @@ import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { WorkflowEntity } from './WorkflowEntity';
import { User } from './User';
import { Role } from './Role';
import { AbstractEntity } from './AbstractEntity';
import { WithTimestamps } from './AbstractEntity';
@Entity()
export class SharedWorkflow extends AbstractEntity {
export class SharedWorkflow extends WithTimestamps {
@ManyToOne('Role', 'sharedWorkflows', { nullable: false })
role: Role;

View File

@@ -1,30 +1,11 @@
import { BeforeInsert, Column, Entity, Index, ManyToMany, OneToMany, PrimaryColumn } from 'typeorm';
import { Column, Entity, Index, ManyToMany, OneToMany } from 'typeorm';
import { IsString, Length } from 'class-validator';
import type { WorkflowEntity } from './WorkflowEntity';
import type { WorkflowTagMapping } from './WorkflowTagMapping';
import { AbstractEntity } from './AbstractEntity';
import { generateNanoId } from '../utils/generators';
import { WithTimestampsAndStringId } from './AbstractEntity';
@Entity()
export class TagEntity extends AbstractEntity {
constructor(data?: Partial<TagEntity>) {
super();
Object.assign(this, data);
if (!this.id) {
this.id = generateNanoId();
}
}
@BeforeInsert()
nanoId() {
if (!this.id) {
this.id = generateNanoId();
}
}
@PrimaryColumn('varchar')
id: string;
export class TagEntity extends WithTimestampsAndStringId {
@Column({ length: 24 })
@Index({ unique: true })
@IsString({ message: 'Tag name must be of type string.' })

View File

@@ -17,7 +17,7 @@ import type { SharedWorkflow } from './SharedWorkflow';
import type { SharedCredentials } from './SharedCredentials';
import { NoXss } from '../utils/customValidators';
import { objectRetriever, lowerCaser } from '../utils/transformers';
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import { WithTimestamps, jsonColumnType } from './AbstractEntity';
import type { IPersonalizationSurveyAnswers } from '@/Interfaces';
import type { AuthIdentity } from './AuthIdentity';
@@ -26,7 +26,7 @@ export const MIN_PASSWORD_LENGTH = 8;
export const MAX_PASSWORD_LENGTH = 64;
@Entity()
export class User extends AbstractEntity implements IUser {
export class User extends WithTimestamps implements IUser {
@PrimaryGeneratedColumn('uuid')
id: string;

View File

@@ -1,25 +1,8 @@
import { BeforeInsert, Column, Entity, PrimaryColumn } from 'typeorm';
import { generateNanoId } from '../utils/generators';
import { Column, Entity } from 'typeorm';
import { WithStringId } from './AbstractEntity';
@Entity()
export class Variables {
constructor(data?: Partial<Variables>) {
Object.assign(this, data);
if (!this.id) {
this.id = generateNanoId();
}
}
@BeforeInsert()
nanoId() {
if (!this.id) {
this.id = generateNanoId();
}
}
@PrimaryColumn('varchar')
id: string;
export class Variables extends WithStringId {
@Column('text')
key: string;

View File

@@ -3,17 +3,7 @@ import { Length } from 'class-validator';
import { IConnections, IDataObject, IWorkflowSettings } from 'n8n-workflow';
import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow';
import {
BeforeInsert,
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
OneToMany,
PrimaryColumn,
} from 'typeorm';
import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, OneToMany } from 'typeorm';
import config from '@/config';
import type { TagEntity } from './TagEntity';
@@ -21,30 +11,11 @@ import type { SharedWorkflow } from './SharedWorkflow';
import type { WorkflowStatistics } from './WorkflowStatistics';
import type { WorkflowTagMapping } from './WorkflowTagMapping';
import { objectRetriever, sqlite } from '../utils/transformers';
import { AbstractEntity, jsonColumnType } from './AbstractEntity';
import { WithTimestampsAndStringId, jsonColumnType } from './AbstractEntity';
import type { IWorkflowDb } from '@/Interfaces';
import { generateNanoId } from '../utils/generators';
@Entity()
export class WorkflowEntity extends AbstractEntity implements IWorkflowDb {
constructor(data?: Partial<WorkflowEntity>) {
super();
Object.assign(this, data);
if (!this.id) {
this.id = generateNanoId();
}
}
@BeforeInsert()
nanoId() {
if (!this.id) {
this.id = generateNanoId();
}
}
@PrimaryColumn('varchar')
id: string;
export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkflowDb {
// TODO: Add XSS check
@Index({ unique: true })
@Length(1, 128, {