refactor(core): Modularize community packages (#18641)

This commit is contained in:
Iván Ovejero
2025-08-22 12:19:01 +02:00
committed by GitHub
parent e3772c13d2
commit 9e420d15c1
43 changed files with 144 additions and 98 deletions

View File

@@ -22,7 +22,10 @@ describe('eligibleModules', () => {
it('should consider a module ineligible if it was disabled via env var', () => {
process.env.N8N_DISABLED_MODULES = 'insights';
expect(Container.get(ModuleRegistry).eligibleModules).toEqual(['external-secrets']);
expect(Container.get(ModuleRegistry).eligibleModules).toEqual([
'external-secrets',
'community-packages',
]);
});
it('should consider a module eligible if it was enabled via env var', () => {
@@ -30,6 +33,7 @@ describe('eligibleModules', () => {
expect(Container.get(ModuleRegistry).eligibleModules).toEqual([
'insights',
'external-secrets',
'community-packages',
'data-store',
]);
});

View File

@@ -26,7 +26,11 @@ export class ModuleRegistry {
private readonly modulesConfig: ModulesConfig,
) {}
private readonly defaultModules: ModuleName[] = ['insights', 'external-secrets'];
private readonly defaultModules: ModuleName[] = [
'insights',
'external-secrets',
'community-packages',
];
private readonly activeModules: string[] = [];
@@ -83,7 +87,7 @@ export class ModuleRegistry {
if (entities?.length) this.entities.push(...entities);
const loadDir = Container.get(ModuleClass).loadDir?.();
const loadDir = await Container.get(ModuleClass).loadDir?.();
if (loadDir) this.loadDirs.push(loadDir);
}

View File

@@ -2,7 +2,12 @@ import { CommaSeparatedStringArray, Config, Env } from '@n8n/config';
import { UnknownModuleError } from './errors/unknown-module.error';
export const MODULE_NAMES = ['insights', 'external-secrets', 'data-store'] as const;
export const MODULE_NAMES = [
'insights',
'external-secrets',
'community-packages',
'data-store',
] as const;
export type ModuleName = (typeof MODULE_NAMES)[number];

View File

@@ -11,8 +11,6 @@ import { ExecutionEntity } from './execution-entity';
import { ExecutionMetadata } from './execution-metadata';
import { Folder } from './folder';
import { FolderTagMapping } from './folder-tag-mapping';
import { InstalledNodes } from './installed-nodes';
import { InstalledPackages } from './installed-packages';
import { InvalidAuthToken } from './invalid-auth-token';
import { ProcessedData } from './processed-data';
import { Project } from './project';
@@ -35,8 +33,6 @@ import { WorkflowTagMapping } from './workflow-tag-mapping';
export {
EventDestinations,
InstalledNodes,
InstalledPackages,
InvalidAuthToken,
ProcessedData,
Settings,
@@ -72,8 +68,6 @@ export {
export const entities = {
EventDestinations,
InstalledNodes,
InstalledPackages,
InvalidAuthToken,
ProcessedData,
Settings,

View File

@@ -1,19 +0,0 @@
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
import { InstalledPackages } from './installed-packages';
@Entity()
export class InstalledNodes {
@Column()
name: string;
@PrimaryColumn()
type: string;
@Column()
latestVersion: number;
@ManyToOne('InstalledPackages', 'installedNodes')
@JoinColumn({ name: 'package', referencedColumnName: 'packageName' })
package: InstalledPackages;
}

View File

@@ -1,23 +0,0 @@
import { Column, Entity, JoinColumn, OneToMany, PrimaryColumn } from '@n8n/typeorm';
import { WithTimestamps } from './abstract-entity';
import type { InstalledNodes } from './installed-nodes';
@Entity()
export class InstalledPackages extends WithTimestamps {
@PrimaryColumn()
packageName: string;
@Column()
installedVersion: string;
@Column()
authorName?: string;
@Column()
authorEmail?: string;
@OneToMany('InstalledNodes', 'package')
@JoinColumn({ referencedColumnName: 'package' })
installedNodes: InstalledNodes[];
}

View File

@@ -12,8 +12,6 @@ export { EventDestinationsRepository } from './event-destinations.repository';
export { FolderRepository } from './folder.repository';
export { FolderTagMappingRepository } from './folder-tag-mapping.repository';
export { ScopeRepository } from './scope.repository';
export { InstalledNodesRepository } from './installed-nodes.repository';
export { InstalledPackagesRepository } from './installed-packages.repository';
export { InvalidAuthTokenRepository } from './invalid-auth-token.repository';
export { LicenseMetricsRepository } from './license-metrics.repository';
export { ProjectRelationRepository } from './project-relation.repository';

View File

@@ -1,11 +0,0 @@
import { Service } from '@n8n/di';
import { DataSource, Repository } from '@n8n/typeorm';
import { InstalledNodes } from '../entities';
@Service()
export class InstalledNodesRepository extends Repository<InstalledNodes> {
constructor(dataSource: DataSource) {
super(InstalledNodes, dataSource.manager);
}
}

View File

@@ -1,51 +0,0 @@
import { Service } from '@n8n/di';
import { DataSource, Repository } from '@n8n/typeorm';
import type { PackageDirectoryLoader } from 'n8n-core';
import { InstalledNodesRepository } from './installed-nodes.repository';
import { InstalledPackages } from '../entities';
@Service()
export class InstalledPackagesRepository extends Repository<InstalledPackages> {
constructor(
dataSource: DataSource,
private installedNodesRepository: InstalledNodesRepository,
) {
super(InstalledPackages, dataSource.manager);
}
async saveInstalledPackageWithNodes(packageLoader: PackageDirectoryLoader) {
const { packageJson, nodeTypes, loadedNodes } = packageLoader;
const { name: packageName, version: installedVersion, author } = packageJson;
let installedPackage: InstalledPackages;
await this.manager.transaction(async (manager) => {
installedPackage = await manager.save(
this.create({
packageName,
installedVersion,
authorName: author?.name,
authorEmail: author?.email,
}),
);
installedPackage.installedNodes = [];
for (const loadedNode of loadedNodes) {
const installedNode = this.installedNodesRepository.create({
name: nodeTypes[loadedNode.name].type.description.displayName,
type: `${packageName}.${loadedNode.name}`,
latestVersion: loadedNode.version,
package: { packageName },
});
installedPackage.installedNodes.push(installedNode);
await manager.save(installedNode);
}
});
return installedPackage!;
}
}

View File

@@ -33,10 +33,10 @@ export interface ModuleInterface {
settings?(): Promise<ModuleSettings>;
/**
* @returns Path to a dir to load nodes and credentials from.
* @returns Path to a dir to load nodes and credentials from. `null` to skip.
* @example '/Users/nathan/.n8n/nodes/node_modules'
*/
loadDir?(): string;
loadDir?(): Promise<string | null>;
}
export type ModuleClass = Constructable<ModuleInterface>;