mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
refactor(core): Port over project request payloads to DTOs (#12528)
This commit is contained in:
parent
44679b42aa
commit
5f1adefca7
|
@ -21,6 +21,10 @@ export { ForgotPasswordRequestDto } from './password-reset/forgot-password-reque
|
|||
export { ResolvePasswordTokenQueryDto } from './password-reset/resolve-password-token-query.dto';
|
||||
export { ChangePasswordRequestDto } from './password-reset/change-password-request.dto';
|
||||
|
||||
export { CreateProjectDto } from './project/create-project.dto';
|
||||
export { UpdateProjectDto } from './project/update-project.dto';
|
||||
export { DeleteProjectDto } from './project/delete-project.dto';
|
||||
|
||||
export { SamlAcsDto } from './saml/saml-acs.dto';
|
||||
export { SamlPreferences } from './saml/saml-preferences.dto';
|
||||
export { SamlToggleDto } from './saml/saml-toggle.dto';
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { CreateProjectDto } from '../create-project.dto';
|
||||
|
||||
describe('CreateProjectDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with just the name',
|
||||
request: {
|
||||
name: 'My Awesome Project',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and emoji icon',
|
||||
request: {
|
||||
name: 'My Awesome Project',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
value: '🚀',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and regular icon',
|
||||
request: {
|
||||
name: 'My Awesome Project',
|
||||
icon: {
|
||||
type: 'icon',
|
||||
value: 'blah',
|
||||
},
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = CreateProjectDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'missing name',
|
||||
request: { icon: { type: 'emoji', value: '🚀' } },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'empty name',
|
||||
request: { name: '', icon: { type: 'emoji', value: '🚀' } },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name too long',
|
||||
request: { name: 'a'.repeat(256), icon: { type: 'emoji', value: '🚀' } },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon type',
|
||||
request: { name: 'My Awesome Project', icon: { type: 'invalid', value: '🚀' } },
|
||||
expectedErrorPath: ['icon', 'type'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon value',
|
||||
request: { name: 'My Awesome Project', icon: { type: 'emoji', value: '' } },
|
||||
expectedErrorPath: ['icon', 'value'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = CreateProjectDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
import { UpdateProjectDto } from '../update-project.dto';
|
||||
|
||||
describe('UpdateProjectDto', () => {
|
||||
describe('Valid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'with just the name',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and emoji icon',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
value: '🚀',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with name and regular icon',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
icon: {
|
||||
type: 'icon',
|
||||
value: 'blah',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with relations',
|
||||
request: {
|
||||
relations: [
|
||||
{
|
||||
userId: 'user-123',
|
||||
role: 'project:admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'with all fields',
|
||||
request: {
|
||||
name: 'My Updated Project',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
value: '🚀',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
userId: 'user-123',
|
||||
role: 'project:admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])('should validate $name', ({ request }) => {
|
||||
const result = UpdateProjectDto.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid requests', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'invalid name type',
|
||||
request: { name: 123 },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'name too long',
|
||||
request: { name: 'a'.repeat(256) },
|
||||
expectedErrorPath: ['name'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon type',
|
||||
request: { icon: { type: 'invalid', value: '🚀' } },
|
||||
expectedErrorPath: ['icon', 'type'],
|
||||
},
|
||||
{
|
||||
name: 'invalid icon value',
|
||||
request: { icon: { type: 'emoji', value: '' } },
|
||||
expectedErrorPath: ['icon', 'value'],
|
||||
},
|
||||
{
|
||||
name: 'invalid relations userId',
|
||||
request: {
|
||||
relations: [
|
||||
{
|
||||
userId: 123,
|
||||
role: 'project:admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['relations', 0, 'userId'],
|
||||
},
|
||||
{
|
||||
name: 'invalid relations role',
|
||||
request: {
|
||||
relations: [
|
||||
{
|
||||
userId: 'user-123',
|
||||
role: 'invalid-role',
|
||||
},
|
||||
],
|
||||
},
|
||||
expectedErrorPath: ['relations', 0, 'role'],
|
||||
},
|
||||
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
|
||||
const result = UpdateProjectDto.safeParse(request);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
if (expectedErrorPath) {
|
||||
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { Z } from 'zod-class';
|
||||
|
||||
import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema';
|
||||
|
||||
export class CreateProjectDto extends Z.class({
|
||||
name: projectNameSchema,
|
||||
icon: projectIconSchema.optional(),
|
||||
}) {}
|
|
@ -0,0 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class DeleteProjectDto extends Z.class({
|
||||
transferId: z.string().optional(),
|
||||
}) {}
|
|
@ -0,0 +1,14 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import {
|
||||
projectIconSchema,
|
||||
projectNameSchema,
|
||||
projectRelationSchema,
|
||||
} from '../../schemas/project.schema';
|
||||
|
||||
export class UpdateProjectDto extends Z.class({
|
||||
name: projectNameSchema.optional(),
|
||||
icon: projectIconSchema.optional(),
|
||||
relations: z.array(projectRelationSchema).optional(),
|
||||
}) {}
|
|
@ -10,3 +10,9 @@ export type { SendWorkerStatusMessage } from './push/worker';
|
|||
|
||||
export type { BannerName } from './schemas/bannerName.schema';
|
||||
export { passwordSchema } from './schemas/password.schema';
|
||||
export {
|
||||
ProjectType,
|
||||
ProjectIcon,
|
||||
ProjectRole,
|
||||
ProjectRelation,
|
||||
} from './schemas/project.schema';
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import {
|
||||
projectNameSchema,
|
||||
projectTypeSchema,
|
||||
projectIconSchema,
|
||||
projectRoleSchema,
|
||||
projectRelationSchema,
|
||||
} from '../project.schema';
|
||||
|
||||
describe('project.schema', () => {
|
||||
describe('projectNameSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid name', value: 'My Project', expected: true },
|
||||
{ name: 'empty name', value: '', expected: false },
|
||||
{ name: 'name too long', value: 'a'.repeat(256), expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectNameSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectTypeSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid type: personal', value: 'personal', expected: true },
|
||||
{ name: 'valid type: team', value: 'team', expected: true },
|
||||
{ name: 'invalid type', value: 'invalid', expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectTypeSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
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('projectRoleSchema', () => {
|
||||
test.each([
|
||||
{ name: 'valid role: project:personalOwner', value: 'project:personalOwner', expected: true },
|
||||
{ name: 'valid role: project:admin', value: 'project:admin', expected: true },
|
||||
{ name: 'valid role: project:editor', value: 'project:editor', expected: true },
|
||||
{ name: 'valid role: project:viewer', value: 'project:viewer', expected: true },
|
||||
{ name: 'invalid role', value: 'invalid-role', expected: false },
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectRoleSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectRelationSchema', () => {
|
||||
test.each([
|
||||
{
|
||||
name: 'valid relation',
|
||||
value: { userId: 'user-123', role: 'project:admin' },
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: 'invalid userId type',
|
||||
value: { userId: 123, role: 'project:admin' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'invalid role',
|
||||
value: { userId: 'user-123', role: 'invalid-role' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'missing userId',
|
||||
value: { role: 'project:admin' },
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: 'missing role',
|
||||
value: { userId: 'user-123' },
|
||||
expected: false,
|
||||
},
|
||||
])('should validate $name', ({ value, expected }) => {
|
||||
const result = projectRelationSchema.safeParse(value);
|
||||
expect(result.success).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
26
packages/@n8n/api-types/src/schemas/project.schema.ts
Normal file
26
packages/@n8n/api-types/src/schemas/project.schema.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
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({
|
||||
type: z.enum(['emoji', 'icon']),
|
||||
value: z.string().min(1),
|
||||
});
|
||||
export type ProjectIcon = z.infer<typeof projectIconSchema>;
|
||||
|
||||
export const projectRoleSchema = z.enum([
|
||||
'project:personalOwner', // personalOwner is only used for personal projects
|
||||
'project:admin',
|
||||
'project:editor',
|
||||
'project:viewer',
|
||||
]);
|
||||
export type ProjectRole = z.infer<typeof projectRoleSchema>;
|
||||
|
||||
export const projectRelationSchema = z.object({
|
||||
userId: z.string(),
|
||||
role: projectRoleSchema,
|
||||
});
|
||||
export type ProjectRelation = z.infer<typeof projectRelationSchema>;
|
|
@ -1,7 +1,7 @@
|
|||
import { nanoId, date, firstName, lastName, email } from 'minifaker';
|
||||
import 'minifaker/locales/en';
|
||||
|
||||
import type { Project, ProjectType } from '@/databases/entities/project';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
|
||||
type RawProjectData = Pick<Project, 'name' | 'type' | 'createdAt' | 'updatedAt' | 'id'>;
|
||||
|
||||
|
@ -13,7 +13,7 @@ export const createRawProjectData = (payload: Partial<RawProjectData>): Project
|
|||
updatedAt: date(),
|
||||
id: nanoId.nanoid(),
|
||||
name: projectName,
|
||||
type: 'personal' as ProjectType,
|
||||
type: 'personal',
|
||||
...payload,
|
||||
} as Project;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { CreateProjectDto, DeleteProjectDto, UpdateProjectDto } from '@n8n/api-types';
|
||||
import { combineScopes } from '@n8n/permissions';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In, Not } from '@n8n/typeorm';
|
||||
import { Response } from 'express';
|
||||
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
|
@ -14,11 +16,15 @@ import {
|
|||
Patch,
|
||||
ProjectScope,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
} from '@/decorators';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import { ProjectRequest } from '@/requests';
|
||||
import type { ProjectRequest } from '@/requests';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import {
|
||||
ProjectService,
|
||||
TeamProjectOverQuotaError,
|
||||
|
@ -36,7 +42,7 @@ export class ProjectController {
|
|||
) {}
|
||||
|
||||
@Get('/')
|
||||
async getAllProjects(req: ProjectRequest.GetAll): Promise<Project[]> {
|
||||
async getAllProjects(req: AuthenticatedRequest): Promise<Project[]> {
|
||||
return await this.projectsService.getAccessibleProjects(req.user);
|
||||
}
|
||||
|
||||
|
@ -49,14 +55,9 @@ export class ProjectController {
|
|||
@GlobalScope('project:create')
|
||||
// Using admin as all plans that contain projects should allow admins at the very least
|
||||
@Licensed('feat:projectRole:admin')
|
||||
async createProject(req: ProjectRequest.Create) {
|
||||
async createProject(req: AuthenticatedRequest, _res: Response, @Body payload: CreateProjectDto) {
|
||||
try {
|
||||
const project = await this.projectsService.createTeamProject(
|
||||
req.body.name,
|
||||
req.user,
|
||||
undefined,
|
||||
req.body.icon,
|
||||
);
|
||||
const project = await this.projectsService.createTeamProject(req.user, payload);
|
||||
|
||||
this.eventService.emit('team-project-created', {
|
||||
userId: req.user.id,
|
||||
|
@ -83,7 +84,8 @@ export class ProjectController {
|
|||
|
||||
@Get('/my-projects')
|
||||
async getMyProjects(
|
||||
req: ProjectRequest.GetMyProjects,
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
): Promise<ProjectRequest.GetMyProjectsResponse> {
|
||||
const relations = await this.projectsService.getProjectRelationsForUser(req.user);
|
||||
const otherTeamProject = req.user.hasGlobalScope('project:read')
|
||||
|
@ -98,10 +100,7 @@ export class ProjectController {
|
|||
for (const pr of relations) {
|
||||
const result: ProjectRequest.GetMyProjectsResponse[number] = Object.assign(
|
||||
this.projectRepository.create(pr.project),
|
||||
{
|
||||
role: pr.role,
|
||||
scopes: req.query.includeScopes ? ([] as Scope[]) : undefined,
|
||||
},
|
||||
{ role: pr.role, scopes: [] },
|
||||
);
|
||||
|
||||
if (result.scopes) {
|
||||
|
@ -124,7 +123,7 @@ export class ProjectController {
|
|||
// own this relationship in that case we use the global user role
|
||||
// instead of the relation role, which is for another user.
|
||||
role: req.user.role,
|
||||
scopes: req.query.includeScopes ? [] : undefined,
|
||||
scopes: [],
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -148,7 +147,7 @@ export class ProjectController {
|
|||
}
|
||||
|
||||
@Get('/personal')
|
||||
async getPersonalProject(req: ProjectRequest.GetPersonalProject) {
|
||||
async getPersonalProject(req: AuthenticatedRequest) {
|
||||
const project = await this.projectsService.getPersonalProject(req.user);
|
||||
if (!project) {
|
||||
throw new NotFoundError('Could not find a personal project for this user');
|
||||
|
@ -167,10 +166,14 @@ export class ProjectController {
|
|||
|
||||
@Get('/:projectId')
|
||||
@ProjectScope('project:read')
|
||||
async getProject(req: ProjectRequest.Get): Promise<ProjectRequest.ProjectWithRelations> {
|
||||
async getProject(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Param('projectId') projectId: string,
|
||||
): Promise<ProjectRequest.ProjectWithRelations> {
|
||||
const [{ id, name, icon, type }, relations] = await Promise.all([
|
||||
this.projectsService.getProject(req.params.projectId),
|
||||
this.projectsService.getProjectRelations(req.params.projectId),
|
||||
this.projectsService.getProject(projectId),
|
||||
this.projectsService.getProjectRelations(projectId),
|
||||
]);
|
||||
const myRelation = relations.find((r) => r.userId === req.user.id);
|
||||
|
||||
|
@ -197,13 +200,19 @@ export class ProjectController {
|
|||
|
||||
@Patch('/:projectId')
|
||||
@ProjectScope('project:update')
|
||||
async updateProject(req: ProjectRequest.Update) {
|
||||
if (req.body.name) {
|
||||
await this.projectsService.updateProject(req.body.name, req.params.projectId, req.body.icon);
|
||||
async updateProject(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Body payload: UpdateProjectDto,
|
||||
@Param('projectId') projectId: string,
|
||||
) {
|
||||
const { name, icon, relations } = payload;
|
||||
if (name || icon) {
|
||||
await this.projectsService.updateProject(projectId, { name, icon });
|
||||
}
|
||||
if (req.body.relations) {
|
||||
if (relations) {
|
||||
try {
|
||||
await this.projectsService.syncProjectRelations(req.params.projectId, req.body.relations);
|
||||
await this.projectsService.syncProjectRelations(projectId, relations);
|
||||
} catch (e) {
|
||||
if (e instanceof UnlicensedProjectRoleError) {
|
||||
throw new BadRequestError(e.message);
|
||||
|
@ -214,25 +223,30 @@ export class ProjectController {
|
|||
this.eventService.emit('team-project-updated', {
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
members: req.body.relations,
|
||||
projectId: req.params.projectId,
|
||||
members: relations,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('/:projectId')
|
||||
@ProjectScope('project:delete')
|
||||
async deleteProject(req: ProjectRequest.Delete) {
|
||||
await this.projectsService.deleteProject(req.user, req.params.projectId, {
|
||||
migrateToProject: req.query.transferId,
|
||||
async deleteProject(
|
||||
req: AuthenticatedRequest,
|
||||
_res: Response,
|
||||
@Query query: DeleteProjectDto,
|
||||
@Param('projectId') projectId: string,
|
||||
) {
|
||||
await this.projectsService.deleteProject(req.user, projectId, {
|
||||
migrateToProject: query.transferId,
|
||||
});
|
||||
|
||||
this.eventService.emit('team-project-deleted', {
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
projectId: req.params.projectId,
|
||||
removalType: req.query.transferId !== undefined ? 'transfer' : 'delete',
|
||||
targetProjectId: req.query.transferId,
|
||||
projectId,
|
||||
removalType: query.transferId !== undefined ? 'transfer' : 'delete',
|
||||
targetProjectId: query.transferId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
import { ProjectRole } from '@n8n/api-types';
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from '@n8n/typeorm';
|
||||
|
||||
import { WithTimestamps } from './abstract-entity';
|
||||
import { Project } from './project';
|
||||
import { User } from './user';
|
||||
|
||||
// personalOwner is only used for personal projects
|
||||
export type ProjectRole =
|
||||
| 'project:personalOwner'
|
||||
| 'project:admin'
|
||||
| 'project:editor'
|
||||
| 'project:viewer';
|
||||
|
||||
@Entity()
|
||||
export class ProjectRelation extends WithTimestamps {
|
||||
@Column()
|
||||
@Column({ type: 'varchar' })
|
||||
role: ProjectRole;
|
||||
|
||||
@ManyToOne('User', 'projectRelations')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ProjectIcon, ProjectType } from '@n8n/api-types';
|
||||
import { Column, Entity, OneToMany } from '@n8n/typeorm';
|
||||
|
||||
import { WithTimestampsAndStringId } from './abstract-entity';
|
||||
|
@ -5,15 +6,12 @@ import type { ProjectRelation } from './project-relation';
|
|||
import type { SharedCredentials } from './shared-credentials';
|
||||
import type { SharedWorkflow } from './shared-workflow';
|
||||
|
||||
export type ProjectType = 'personal' | 'team';
|
||||
export type ProjectIcon = { type: 'emoji' | 'icon'; value: string } | null;
|
||||
|
||||
@Entity()
|
||||
export class Project extends WithTimestampsAndStringId {
|
||||
@Column({ length: 255 })
|
||||
name: string;
|
||||
|
||||
@Column({ length: 36 })
|
||||
@Column({ type: 'varchar', length: 36 })
|
||||
type: ProjectType;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import type { MigrationContext, ReversibleMigration } from '@/databases/types';
|
||||
import { generateNanoId } from '@/databases/utils/generators';
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import { DataSource, In, Repository } from '@n8n/typeorm';
|
||||
|
||||
import { ProjectRelation, type ProjectRole } from '../entities/project-relation';
|
||||
import { ProjectRelation } from '../entities/project-relation';
|
||||
|
||||
@Service()
|
||||
export class ProjectRelationRepository extends Repository<ProjectRelation> {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { EntityManager, FindOptionsRelations, FindOptionsWhere } from '@n8n/typeorm';
|
||||
|
@ -6,7 +7,6 @@ import { DataSource, In, Not, Repository } from '@n8n/typeorm';
|
|||
import { RoleService } from '@/services/role.service';
|
||||
|
||||
import type { Project } from '../entities/project';
|
||||
import type { ProjectRole } from '../entities/project-relation';
|
||||
import { type CredentialSharingRole, SharedCredentials } from '../entities/shared-credentials';
|
||||
import type { User } from '../entities/user';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { AuthenticationMethod } from '@n8n/api-types';
|
||||
import type { AuthenticationMethod, ProjectRelation } from '@n8n/api-types';
|
||||
import type {
|
||||
IPersonalizationSurveyAnswersV4,
|
||||
IRun,
|
||||
|
@ -7,7 +7,6 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
|
||||
import type { AuthProviderType } from '@/databases/entities/auth-identity';
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { GlobalRole, User } from '@/databases/entities/user';
|
||||
import type { IWorkflowDb } from '@/interfaces';
|
||||
|
||||
|
@ -351,10 +350,7 @@ export type RelayEventMap = {
|
|||
'team-project-updated': {
|
||||
userId: string;
|
||||
role: GlobalRole;
|
||||
members: Array<{
|
||||
userId: string;
|
||||
role: ProjectRole;
|
||||
}>;
|
||||
members: ProjectRelation[];
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
import { CreateProjectDto, DeleteProjectDto, UpdateProjectDto } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { ProjectController } from '@/controllers/project.controller';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import type { PaginatedRequest } from '@/public-api/types';
|
||||
import type { ProjectRequest } from '@/requests';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
|
||||
import { globalScope, isLicensed, validCursor } from '../../shared/middlewares/global.middleware';
|
||||
import { encodeNextCursor } from '../../shared/services/pagination.service';
|
||||
|
||||
type Create = ProjectRequest.Create;
|
||||
type Update = ProjectRequest.Update;
|
||||
type Delete = ProjectRequest.Delete;
|
||||
type GetAll = PaginatedRequest;
|
||||
|
||||
export = {
|
||||
createProject: [
|
||||
isLicensed('feat:projectRole:admin'),
|
||||
globalScope('project:create'),
|
||||
async (req: Create, res: Response) => {
|
||||
const project = await Container.get(ProjectController).createProject(req);
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
const payload = CreateProjectDto.safeParse(req.body);
|
||||
if (payload.error) {
|
||||
return res.status(400).json(payload.error.errors[0]);
|
||||
}
|
||||
|
||||
const project = await Container.get(ProjectController).createProject(req, res, payload.data);
|
||||
|
||||
return res.status(201).json(project);
|
||||
},
|
||||
|
@ -27,8 +30,18 @@ export = {
|
|||
updateProject: [
|
||||
isLicensed('feat:projectRole:admin'),
|
||||
globalScope('project:update'),
|
||||
async (req: Update, res: Response) => {
|
||||
await Container.get(ProjectController).updateProject(req);
|
||||
async (req: AuthenticatedRequest<{ projectId: string }>, res: Response) => {
|
||||
const payload = UpdateProjectDto.safeParse(req.body);
|
||||
if (payload.error) {
|
||||
return res.status(400).json(payload.error.errors[0]);
|
||||
}
|
||||
|
||||
await Container.get(ProjectController).updateProject(
|
||||
req,
|
||||
res,
|
||||
payload.data,
|
||||
req.params.projectId,
|
||||
);
|
||||
|
||||
return res.status(204).send();
|
||||
},
|
||||
|
@ -36,8 +49,18 @@ export = {
|
|||
deleteProject: [
|
||||
isLicensed('feat:projectRole:admin'),
|
||||
globalScope('project:delete'),
|
||||
async (req: Delete, res: Response) => {
|
||||
await Container.get(ProjectController).deleteProject(req);
|
||||
async (req: AuthenticatedRequest<{ projectId: string }>, res: Response) => {
|
||||
const query = DeleteProjectDto.safeParse(req.query);
|
||||
if (query.error) {
|
||||
return res.status(400).json(query.error.errors[0]);
|
||||
}
|
||||
|
||||
await Container.get(ProjectController).deleteProject(
|
||||
req,
|
||||
res,
|
||||
query.data,
|
||||
req.params.projectId,
|
||||
);
|
||||
|
||||
return res.status(204).send();
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ProjectIcon, ProjectRole, ProjectType } from '@n8n/api-types';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type express from 'express';
|
||||
import type {
|
||||
|
@ -9,14 +10,13 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
|
||||
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
||||
import type { Project, ProjectIcon, ProjectType } from '@/databases/entities/project';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { AssignableRole, GlobalRole, User } from '@/databases/entities/user';
|
||||
import type { Variables } from '@/databases/entities/variables';
|
||||
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||
import type { WorkflowHistory } from '@/databases/entities/workflow-history';
|
||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||
|
||||
import type { ProjectRole } from './databases/entities/project-relation';
|
||||
import type { ScopesField } from './services/role.service';
|
||||
|
||||
export type APIRequest<
|
||||
|
@ -388,32 +388,10 @@ export declare namespace ActiveWorkflowRequest {
|
|||
// ----------------------------------
|
||||
|
||||
export declare namespace ProjectRequest {
|
||||
type GetAll = AuthenticatedRequest<{}, Project[]>;
|
||||
|
||||
type Create = AuthenticatedRequest<
|
||||
{},
|
||||
Project,
|
||||
{
|
||||
name: string;
|
||||
icon?: ProjectIcon;
|
||||
}
|
||||
>;
|
||||
|
||||
type GetMyProjects = AuthenticatedRequest<
|
||||
{},
|
||||
Array<Project & { role: ProjectRole }>,
|
||||
{},
|
||||
{
|
||||
includeScopes?: boolean;
|
||||
}
|
||||
>;
|
||||
type GetMyProjectsResponse = Array<
|
||||
Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }
|
||||
>;
|
||||
|
||||
type GetPersonalProject = AuthenticatedRequest<{}, Project>;
|
||||
|
||||
type ProjectRelationPayload = { userId: string; role: ProjectRole };
|
||||
type ProjectRelationResponse = {
|
||||
id: string;
|
||||
email: string;
|
||||
|
@ -429,18 +407,6 @@ export declare namespace ProjectRequest {
|
|||
relations: ProjectRelationResponse[];
|
||||
scopes: Scope[];
|
||||
};
|
||||
|
||||
type Get = AuthenticatedRequest<{ projectId: string }, {}>;
|
||||
type Update = AuthenticatedRequest<
|
||||
{ projectId: string },
|
||||
{},
|
||||
{
|
||||
name?: string;
|
||||
relations?: ProjectRelationPayload[];
|
||||
icon?: { type: 'icon' | 'emoji'; value: string };
|
||||
}
|
||||
>;
|
||||
type Delete = AuthenticatedRequest<{ projectId: string }, {}, {}, { transferId?: string }>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { CreateProjectDto, ProjectRole, ProjectType, UpdateProjectDto } from '@n8n/api-types';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { type Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
|
@ -7,10 +8,8 @@ import { In, Not } from '@n8n/typeorm';
|
|||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
import { UNLIMITED_LICENSE_QUOTA } from '@/constants';
|
||||
import type { ProjectIcon, ProjectType } from '@/databases/entities/project';
|
||||
import { Project } from '@/databases/entities/project';
|
||||
import { ProjectRelation } from '@/databases/entities/project-relation';
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
|
@ -168,12 +167,7 @@ export class ProjectService {
|
|||
return await this.projectRelationRepository.getPersonalProjectOwners(projectIds);
|
||||
}
|
||||
|
||||
async createTeamProject(
|
||||
name: string,
|
||||
adminUser: User,
|
||||
id?: string,
|
||||
icon?: ProjectIcon,
|
||||
): Promise<Project> {
|
||||
async createTeamProject(adminUser: User, data: CreateProjectDto): Promise<Project> {
|
||||
const limit = this.license.getTeamProjectLimit();
|
||||
if (
|
||||
limit !== UNLIMITED_LICENSE_QUOTA &&
|
||||
|
@ -183,12 +177,7 @@ export class ProjectService {
|
|||
}
|
||||
|
||||
const project = await this.projectRepository.save(
|
||||
this.projectRepository.create({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
type: 'team',
|
||||
}),
|
||||
this.projectRepository.create({ ...data, type: 'team' }),
|
||||
);
|
||||
|
||||
// Link admin
|
||||
|
@ -198,20 +187,10 @@ export class ProjectService {
|
|||
}
|
||||
|
||||
async updateProject(
|
||||
name: string,
|
||||
projectId: string,
|
||||
icon?: { type: 'icon' | 'emoji'; value: string },
|
||||
data: Pick<UpdateProjectDto, 'name' | 'icon'>,
|
||||
): Promise<Project> {
|
||||
const result = await this.projectRepository.update(
|
||||
{
|
||||
id: projectId,
|
||||
type: 'team',
|
||||
},
|
||||
{
|
||||
name,
|
||||
icon,
|
||||
},
|
||||
);
|
||||
const result = await this.projectRepository.update({ id: projectId, type: 'team' }, data);
|
||||
|
||||
if (!result.affected) {
|
||||
throw new ForbiddenError('Project not found');
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import { combineScopes, type Resource, type Scope } from '@n8n/permissions';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
import type { CredentialsEntity } from '@/databases/entities/credentials-entity';
|
||||
import type { ProjectRelation, ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { ProjectRelation } from '@/databases/entities/project-relation';
|
||||
import type {
|
||||
CredentialSharingRole,
|
||||
SharedCredentials,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Service } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
|
||||
import { In } from '@n8n/typeorm';
|
||||
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { WorkflowSharingRole } from '@/databases/entities/shared-workflow';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import { In } from '@n8n/typeorm';
|
||||
|
||||
import config from '@/config';
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
|
||||
|
@ -226,12 +226,12 @@ describe('GET /credentials', () => {
|
|||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const project1 = await projectService.createTeamProject('Team Project', member);
|
||||
const project1 = await projectService.createTeamProject(member, { name: 'Team Project' });
|
||||
await projectService.addUser(project1.id, anotherMember.id, 'project:editor');
|
||||
// anotherMember should see this one
|
||||
const credential1 = await saveCredential(randomCredentialPayload(), { project: project1 });
|
||||
|
||||
const project2 = await projectService.createTeamProject('Team Project', member);
|
||||
const project2 = await projectService.createTeamProject(member, { name: 'Team Project' });
|
||||
// anotherMember should NOT see this one
|
||||
await saveCredential(randomCredentialPayload(), { project: project2 });
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { EntityNotFoundError } from '@n8n/typeorm';
|
||||
|
||||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { GlobalRole } from '@/databases/entities/user';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
|
@ -177,11 +177,7 @@ describe('GET /projects/my-projects', () => {
|
|||
//
|
||||
// ACT
|
||||
//
|
||||
const resp = await testServer
|
||||
.authAgentFor(testUser1)
|
||||
.get('/projects/my-projects')
|
||||
.query({ includeScopes: true })
|
||||
.expect(200);
|
||||
const resp = await testServer.authAgentFor(testUser1).get('/projects/my-projects').expect(200);
|
||||
const respProjects: Array<Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }> =
|
||||
resp.body.data;
|
||||
|
||||
|
@ -258,11 +254,7 @@ describe('GET /projects/my-projects', () => {
|
|||
//
|
||||
// ACT
|
||||
//
|
||||
const resp = await testServer
|
||||
.authAgentFor(ownerUser)
|
||||
.get('/projects/my-projects')
|
||||
.query({ includeScopes: true })
|
||||
.expect(200);
|
||||
const resp = await testServer.authAgentFor(ownerUser).get('/projects/my-projects').expect(200);
|
||||
const respProjects: Array<Project & { role: ProjectRole | GlobalRole; scopes?: Scope[] }> =
|
||||
resp.body.data;
|
||||
|
||||
|
|
|
@ -263,8 +263,12 @@ describe('GET /workflows', () => {
|
|||
|
||||
test('for owner, should return all workflows filtered by `projectId`', async () => {
|
||||
license.setQuota('quota:maxTeamProjects', -1);
|
||||
const firstProject = await Container.get(ProjectService).createTeamProject('First', owner);
|
||||
const secondProject = await Container.get(ProjectService).createTeamProject('Second', member);
|
||||
const firstProject = await Container.get(ProjectService).createTeamProject(owner, {
|
||||
name: 'First',
|
||||
});
|
||||
const secondProject = await Container.get(ProjectService).createTeamProject(member, {
|
||||
name: 'Second',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
createWorkflow({ name: 'First workflow' }, firstProject),
|
||||
|
@ -285,10 +289,9 @@ describe('GET /workflows', () => {
|
|||
|
||||
test('for member, should return all member-accessible workflows filtered by `projectId`', async () => {
|
||||
license.setQuota('quota:maxTeamProjects', -1);
|
||||
const otherProject = await Container.get(ProjectService).createTeamProject(
|
||||
'Other project',
|
||||
member,
|
||||
);
|
||||
const otherProject = await Container.get(ProjectService).createTeamProject(member, {
|
||||
name: 'Other project',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
createWorkflow({}, member),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { CredentialSharingRole } from '@/databases/entities/shared-credentials';
|
||||
import type { WorkflowSharingRole } from '@/databases/entities/shared-workflow';
|
||||
import type { GlobalRole } from '@/databases/entities/user';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { ProjectRelation, ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { ProjectRelation } from '@/databases/entities/project-relation';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
|
|
|
@ -72,7 +72,7 @@ describe('WorkflowSharingService', () => {
|
|||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const project = await projectService.createTeamProject('Team Project', member);
|
||||
const project = await projectService.createTeamProject(member, { name: 'Team Project' });
|
||||
await projectService.addUser(project.id, anotherMember.id, 'project:admin');
|
||||
const workflow = await createWorkflow(undefined, project);
|
||||
|
||||
|
@ -93,9 +93,9 @@ describe('WorkflowSharingService', () => {
|
|||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const project1 = await projectService.createTeamProject('Team Project 1', member);
|
||||
const project1 = await projectService.createTeamProject(member, { name: 'Team Project 1' });
|
||||
const workflow1 = await createWorkflow(undefined, project1);
|
||||
const project2 = await projectService.createTeamProject('Team Project 2', member);
|
||||
const project2 = await projectService.createTeamProject(member, { name: 'Team Project 2' });
|
||||
const workflow2 = await createWorkflow(undefined, project2);
|
||||
await projectService.addUser(project1.id, anotherMember.id, 'project:admin');
|
||||
await projectService.addUser(project2.id, anotherMember.id, 'project:viewer');
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ProjectRole } from '@n8n/api-types';
|
||||
import { Container } from '@n8n/di';
|
||||
import { ApplicationError, WorkflowActivationError, type INode } from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
@ -5,7 +6,6 @@ import { v4 as uuid } from 'uuid';
|
|||
import { ActiveWorkflowManager } from '@/active-workflow-manager';
|
||||
import config from '@/config';
|
||||
import type { Project } from '@/databases/entities/project';
|
||||
import type { ProjectRole } from '@/databases/entities/project-relation';
|
||||
import type { User } from '@/databases/entities/user';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { WorkflowHistoryRepository } from '@/databases/repositories/workflow-history.repository';
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
import type { IRestApiContext } from '@/Interface';
|
||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
import type {
|
||||
Project,
|
||||
ProjectCreateRequest,
|
||||
ProjectListItem,
|
||||
ProjectUpdateRequest,
|
||||
ProjectsCount,
|
||||
} from '@/types/projects.types';
|
||||
import type { Project, ProjectListItem, ProjectsCount } from '@/types/projects.types';
|
||||
import type { CreateProjectDto, UpdateProjectDto } from '@n8n/api-types';
|
||||
|
||||
export const getAllProjects = async (context: IRestApiContext): Promise<ProjectListItem[]> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/projects');
|
||||
};
|
||||
|
||||
export const getMyProjects = async (context: IRestApiContext): Promise<ProjectListItem[]> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/projects/my-projects', {
|
||||
includeScopes: true,
|
||||
});
|
||||
return await makeRestApiRequest(context, 'GET', '/projects/my-projects');
|
||||
};
|
||||
|
||||
export const getPersonalProject = async (context: IRestApiContext): Promise<Project> => {
|
||||
|
@ -28,17 +21,17 @@ export const getProject = async (context: IRestApiContext, id: string): Promise<
|
|||
|
||||
export const createProject = async (
|
||||
context: IRestApiContext,
|
||||
req: ProjectCreateRequest,
|
||||
payload: CreateProjectDto,
|
||||
): Promise<Project> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/projects', req);
|
||||
return await makeRestApiRequest(context, 'POST', '/projects', payload);
|
||||
};
|
||||
|
||||
export const updateProject = async (
|
||||
context: IRestApiContext,
|
||||
req: ProjectUpdateRequest,
|
||||
id: Project['id'],
|
||||
payload: UpdateProjectDto,
|
||||
): Promise<void> => {
|
||||
const { id, name, icon, relations } = req;
|
||||
await makeRestApiRequest(context, 'PATCH', `/projects/${id}`, { name, icon, relations });
|
||||
await makeRestApiRequest(context, 'PATCH', `/projects/${id}`, payload);
|
||||
};
|
||||
|
||||
export const deleteProject = async (
|
||||
|
|
|
@ -5,13 +5,7 @@ import { useRootStore } from '@/stores/root.store';
|
|||
import * as projectsApi from '@/api/projects.api';
|
||||
import * as workflowsEEApi from '@/api/workflows.ee';
|
||||
import * as credentialsEEApi from '@/api/credentials.ee';
|
||||
import type {
|
||||
Project,
|
||||
ProjectCreateRequest,
|
||||
ProjectListItem,
|
||||
ProjectUpdateRequest,
|
||||
ProjectsCount,
|
||||
} from '@/types/projects.types';
|
||||
import type { Project, ProjectListItem, ProjectsCount } from '@/types/projects.types';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { hasPermission } from '@/utils/rbac/permissions';
|
||||
|
@ -21,6 +15,7 @@ import { useCredentialsStore } from '@/stores/credentials.store';
|
|||
import { STORES } from '@/constants';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import type { CreateProjectDto, UpdateProjectDto } from '@n8n/api-types';
|
||||
|
||||
export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
||||
const route = useRoute();
|
||||
|
@ -112,26 +107,30 @@ export const useProjectsStore = defineStore(STORES.PROJECTS, () => {
|
|||
currentProject.value = await fetchProject(id);
|
||||
};
|
||||
|
||||
const createProject = async (project: ProjectCreateRequest): Promise<Project> => {
|
||||
const createProject = async (project: CreateProjectDto): Promise<Project> => {
|
||||
const newProject = await projectsApi.createProject(rootStore.restApiContext, project);
|
||||
await getProjectsCount();
|
||||
myProjects.value = [...myProjects.value, newProject as unknown as ProjectListItem];
|
||||
return newProject;
|
||||
};
|
||||
|
||||
const updateProject = async (projectData: ProjectUpdateRequest): Promise<void> => {
|
||||
await projectsApi.updateProject(rootStore.restApiContext, projectData);
|
||||
const projectIndex = myProjects.value.findIndex((p) => p.id === projectData.id);
|
||||
const updateProject = async (
|
||||
id: Project['id'],
|
||||
projectData: Required<UpdateProjectDto>,
|
||||
): Promise<void> => {
|
||||
await projectsApi.updateProject(rootStore.restApiContext, id, projectData);
|
||||
const projectIndex = myProjects.value.findIndex((p) => p.id === id);
|
||||
const { name, icon } = projectData;
|
||||
if (projectIndex !== -1) {
|
||||
myProjects.value[projectIndex].name = projectData.name;
|
||||
myProjects.value[projectIndex].icon = projectData.icon;
|
||||
myProjects.value[projectIndex].name = name;
|
||||
myProjects.value[projectIndex].icon = icon;
|
||||
}
|
||||
if (currentProject.value) {
|
||||
currentProject.value.name = projectData.name;
|
||||
currentProject.value.icon = projectData.icon;
|
||||
currentProject.value.name = name;
|
||||
currentProject.value.icon = icon;
|
||||
}
|
||||
if (projectData.relations) {
|
||||
await getProject(projectData.id);
|
||||
await getProject(id);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -31,10 +31,6 @@ export type ProjectListItem = ProjectSharingData & {
|
|||
role: ProjectRole;
|
||||
scopes?: Scope[];
|
||||
};
|
||||
export type ProjectCreateRequest = { name: string; icon: ProjectIcon };
|
||||
export type ProjectUpdateRequest = Pick<Project, 'id' | 'name' | 'icon'> & {
|
||||
relations: ProjectRelationPayload[];
|
||||
};
|
||||
export type ProjectsCount = Record<ProjectType, number>;
|
||||
|
||||
export type ProjectIcon = {
|
||||
|
|
|
@ -192,9 +192,8 @@ const updateProject = async () => {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
await projectsStore.updateProject({
|
||||
id: projectsStore.currentProject.id,
|
||||
name: formData.value.name,
|
||||
await projectsStore.updateProject(projectsStore.currentProject.id, {
|
||||
name: formData.value.name!,
|
||||
icon: projectIcon.value,
|
||||
relations: formData.value.relations.map((r: ProjectRelation) => ({
|
||||
userId: r.id,
|
||||
|
|
Loading…
Reference in a new issue