diff --git a/packages/cli/src/databases/entities/AbstractEntity.ts b/packages/cli/src/databases/entities/AbstractEntity.ts index e12933572c..9125694619 100644 --- a/packages/cli/src/databases/entities/AbstractEntity.ts +++ b/packages/cli/src/databases/entities/AbstractEntity.ts @@ -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 = new (...args: any[]) => T; - @BeforeUpdate() - setUpdateDate(): void { - this.updatedAt = new Date(); +function mixinStringId>(base: T) { + class Derived extends base { + @PrimaryColumn('varchar') + id: string; + + @BeforeInsert() + generateId() { + if (!this.id) { + this.id = generateNanoId(); + } + } } + return Derived; } + +function mixinTimestamps>(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); diff --git a/packages/cli/src/databases/entities/AuthIdentity.ts b/packages/cli/src/databases/entities/AuthIdentity.ts index 7069206287..47289c9306 100644 --- a/packages/cli/src/databases/entities/AuthIdentity.ts +++ b/packages/cli/src/databases/entities/AuthIdentity.ts @@ -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; diff --git a/packages/cli/src/databases/entities/CredentialsEntity.ts b/packages/cli/src/databases/entities/CredentialsEntity.ts index f9cb8fa49f..23a2e70760 100644 --- a/packages/cli/src/databases/entities/CredentialsEntity.ts +++ b/packages/cli/src/databases/entities/CredentialsEntity.ts @@ -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) { - 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, { diff --git a/packages/cli/src/databases/entities/EventDestinations.ts b/packages/cli/src/databases/entities/EventDestinations.ts index 8f3452936c..b302e9a1a8 100644 --- a/packages/cli/src/databases/entities/EventDestinations.ts +++ b/packages/cli/src/databases/entities/EventDestinations.ts @@ -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; diff --git a/packages/cli/src/databases/entities/InstalledPackages.ts b/packages/cli/src/databases/entities/InstalledPackages.ts index ec406ca5d6..c0c06a00da 100644 --- a/packages/cli/src/databases/entities/InstalledPackages.ts +++ b/packages/cli/src/databases/entities/InstalledPackages.ts @@ -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; diff --git a/packages/cli/src/databases/entities/Role.ts b/packages/cli/src/databases/entities/Role.ts index b1943866d4..b3dc3c3d0a 100644 --- a/packages/cli/src/databases/entities/Role.ts +++ b/packages/cli/src/databases/entities/Role.ts @@ -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; diff --git a/packages/cli/src/databases/entities/SharedCredentials.ts b/packages/cli/src/databases/entities/SharedCredentials.ts index bda9006125..6686e5a3c4 100644 --- a/packages/cli/src/databases/entities/SharedCredentials.ts +++ b/packages/cli/src/databases/entities/SharedCredentials.ts @@ -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; diff --git a/packages/cli/src/databases/entities/SharedWorkflow.ts b/packages/cli/src/databases/entities/SharedWorkflow.ts index 12b9dd1def..4b181e5ab1 100644 --- a/packages/cli/src/databases/entities/SharedWorkflow.ts +++ b/packages/cli/src/databases/entities/SharedWorkflow.ts @@ -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; diff --git a/packages/cli/src/databases/entities/TagEntity.ts b/packages/cli/src/databases/entities/TagEntity.ts index 8e9f518f22..c425b505ba 100644 --- a/packages/cli/src/databases/entities/TagEntity.ts +++ b/packages/cli/src/databases/entities/TagEntity.ts @@ -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) { - 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.' }) diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 029275902f..15370aade6 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -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; diff --git a/packages/cli/src/databases/entities/Variables.ts b/packages/cli/src/databases/entities/Variables.ts index 6564da2fd0..42f52c4ace 100644 --- a/packages/cli/src/databases/entities/Variables.ts +++ b/packages/cli/src/databases/entities/Variables.ts @@ -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) { - 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; diff --git a/packages/cli/src/databases/entities/WorkflowEntity.ts b/packages/cli/src/databases/entities/WorkflowEntity.ts index e40c1e1b7d..0bbb70ccf2 100644 --- a/packages/cli/src/databases/entities/WorkflowEntity.ts +++ b/packages/cli/src/databases/entities/WorkflowEntity.ts @@ -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) { - 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, { diff --git a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts index e7126350b2..c99363cacf 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts @@ -14,7 +14,7 @@ import { readFile as fsReadFile } from 'fs/promises'; import { Credentials, UserSettings } from 'n8n-core'; import type { IWorkflowToImport } from '@/Interfaces'; import type { ExportableCredential } from './types/exportableCredential'; -import { Variables } from '@db/entities/Variables'; +import type { Variables } from '@db/entities/Variables'; import { UM_FIX_INSTRUCTION } from '@/commands/BaseCommand'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import type { WorkflowTagMapping } from '@db/entities/WorkflowTagMapping'; @@ -576,7 +576,7 @@ export class SourceControlImportService { if (overriddenKeys.length > 0 && valueOverrides) { for (const key of overriddenKeys) { result.imported.push(key); - const newVariable = new Variables({ key, value: valueOverrides[key] }); + const newVariable = Db.collections.Variables.create({ key, value: valueOverrides[key] }); await Db.collections.Variables.save(newVariable); } } diff --git a/packages/cli/test/integration/audit/filesystem.risk.test.ts b/packages/cli/test/integration/audit/filesystem.risk.test.ts index 8418dbc102..d8f3e711e7 100644 --- a/packages/cli/test/integration/audit/filesystem.risk.test.ts +++ b/packages/cli/test/integration/audit/filesystem.risk.test.ts @@ -4,7 +4,6 @@ import { audit } from '@/audit'; import { FILESYSTEM_INTERACTION_NODE_TYPES, FILESYSTEM_REPORT } from '@/audit/constants'; import { getRiskSection, saveManualTriggerWorkflow } from './utils'; import * as testDb from '../shared/testDb'; -import { WorkflowEntity } from '@db/entities/WorkflowEntity'; beforeAll(async () => { await testDb.init(); @@ -27,7 +26,7 @@ test('should report filesystem interaction nodes', async () => { ); const promises = Object.entries(map).map(async ([nodeType, nodeId]) => { - const details = new WorkflowEntity({ + const details = Db.collections.Workflow.create({ name: 'My Test Workflow', active: false, connections: {}, diff --git a/packages/cli/test/integration/audit/nodes.risk.test.ts b/packages/cli/test/integration/audit/nodes.risk.test.ts index 884594cfe0..49fb65bbc0 100644 --- a/packages/cli/test/integration/audit/nodes.risk.test.ts +++ b/packages/cli/test/integration/audit/nodes.risk.test.ts @@ -9,7 +9,6 @@ import { toReportTitle } from '@/audit/utils'; import { mockInstance } from '../shared/utils/'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { NodeTypes } from '@/NodeTypes'; -import { WorkflowEntity } from '@db/entities/WorkflowEntity'; const nodesAndCredentials = mockInstance(LoadNodesAndCredentials); nodesAndCredentials.getCustomDirectories.mockReturnValue([]); @@ -33,7 +32,7 @@ test('should report risky official nodes', async () => { }, {}); const promises = Object.entries(map).map(async ([nodeType, nodeId]) => { - const details = new WorkflowEntity({ + const details = Db.collections.Workflow.create({ name: 'My Test Workflow', active: false, connections: {}, diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 92fb56ea32..55a042d282 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -19,7 +19,7 @@ import { InstalledPackages } from '@db/entities/InstalledPackages'; import type { Role } from '@db/entities/Role'; import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; -import { WorkflowEntity } from '@db/entities/WorkflowEntity'; +import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { RoleRepository } from '@db/repositories'; import type { ICredentialsDb } from '@/Interfaces'; @@ -412,7 +412,7 @@ export async function createManyWorkflows( export async function createWorkflow(attributes: Partial = {}, user?: User) { const { active, name, nodes, connections } = attributes; - const workflowEntity = new WorkflowEntity({ + const workflowEntity = Db.collections.Workflow.create({ active: active ?? false, name: name ?? 'test workflow', nodes: nodes ?? [