diff --git a/packages/@n8n/api-types/src/dto/project/__tests__/create-project.dto.test.ts b/packages/@n8n/api-types/src/dto/project/__tests__/create-project.dto.test.ts index 42eb553030..570982e375 100644 --- a/packages/@n8n/api-types/src/dto/project/__tests__/create-project.dto.test.ts +++ b/packages/@n8n/api-types/src/dto/project/__tests__/create-project.dto.test.ts @@ -13,9 +13,11 @@ describe('CreateProjectDto', () => { name: 'with name and emoji icon', request: { name: 'My Awesome Project', - icon: { - type: 'emoji', - value: '🚀', + settings: { + icon: { + type: 'emoji', + value: '🚀', + }, }, }, }, @@ -23,9 +25,11 @@ describe('CreateProjectDto', () => { name: 'with name and regular icon', request: { name: 'My Awesome Project', - icon: { - type: 'icon', - value: 'blah', + settings: { + icon: { + type: 'icon', + value: 'blah', + }, }, }, }, @@ -39,28 +43,31 @@ describe('CreateProjectDto', () => { test.each([ { name: 'missing name', - request: { icon: { type: 'emoji', value: '🚀' } }, + request: { settings: { icon: { type: 'emoji', value: '🚀' } } }, expectedErrorPath: ['name'], }, { name: 'empty name', - request: { name: '', icon: { type: 'emoji', value: '🚀' } }, + request: { name: '', settings: { icon: { type: 'emoji', value: '🚀' } } }, expectedErrorPath: ['name'], }, { name: 'name too long', - request: { name: 'a'.repeat(256), icon: { type: 'emoji', value: '🚀' } }, + request: { name: 'a'.repeat(256), settings: { icon: { type: 'emoji', value: '🚀' } } }, expectedErrorPath: ['name'], }, { name: 'invalid icon type', - request: { name: 'My Awesome Project', icon: { type: 'invalid', value: '🚀' } }, - expectedErrorPath: ['icon', 'type'], + request: { + name: 'My Awesome Project', + settings: { icon: { type: 'invalid', value: '🚀' } }, + }, + expectedErrorPath: ['settings', 'icon', 'type'], }, { name: 'invalid icon value', - request: { name: 'My Awesome Project', icon: { type: 'emoji', value: '' } }, - expectedErrorPath: ['icon', 'value'], + request: { name: 'My Awesome Project', settings: { icon: { type: 'emoji', value: '' } } }, + expectedErrorPath: ['settings', 'icon', 'value'], }, ])('should fail validation for $name', ({ request, expectedErrorPath }) => { const result = CreateProjectDto.safeParse(request); diff --git a/packages/@n8n/api-types/src/dto/project/__tests__/update-project.dto.test.ts b/packages/@n8n/api-types/src/dto/project/__tests__/update-project.dto.test.ts index 86a8630026..72779101d2 100644 --- a/packages/@n8n/api-types/src/dto/project/__tests__/update-project.dto.test.ts +++ b/packages/@n8n/api-types/src/dto/project/__tests__/update-project.dto.test.ts @@ -13,9 +13,11 @@ describe('UpdateProjectDto', () => { name: 'with name and emoji icon', request: { name: 'My Updated Project', - icon: { - type: 'emoji', - value: '🚀', + settings: { + icon: { + type: 'emoji', + value: '🚀', + }, }, }, }, @@ -23,9 +25,11 @@ describe('UpdateProjectDto', () => { name: 'with name and regular icon', request: { name: 'My Updated Project', - icon: { - type: 'icon', - value: 'blah', + settings: { + icon: { + type: 'icon', + value: 'blah', + }, }, }, }, @@ -44,9 +48,11 @@ describe('UpdateProjectDto', () => { name: 'with all fields', request: { name: 'My Updated Project', - icon: { - type: 'emoji', - value: '🚀', + settings: { + icon: { + type: 'emoji', + value: '🚀', + }, }, relations: [ { @@ -76,13 +82,13 @@ describe('UpdateProjectDto', () => { }, { name: 'invalid icon type', - request: { icon: { type: 'invalid', value: '🚀' } }, - expectedErrorPath: ['icon', 'type'], + request: { settings: { icon: { type: 'invalid', value: '🚀' } } }, + expectedErrorPath: ['settings', 'icon', 'type'], }, { name: 'invalid icon value', - request: { icon: { type: 'emoji', value: '' } }, - expectedErrorPath: ['icon', 'value'], + request: { settings: { icon: { type: 'emoji', value: '' } } }, + expectedErrorPath: ['settings', 'icon', 'value'], }, { name: 'invalid relations userId', diff --git a/packages/@n8n/api-types/src/dto/project/create-project.dto.ts b/packages/@n8n/api-types/src/dto/project/create-project.dto.ts index cf748f5e13..2ca6b8dac2 100644 --- a/packages/@n8n/api-types/src/dto/project/create-project.dto.ts +++ b/packages/@n8n/api-types/src/dto/project/create-project.dto.ts @@ -1,8 +1,8 @@ import { Z } from 'zod-class'; -import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema'; +import { projectNameSchema, projectSettingsSchema } from '../../schemas/project.schema'; export class CreateProjectDto extends Z.class({ name: projectNameSchema, - icon: projectIconSchema.optional(), + settings: projectSettingsSchema.optional(), }) {} diff --git a/packages/@n8n/api-types/src/dto/project/update-project.dto.ts b/packages/@n8n/api-types/src/dto/project/update-project.dto.ts index b167ed88d3..bae0ef67f6 100644 --- a/packages/@n8n/api-types/src/dto/project/update-project.dto.ts +++ b/packages/@n8n/api-types/src/dto/project/update-project.dto.ts @@ -2,13 +2,13 @@ import { z } from 'zod'; import { Z } from 'zod-class'; import { - projectIconSchema, projectNameSchema, + projectSettingsSchema, projectRelationSchema, } from '../../schemas/project.schema'; export class UpdateProjectDto extends Z.class({ name: projectNameSchema.optional(), - icon: projectIconSchema.optional(), + settings: projectSettingsSchema.optional(), relations: z.array(projectRelationSchema).optional(), }) {} diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index a51850cc6c..caee63f159 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -15,6 +15,7 @@ export { passwordSchema } from './schemas/password.schema'; export type { ProjectType, ProjectIcon, + ProjectSettings, ProjectRole, ProjectRelation, } from './schemas/project.schema'; diff --git a/packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts b/packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts index 9a1cf47414..0260049aa7 100644 --- a/packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts +++ b/packages/@n8n/api-types/src/schemas/__tests__/project.schema.test.ts @@ -1,9 +1,9 @@ import { projectNameSchema, projectTypeSchema, - projectIconSchema, projectRoleSchema, projectRelationSchema, + projectSettingsSchema, } from '../project.schema'; describe('project.schema', () => { @@ -29,31 +29,33 @@ describe('project.schema', () => { }); }); - describe('projectIconSchema', () => { - test.each([ - { - name: 'valid emoji icon', - value: { type: 'emoji', value: '🚀' }, - expected: true, - }, - { - name: 'valid icon', - value: { type: 'icon', value: 'blah' }, - expected: true, - }, - { - name: 'invalid icon type', - value: { type: 'invalid', value: '🚀' }, - expected: false, - }, - { - name: 'empty icon value', - value: { type: 'emoji', value: '' }, - expected: false, - }, - ])('should validate $name', ({ value, expected }) => { - const result = projectIconSchema.safeParse(value); - expect(result.success).toBe(expected); + describe('projectSettingsSchema', () => { + describe('.icon', () => { + test.each([ + { + name: 'valid emoji icon', + value: { type: 'emoji', value: '🚀' }, + expected: true, + }, + { + name: 'valid icon', + value: { type: 'icon', value: 'blah' }, + expected: true, + }, + { + name: 'invalid icon type', + value: { type: 'invalid', value: '🚀' }, + expected: false, + }, + { + name: 'empty icon value', + value: { type: 'emoji', value: '' }, + expected: false, + }, + ])('should validate $name', ({ value, expected }) => { + const result = projectSettingsSchema.safeParse({ icon: value }); + expect(result.success).toBe(expected); + }); }); }); diff --git a/packages/@n8n/api-types/src/schemas/project.schema.ts b/packages/@n8n/api-types/src/schemas/project.schema.ts index 11c6cc2b37..fe644aadb1 100644 --- a/packages/@n8n/api-types/src/schemas/project.schema.ts +++ b/packages/@n8n/api-types/src/schemas/project.schema.ts @@ -5,12 +5,20 @@ export const projectNameSchema = z.string().min(1).max(255); export const projectTypeSchema = z.enum(['personal', 'team']); export type ProjectType = z.infer; -export const projectIconSchema = z.object({ +const projectIconSchema = z.object({ type: z.enum(['emoji', 'icon']), value: z.string().min(1), }); export type ProjectIcon = z.infer; +export const projectSettingsSchema = z.object({ + icon: projectIconSchema.optional(), + dedicatedQueue: z.boolean().default(false).optional(), + // TODO: add timezone, callerPolicy, saving options, etc + // TODO: add 'workflowsFromSameProject` to `CallerPolicy` +}); +export type ProjectSettings = z.infer; + export const projectRoleSchema = z.enum([ 'project:personalOwner', // personalOwner is only used for personal projects 'project:admin', diff --git a/packages/cli/src/controllers/project.controller.ts b/packages/cli/src/controllers/project.controller.ts index 665eba9636..4a8256413d 100644 --- a/packages/cli/src/controllers/project.controller.ts +++ b/packages/cli/src/controllers/project.controller.ts @@ -171,7 +171,7 @@ export class ProjectController { _res: Response, @Param('projectId') projectId: string, ): Promise { - const [{ id, name, icon, type }, relations] = await Promise.all([ + const [{ id, name, type, settings }, relations] = await Promise.all([ this.projectsService.getProject(projectId), this.projectsService.getProjectRelations(projectId), ]); @@ -180,8 +180,8 @@ export class ProjectController { return { id, name, - icon, type, + settings, relations: relations.map((r) => ({ id: r.user.id, email: r.user.email, @@ -206,9 +206,9 @@ export class ProjectController { @Body payload: UpdateProjectDto, @Param('projectId') projectId: string, ) { - const { name, icon, relations } = payload; - if (name || icon) { - await this.projectsService.updateProject(projectId, { name, icon }); + const { name, settings, relations } = payload; + if (name || settings) { + await this.projectsService.updateProject(projectId, { name, settings }); } if (relations) { try { diff --git a/packages/cli/src/databases/dsl/column.ts b/packages/cli/src/databases/dsl/column.ts index 95ff31c200..05f3ae6eb3 100644 --- a/packages/cli/src/databases/dsl/column.ts +++ b/packages/cli/src/databases/dsl/column.ts @@ -129,6 +129,12 @@ export class Column { options.default = isSqlite ? "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')" : 'CURRENT_TIMESTAMP(3)'; + } + if (type === 'json' && isSqlite) { + if (typeof this.defaultValue !== 'string') { + this.defaultValue = JSON.stringify(this.defaultValue); + } + options.default = `'${this.defaultValue as string}'`; } else { options.default = this.defaultValue; } diff --git a/packages/cli/src/databases/entities/project.ts b/packages/cli/src/databases/entities/project.ts index 48f86fe0e9..9380b9c91f 100644 --- a/packages/cli/src/databases/entities/project.ts +++ b/packages/cli/src/databases/entities/project.ts @@ -1,7 +1,7 @@ -import { ProjectIcon, ProjectType } from '@n8n/api-types'; +import { ProjectSettings, ProjectType } from '@n8n/api-types'; import { Column, Entity, OneToMany } from '@n8n/typeorm'; -import { WithTimestampsAndStringId } from './abstract-entity'; +import { jsonColumnType, WithTimestampsAndStringId } from './abstract-entity'; import type { ProjectRelation } from './project-relation'; import type { SharedCredentials } from './shared-credentials'; import type { SharedWorkflow } from './shared-workflow'; @@ -14,8 +14,8 @@ export class Project extends WithTimestampsAndStringId { @Column({ type: 'varchar', length: 36 }) type: ProjectType; - @Column({ type: 'json', nullable: true }) - icon: ProjectIcon; + @Column({ type: jsonColumnType }) + settings: ProjectSettings = {}; @OneToMany('ProjectRelation', 'project') projectRelations: ProjectRelation[]; diff --git a/packages/cli/src/databases/migrations/common/1738496517619-AddProjectSettings.ts b/packages/cli/src/databases/migrations/common/1738496517619-AddProjectSettings.ts new file mode 100644 index 0000000000..0d3d9174c9 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1738496517619-AddProjectSettings.ts @@ -0,0 +1,14 @@ +import type { MigrationContext, ReversibleMigration } from '@/databases/types'; + +export class AddProjectSettings1738496517619 implements ReversibleMigration { + async up({ schemaBuilder: { addColumns, dropColumns, column } }: MigrationContext) { + // TODO: copy over the icons data before dropping that column + await addColumns('project', [column('settings').json.notNull.default({})]); + await dropColumns('project', ['icon']); + } + + async down({ schemaBuilder: { addColumns, dropColumns, column } }: MigrationContext) { + await addColumns('project', [column('icon').json]); + await dropColumns('project', ['settings']); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 32b1dd9751..f5b29ea4f3 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -78,6 +78,7 @@ import { AddMockedNodesColumnToTestDefinition1733133775640 } from '../common/173 import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/1734479635324-AddManagedColumnToCredentialsTable'; import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun'; import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable'; +import { AddProjectSettings1738496517619 } from '../common/1738496517619-AddProjectSettings'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -158,4 +159,5 @@ export const mysqlMigrations: Migration[] = [ AddProjectIcons1729607673469, AddStatsColumnsToTestRun1736172058779, CreateTestCaseExecutionTable1736947513045, + AddProjectSettings1738496517619, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index c5547271b7..a1ea898793 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -78,6 +78,7 @@ import { AddMockedNodesColumnToTestDefinition1733133775640 } from '../common/173 import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/1734479635324-AddManagedColumnToCredentialsTable'; import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun'; import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable'; +import { AddProjectSettings1738496517619 } from '../common/1738496517619-AddProjectSettings'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -158,4 +159,5 @@ export const postgresMigrations: Migration[] = [ AddProjectIcons1729607673469, AddStatsColumnsToTestRun1736172058779, CreateTestCaseExecutionTable1736947513045, + AddProjectSettings1738496517619, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1738496517619-AddProjectSettings.ts b/packages/cli/src/databases/migrations/sqlite/1738496517619-AddProjectSettings.ts new file mode 100644 index 0000000000..31766f6030 --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1738496517619-AddProjectSettings.ts @@ -0,0 +1,5 @@ +import { AddProjectSettings1738496517619 as BaseMigration } from '../common/1738496517619-AddProjectSettings'; + +export class AddProjectSettings1738496517619 extends BaseMigration { + transaction = false as const; +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index b8b8e26d3d..4a4a3c545e 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -42,6 +42,7 @@ import { AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644 } from './17286 import { AddProjectIcons1729607673469 } from './1729607673469-AddProjectIcons'; import { AddDescriptionToTestDefinition1731404028106 } from './1731404028106-AddDescriptionToTestDefinition'; import { MigrateTestDefinitionKeyToString1731582748663 } from './1731582748663-MigrateTestDefinitionKeyToString'; +import { AddProjectSettings1738496517619 } from './1738496517619-AddProjectSettings'; import { UniqueWorkflowNames1620821879465 } from '../common/1620821879465-UniqueWorkflowNames'; import { UpdateWorkflowCredentials1630330987096 } from '../common/1630330987096-UpdateWorkflowCredentials'; import { AddNodeIds1658930531669 } from '../common/1658930531669-AddNodeIds'; @@ -152,6 +153,7 @@ const sqliteMigrations: Migration[] = [ AddProjectIcons1729607673469, AddStatsColumnsToTestRun1736172058779, CreateTestCaseExecutionTable1736947513045, + AddProjectSettings1738496517619, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 9c26f740bb..a1ec603bc6 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -1,4 +1,4 @@ -import type { ProjectIcon, ProjectRole, ProjectType } from '@n8n/api-types'; +import type { ProjectRole, ProjectType, ProjectSettings } from '@n8n/api-types'; import type { Scope } from '@n8n/permissions'; import type express from 'express'; import type { @@ -119,7 +119,7 @@ export namespace ListQuery { } type SlimUser = Pick; -export type SlimProject = Pick; +export type SlimProject = Pick; export function hasSharing( workflows: ListQuery.Workflow.Plain[] | ListQuery.Workflow.WithSharing[], @@ -383,8 +383,8 @@ export declare namespace ProjectRequest { type ProjectWithRelations = { id: string; name: string | undefined; - icon: ProjectIcon; type: ProjectType; + settings: ProjectSettings; relations: ProjectRelationResponse[]; scopes: Scope[]; }; diff --git a/packages/cli/src/scaling/scaling.service.ts b/packages/cli/src/scaling/scaling.service.ts index 7c48ce57e2..c20cb98a61 100644 --- a/packages/cli/src/scaling/scaling.service.ts +++ b/packages/cli/src/scaling/scaling.service.ts @@ -37,7 +37,9 @@ import type { @Service() export class ScalingService { - private queue: JobQueue; + private sharedQueue: JobQueue; + + private readonly dedicatedQueues = new Map(); constructor( private readonly logger: Logger, @@ -63,7 +65,7 @@ export class ScalingService { const bullPrefix = this.globalConfig.queue.bull.prefix; const prefix = service.toValidPrefix(bullPrefix); - this.queue = new BullQueue(QUEUE_NAME, { + this.sharedQueue = new BullQueue(QUEUE_NAME, { prefix, settings: this.globalConfig.queue.bull.settings, createClient: (type) => service.createClient({ type: `${type}(bull)` }), @@ -90,7 +92,7 @@ export class ScalingService { this.assertWorker(); this.assertQueue(); - void this.queue.process(JOB_TYPE_NAME, concurrency, async (job: Job) => { + void this.sharedQueue.process(JOB_TYPE_NAME, concurrency, async (job: Job) => { try { if (!this.hasValidJobData(job)) { throw new ApplicationError('Worker received invalid job', { @@ -141,7 +143,7 @@ export class ScalingService { private async stopMain() { if (this.instanceSettings.isSingleMain) { - await this.queue.pause(true, true); // no more jobs will be picked up + await this.sharedQueue.pause(true, true); // no more jobs will be picked up this.logger.debug('Queue paused'); } @@ -164,7 +166,7 @@ export class ScalingService { } async pingQueue() { - await this.queue.client.ping(); + await this.sharedQueue.client.ping(); } // #endregion @@ -172,7 +174,7 @@ export class ScalingService { // #region Jobs async getPendingJobCounts() { - const { active, waiting } = await this.queue.getJobCounts(); + const { active, waiting } = await this.sharedQueue.getJobCounts(); return { active, waiting }; } @@ -192,7 +194,9 @@ export class ScalingService { removeOnFail: true, }; - const job = await this.queue.add(JOB_TYPE_NAME, jobData, jobOptions); + const queue = this.dedicatedQueues.get(jobData.projectId!) ?? this.sharedQueue; + + const job = await queue.add(JOB_TYPE_NAME, jobData, jobOptions); const { executionId } = jobData; const jobId = job.id; @@ -203,11 +207,15 @@ export class ScalingService { } async getJob(jobId: JobId) { - return await this.queue.getJob(jobId); + // TODO: keep a jobId -> projectId mapping in redis + // TODO: use the project specific queue if available + return await this.sharedQueue.getJob(jobId); } async findJobsByStatus(statuses: JobStatus[]) { - const jobs = await this.queue.getJobs(statuses); + // TODO: keep a jobId -> projectId mapping in redis + // TODO: use the project specific queue if available + const jobs = await this.sharedQueue.getJobs(statuses); return jobs.filter((job) => job !== null); } @@ -261,13 +269,13 @@ export class ScalingService { * Register listeners on a `worker` process for Bull queue events. */ private registerWorkerListeners() { - this.queue.on('global:progress', (jobId: JobId, msg: unknown) => { + this.sharedQueue.on('global:progress', (jobId: JobId, msg: unknown) => { if (!this.isJobMessage(msg)) return; if (msg.kind === 'abort-job') this.jobProcessor.stopJob(jobId); }); - this.queue.on('error', (error: Error) => { + this.sharedQueue.on('error', (error: Error) => { if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by RedisClientService.retryStrategy /** @@ -290,7 +298,7 @@ export class ScalingService { * Register listeners on a `main` or `webhook` process for Bull queue events. */ private registerMainOrWebhookListeners() { - this.queue.on('error', (error: Error) => { + this.sharedQueue.on('error', (error: Error) => { if ('code' in error && error.code === 'ECONNREFUSED') return; // handled by RedisClientService.retryStrategy this.logger.error('Queue errored', { error }); @@ -298,7 +306,7 @@ export class ScalingService { throw error; }); - this.queue.on('global:progress', (jobId: JobId, msg: unknown) => { + this.sharedQueue.on('global:progress', (jobId: JobId, msg: unknown) => { if (!this.isJobMessage(msg)) return; // completion and failure are reported via `global:progress` to convey more details @@ -338,8 +346,8 @@ export class ScalingService { }); if (this.isQueueMetricsEnabled) { - this.queue.on('global:completed', () => this.jobCounters.completed++); - this.queue.on('global:failed', () => this.jobCounters.failed++); + this.sharedQueue.on('global:completed', () => this.jobCounters.completed++); + this.sharedQueue.on('global:failed', () => this.jobCounters.failed++); } } @@ -367,7 +375,7 @@ export class ScalingService { } private assertQueue() { - if (this.queue) return; + if (this.sharedQueue) return; throw new ApplicationError('This method must be called after `setupQueue`'); } diff --git a/packages/cli/src/scaling/scaling.types.ts b/packages/cli/src/scaling/scaling.types.ts index 3c69294172..c0cb985f27 100644 --- a/packages/cli/src/scaling/scaling.types.ts +++ b/packages/cli/src/scaling/scaling.types.ts @@ -11,6 +11,7 @@ export type JobId = Job['id']; export type JobData = { executionId: string; + projectId?: string; loadStaticData: boolean; pushRef?: string; }; diff --git a/packages/cli/src/services/ownership.service.ts b/packages/cli/src/services/ownership.service.ts index 21ce4b573a..7d615392f0 100644 --- a/packages/cli/src/services/ownership.service.ts +++ b/packages/cli/src/services/ownership.service.ts @@ -81,21 +81,17 @@ export class OwnershipService { for (const sharedEntity of shared) { const { project, role } = sharedEntity; + const slimProject = { + id: project.id, + type: project.type, + name: project.name, + settings: project.settings, + }; if (role === 'credential:owner' || role === 'workflow:owner') { - entity.homeProject = { - id: project.id, - type: project.type, - name: project.name, - icon: project.icon, - }; + entity.homeProject = slimProject; } else { - entity.sharedWithProjects.push({ - id: project.id, - type: project.type, - name: project.name, - icon: project.icon, - }); + entity.sharedWithProjects.push(slimProject); } } diff --git a/packages/cli/src/services/project.service.ee.ts b/packages/cli/src/services/project.service.ee.ts index 9eb3f8efa1..d734620a5d 100644 --- a/packages/cli/src/services/project.service.ee.ts +++ b/packages/cli/src/services/project.service.ee.ts @@ -188,7 +188,7 @@ export class ProjectService { async updateProject( projectId: string, - data: Pick, + data: Pick, ): Promise { const result = await this.projectRepository.update({ id: projectId, type: 'team' }, data); diff --git a/packages/cli/src/workflow-runner.ts b/packages/cli/src/workflow-runner.ts index da48255faa..9b270dccdc 100644 --- a/packages/cli/src/workflow-runner.ts +++ b/packages/cli/src/workflow-runner.ts @@ -344,6 +344,7 @@ export class WorkflowRunner { ): Promise { const jobData: JobData = { executionId, + projectId: data.projectId, loadStaticData: !!loadStaticData, pushRef: data.pushRef, }; diff --git a/packages/editor-ui/src/__tests__/data/projects.ts b/packages/editor-ui/src/__tests__/data/projects.ts index d7c26b245e..1875326284 100644 --- a/packages/editor-ui/src/__tests__/data/projects.ts +++ b/packages/editor-ui/src/__tests__/data/projects.ts @@ -10,7 +10,9 @@ import { ProjectTypes } from '@/types/projects.types'; export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({ id: faker.string.uuid(), name: faker.lorem.words({ min: 1, max: 3 }), - icon: { type: 'icon', value: 'folder' }, + settings: { + icon: { type: 'icon', value: 'folder' }, + }, type: projectType ?? ProjectTypes.Personal, createdAt: faker.date.past().toISOString(), updatedAt: faker.date.recent().toISOString(), @@ -30,7 +32,9 @@ export function createTestProject(data: Partial): Project { return { id: faker.string.uuid(), name: faker.lorem.words({ min: 1, max: 3 }), - icon: { type: 'icon', value: 'folder' }, + settings: { + icon: { type: 'icon', value: 'folder' }, + }, createdAt: faker.date.past().toISOString(), updatedAt: faker.date.recent().toISOString(), type: ProjectTypes.Team, diff --git a/packages/editor-ui/src/components/Projects/ProjectHeader.vue b/packages/editor-ui/src/components/Projects/ProjectHeader.vue index 80a45bfeb6..9890b8b2e9 100644 --- a/packages/editor-ui/src/components/Projects/ProjectHeader.vue +++ b/packages/editor-ui/src/components/Projects/ProjectHeader.vue @@ -2,8 +2,9 @@ import { computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { N8nButton, N8nTooltip } from 'n8n-design-system'; +import type { ProjectIcon } from '@n8n/api-types'; import { useI18n } from '@/composables/useI18n'; -import { type ProjectIcon, ProjectTypes } from '@/types/projects.types'; +import { ProjectTypes } from '@/types/projects.types'; import { useProjectsStore } from '@/stores/projects.store'; import ProjectTabs from '@/components/Projects/ProjectTabs.vue'; import { getResourcePermissions } from '@/permissions'; @@ -21,7 +22,7 @@ const headerIcon = computed((): ProjectIcon => { if (projectsStore.currentProject?.type === ProjectTypes.Personal) { return { type: 'icon', value: 'user' }; } else if (projectsStore.currentProject?.name) { - return projectsStore.currentProject.icon ?? { type: 'icon', value: 'layer-group' }; + return projectsStore.currentProject.settings.icon ?? { type: 'icon', value: 'layer-group' }; } else { return { type: 'icon', value: 'home' }; } diff --git a/packages/editor-ui/src/components/Projects/ProjectIcon.vue b/packages/editor-ui/src/components/Projects/ProjectIcon.vue index ae3c47b4e3..7c3f26f41a 100644 --- a/packages/editor-ui/src/components/Projects/ProjectIcon.vue +++ b/packages/editor-ui/src/components/Projects/ProjectIcon.vue @@ -1,5 +1,5 @@