mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
add project settings
This commit is contained in:
parent
1a915239c6
commit
57f1a3fc59
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
}) {}
|
||||
|
|
|
@ -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(),
|
||||
}) {}
|
||||
|
|
|
@ -15,6 +15,7 @@ export { passwordSchema } from './schemas/password.schema';
|
|||
export type {
|
||||
ProjectType,
|
||||
ProjectIcon,
|
||||
ProjectSettings,
|
||||
ProjectRole,
|
||||
ProjectRelation,
|
||||
} from './schemas/project.schema';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<typeof projectTypeSchema>;
|
||||
|
||||
export const projectIconSchema = z.object({
|
||||
const projectIconSchema = z.object({
|
||||
type: z.enum(['emoji', 'icon']),
|
||||
value: z.string().min(1),
|
||||
});
|
||||
export type ProjectIcon = z.infer<typeof projectIconSchema>;
|
||||
|
||||
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<typeof projectSettingsSchema>;
|
||||
|
||||
export const projectRoleSchema = z.enum([
|
||||
'project:personalOwner', // personalOwner is only used for personal projects
|
||||
'project:admin',
|
||||
|
|
|
@ -171,7 +171,7 @@ export class ProjectController {
|
|||
_res: Response,
|
||||
@Param('projectId') projectId: string,
|
||||
): Promise<ProjectRequest.ProjectWithRelations> {
|
||||
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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { AddProjectSettings1738496517619 as BaseMigration } from '../common/1738496517619-AddProjectSettings';
|
||||
|
||||
export class AddProjectSettings1738496517619 extends BaseMigration {
|
||||
transaction = false as const;
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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<IUser, 'id' | 'email' | 'firstName' | 'lastName'>;
|
||||
export type SlimProject = Pick<Project, 'id' | 'type' | 'name' | 'icon'>;
|
||||
export type SlimProject = Pick<Project, 'id' | 'type' | 'name' | 'settings'>;
|
||||
|
||||
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[];
|
||||
};
|
||||
|
|
|
@ -37,7 +37,9 @@ import type {
|
|||
|
||||
@Service()
|
||||
export class ScalingService {
|
||||
private queue: JobQueue;
|
||||
private sharedQueue: JobQueue;
|
||||
|
||||
private readonly dedicatedQueues = new Map<string, JobQueue>();
|
||||
|
||||
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`');
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ export type JobId = Job['id'];
|
|||
|
||||
export type JobData = {
|
||||
executionId: string;
|
||||
projectId?: string;
|
||||
loadStaticData: boolean;
|
||||
pushRef?: string;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -188,7 +188,7 @@ export class ProjectService {
|
|||
|
||||
async updateProject(
|
||||
projectId: string,
|
||||
data: Pick<UpdateProjectDto, 'name' | 'icon'>,
|
||||
data: Pick<UpdateProjectDto, 'name' | 'settings'>,
|
||||
): Promise<Project> {
|
||||
const result = await this.projectRepository.update({ id: projectId, type: 'team' }, data);
|
||||
|
||||
|
|
|
@ -344,6 +344,7 @@ export class WorkflowRunner {
|
|||
): Promise<void> {
|
||||
const jobData: JobData = {
|
||||
executionId,
|
||||
projectId: data.projectId,
|
||||
loadStaticData: !!loadStaticData,
|
||||
pushRef: data.pushRef,
|
||||
};
|
||||
|
|
|
@ -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>): 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,
|
||||
|
|
|
@ -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' };
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { ProjectIcon } from '@/types/projects.types';
|
||||
import type { ProjectIcon } from '@n8n/api-types';
|
||||
|
||||
type Props = {
|
||||
icon: ProjectIcon;
|
||||
|
|
|
@ -33,7 +33,7 @@ const home = computed<IMenuItem>(() => ({
|
|||
const getProjectMenuItem = (project: ProjectListItem) => ({
|
||||
id: project.id,
|
||||
label: project.name,
|
||||
icon: project.icon,
|
||||
icon: project.settings.icon,
|
||||
route: {
|
||||
to: {
|
||||
name: VIEWS.PROJECTS_WORKFLOWS,
|
||||
|
|
|
@ -21,8 +21,8 @@ const processedName = computed(() => {
|
|||
});
|
||||
|
||||
const projectIcon = computed(() => {
|
||||
if (props.project.icon) {
|
||||
return props.project.icon;
|
||||
if (props.project.settings.icon) {
|
||||
return props.project.settings.icon;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
|
|
@ -173,7 +173,9 @@ export const useGlobalEntityCreation = () => {
|
|||
try {
|
||||
const newProject = await projectsStore.createProject({
|
||||
name: i18n.baseText('projects.settings.newProjectName'),
|
||||
icon: { type: 'icon', value: 'layer-group' },
|
||||
settings: {
|
||||
icon: { type: 'icon', value: 'layer-group' },
|
||||
},
|
||||
});
|
||||
await router.push({ name: VIEWS.PROJECT_SETTINGS, params: { projectId: newProject.id } });
|
||||
toast.showMessage({
|
||||
|
|
|
@ -32,7 +32,6 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
|||
const projectsCount = ref<ProjectsCount>({
|
||||
personal: 0,
|
||||
team: 0,
|
||||
public: 0,
|
||||
});
|
||||
const projectNavActiveIdState = ref<string | string[] | null>(null);
|
||||
|
||||
|
@ -120,14 +119,14 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
|||
): Promise<void> => {
|
||||
await projectsApi.updateProject(rootStore.restApiContext, id, projectData);
|
||||
const projectIndex = myProjects.value.findIndex((p) => p.id === id);
|
||||
const { name, icon } = projectData;
|
||||
const { name, settings } = projectData;
|
||||
if (projectIndex !== -1) {
|
||||
myProjects.value[projectIndex].name = name;
|
||||
myProjects.value[projectIndex].icon = icon;
|
||||
myProjects.value[projectIndex].settings = settings;
|
||||
}
|
||||
if (currentProject.value) {
|
||||
currentProject.value.name = name;
|
||||
currentProject.value.icon = icon;
|
||||
currentProject.value.settings = settings;
|
||||
}
|
||||
if (projectData.relations) {
|
||||
await getProject(id);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ProjectSettings } from '@n8n/api-types';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { IUserResponse } from '@/Interface';
|
||||
import type { ProjectRole } from '@/types/roles.types';
|
||||
|
@ -5,7 +6,6 @@ import type { ProjectRole } from '@/types/roles.types';
|
|||
export const ProjectTypes = {
|
||||
Personal: 'personal',
|
||||
Team: 'team',
|
||||
Public: 'public',
|
||||
} as const;
|
||||
|
||||
type ProjectTypeKeys = typeof ProjectTypes;
|
||||
|
@ -18,8 +18,8 @@ export type ProjectRelationPayload = { userId: string; role: ProjectRole };
|
|||
export type ProjectSharingData = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
icon: ProjectIcon | null;
|
||||
type: ProjectType;
|
||||
settings: ProjectSettings;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
@ -32,8 +32,3 @@ export type ProjectListItem = ProjectSharingData & {
|
|||
scopes?: Scope[];
|
||||
};
|
||||
export type ProjectsCount = Record<ProjectType, number>;
|
||||
|
||||
export type ProjectIcon = {
|
||||
type: 'icon' | 'emoji';
|
||||
value: string;
|
||||
};
|
||||
|
|
|
@ -66,7 +66,7 @@ describe('ProjectSettings', () => {
|
|||
id: '123',
|
||||
type: 'team',
|
||||
name: 'Test Project',
|
||||
icon: { type: 'icon', value: 'folder' },
|
||||
settings: { icon: { type: 'icon', value: 'folder' } },
|
||||
relations: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
|
|
@ -3,11 +3,11 @@ import { computed, ref, watch, onBeforeMount, onMounted, nextTick } from 'vue';
|
|||
import { useRouter } from 'vue-router';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { N8nFormInput } from 'n8n-design-system';
|
||||
import type { ProjectIcon } from '@n8n/api-types';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import type { IUser } from '@/Interface';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import type { ProjectIcon } from '@/types/projects.types';
|
||||
import { type Project, type ProjectRelation } from '@/types/projects.types';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { VIEWS } from '@/constants';
|
||||
|
@ -194,7 +194,9 @@ const updateProject = async () => {
|
|||
try {
|
||||
await projectsStore.updateProject(projectsStore.currentProject.id, {
|
||||
name: formData.value.name!,
|
||||
icon: projectIcon.value,
|
||||
settings: {
|
||||
icon: projectIcon.value,
|
||||
},
|
||||
relations: formData.value.relations.map((r: ProjectRelation) => ({
|
||||
userId: r.id,
|
||||
role: r.role,
|
||||
|
@ -269,8 +271,8 @@ watch(
|
|||
: [];
|
||||
await nextTick();
|
||||
selectProjectNameIfMatchesDefault();
|
||||
if (projectsStore.currentProject?.icon) {
|
||||
projectIcon.value = projectsStore.currentProject.icon;
|
||||
if (projectsStore.currentProject?.settings.icon) {
|
||||
projectIcon.value = projectsStore.currentProject.settings.icon;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
|
@ -125,8 +125,10 @@ export interface IUser {
|
|||
export type ProjectSharingData = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
icon: { type: 'emoji' | 'icon'; value: string } | null;
|
||||
type: 'personal' | 'team' | 'public';
|
||||
type: 'personal' | 'team';
|
||||
settings: {
|
||||
icon?: { type: 'emoji' | 'icon'; value: string };
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue